Skip to content
This repository was archived by the owner on Feb 23, 2024. It is now read-only.

Commit 854d258

Browse files
mikejolleysenadir
andauthored
Store billing address to local state and persist globally to prevent loss of data (#2374)
* Remove unused shippingAsBilling prop from billing data context * Move functions out of component and add docblocks * Local address state * Refactor into new custom hook * Remove TODO and code fixed in core * useShallowEqual to prevent updates on all field changes * Fix stale validation errors * cleanup * Should be setting local state not global state for email and phone * Combine useEffects and pass correct deps * Update assets/js/base/hooks/checkout/use-checkout-address.js Co-authored-by: Seghir Nadir <[email protected]> * Prettier * Move validation update check into updateValidationError * Fix state updaters * Fix context definition for setShippingAddress * Fix validation updates * errorId dep * Reapply changes to checkout block * Update equality checks Co-authored-by: Seghir Nadir <[email protected]>
1 parent ac08822 commit 854d258

File tree

10 files changed

+294
-178
lines changed

10 files changed

+294
-178
lines changed

assets/js/base/components/select/validated.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { __ } from '@wordpress/i18n';
55
import { useEffect } from 'react';
66
import { useValidationContext } from '@woocommerce/base-context';
7+
import { useShallowEqual } from '@woocommerce/base-hooks';
78
import PropTypes from 'prop-types';
89
import classnames from 'classnames';
910
import { withInstanceId } from '@woocommerce/base-hocs/with-instance-id';
@@ -30,13 +31,18 @@ const ValidatedSelect = ( {
3031
} ) => {
3132
const selectId = id || 'select-' + instanceId;
3233
errorId = errorId || selectId;
34+
35+
// Prevents re-renders when value is an object, e.g. {key: "NY", name: "New York"}
36+
const currentValue = useShallowEqual( value );
37+
3338
const {
3439
getValidationError,
3540
setValidationErrors,
3641
clearValidationError,
3742
} = useValidationContext();
43+
3844
const validateSelect = () => {
39-
if ( ! required || value ) {
45+
if ( ! required || currentValue ) {
4046
clearValidationError( errorId );
4147
} else {
4248
setValidationErrors( {
@@ -50,14 +56,14 @@ const ValidatedSelect = ( {
5056

5157
useEffect( () => {
5258
validateSelect();
53-
}, [ value ] );
59+
}, [ currentValue ] );
5460

5561
// Remove validation errors when unmounted.
5662
useEffect( () => {
5763
return () => {
5864
clearValidationError( errorId );
5965
};
60-
}, [] );
66+
}, [ errorId ] );
6167

6268
const error = getValidationError( errorId ) || {};
6369

@@ -68,7 +74,7 @@ const ValidatedSelect = ( {
6874
'has-error': error.message && ! error.hidden,
6975
} ) }
7076
feedback={ <ValidationInputError propertyName={ errorId } /> }
71-
value={ value }
77+
value={ currentValue }
7278
{ ...rest }
7379
/>
7480
);

assets/js/base/components/state-input/state-input.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { __ } from '@wordpress/i18n';
55
import PropTypes from 'prop-types';
66
import { decodeEntities } from '@wordpress/html-entities';
7-
import { useCallback, useEffect } from '@wordpress/element';
7+
import { useCallback } from '@wordpress/element';
88

99
/**
1010
* Internal dependencies
@@ -30,15 +30,7 @@ const StateInput = ( {
3030
name: decodeEntities( countryStates[ key ] ),
3131
} ) )
3232
: [];
33-
// @todo: remove this code block when issue https://github.com/woocommerce/woocommerce/issues/25854 is merged
34-
// Defaults to the first state when selecting a country with states, this is here
35-
// until a bug in Woo core is fixed.
36-
// see: https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/1919
37-
useEffect( () => {
38-
if ( ! value && options.length ) {
39-
onChangeState( options[ 0 ].key );
40-
}
41-
}, [ country ] );
33+
4234
/**
4335
* Handles state selection onChange events. Finds a matching state by key or value.
4436
*

assets/js/base/components/text-input/validated.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const ValidatedTextInput = ( {
3737

3838
const textInputId = id || 'textinput-' + instanceId;
3939
errorId = errorId || textInputId;
40+
4041
const validateInput = ( errorsHidden = true ) => {
4142
if ( inputRef.current.checkValidity() ) {
4243
clearValidationError( errorId );
@@ -63,7 +64,7 @@ const ValidatedTextInput = ( {
6364
return () => {
6465
clearValidationError( errorId );
6566
};
66-
}, [] );
67+
}, [ errorId ] );
6768

6869
const errorMessage = getValidationError( errorId ) || {};
6970
const hasError = errorMessage.message && ! errorMessage.hidden;

assets/js/base/context/cart-checkout/billing/constants.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export const DEFAULT_BILLING_DATA = {
2727
country: '',
2828
email: '',
2929
phone: '',
30-
shippingAsBilling: true,
3130
};
3231

3332
const billingAddress = mapValues( checkoutData.billing_address, ( value ) =>

assets/js/base/context/cart-checkout/validation/index.js

Lines changed: 91 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
useState,
99
} from '@wordpress/element';
1010
import { omit, pickBy } from 'lodash';
11+
import isShallowEqual from '@wordpress/is-shallow-equal';
1112

1213
/**
1314
* @typedef { import('@woocommerce/type-defs/contexts').ValidationContext } ValidationContext
@@ -49,7 +50,29 @@ export const ValidationContextProvider = ( { children } ) => {
4950
*
5051
* @return {Object} The error object for the given property.
5152
*/
52-
const getValidationError = ( property ) => validationErrors[ property ];
53+
const getValidationError = useCallback(
54+
( property ) => validationErrors[ property ],
55+
[ validationErrors ]
56+
);
57+
58+
/**
59+
* Provides an id for the validation error that can be used to fill out
60+
* aria-describedby attribute values.
61+
*
62+
* @param {string} errorId The input css id the validation error is related
63+
* to.
64+
* @return {string} The id to use for the validation error container.
65+
*/
66+
const getValidationErrorId = useCallback(
67+
( errorId ) => {
68+
const error = validationErrors[ errorId ];
69+
if ( ! error || error.hidden ) {
70+
return '';
71+
}
72+
return `validate-error-${ errorId }`;
73+
},
74+
[ validationErrors ]
75+
);
5376

5477
/**
5578
* Clears any validation error that exists in state for the given property
@@ -59,9 +82,12 @@ export const ValidationContextProvider = ( { children } ) => {
5982
* validation error state.
6083
*/
6184
const clearValidationError = ( property ) => {
62-
if ( validationErrors[ property ] ) {
63-
updateValidationErrors( omit( validationErrors, [ property ] ) );
64-
}
85+
updateValidationErrors( ( prevErrors ) => {
86+
if ( ! prevErrors[ property ] ) {
87+
return prevErrors;
88+
}
89+
return omit( prevErrors, [ property ] );
90+
} );
6591
};
6692

6793
/**
@@ -76,38 +102,51 @@ export const ValidationContextProvider = ( { children } ) => {
76102
* validation error is for and values are the
77103
* validation error message displayed to the user.
78104
*/
79-
const setValidationErrors = useCallback(
80-
( newErrors ) => {
81-
if ( ! newErrors ) {
82-
return;
83-
}
84-
// all values must be a string.
85-
newErrors = pickBy(
86-
newErrors,
87-
( { message } ) => typeof message === 'string'
88-
);
89-
if ( Object.values( newErrors ).length > 0 ) {
90-
updateValidationErrors( ( prevErrors ) => ( {
91-
...prevErrors,
92-
...newErrors,
93-
} ) );
105+
const setValidationErrors = ( newErrors ) => {
106+
if ( ! newErrors ) {
107+
return;
108+
}
109+
updateValidationErrors( ( prevErrors ) => {
110+
newErrors = pickBy( newErrors, ( error, property ) => {
111+
if ( typeof error.message !== 'string' ) {
112+
return false;
113+
}
114+
if ( prevErrors.hasOwnProperty( property ) ) {
115+
return ! isShallowEqual( prevErrors[ property ], error );
116+
}
117+
return true;
118+
} );
119+
if ( Object.values( newErrors ).length === 0 ) {
120+
return prevErrors;
94121
}
95-
},
96-
[ updateValidationErrors ]
97-
);
122+
return {
123+
...prevErrors,
124+
...newErrors,
125+
};
126+
} );
127+
};
98128

129+
/**
130+
* Used to update a validation error.
131+
*
132+
* @param {string} property The name of the property to update.
133+
* @param {Object} newError New validation error object.
134+
*/
99135
const updateValidationError = ( property, newError ) => {
100136
updateValidationErrors( ( prevErrors ) => {
101137
if ( ! prevErrors.hasOwnProperty( property ) ) {
102138
return prevErrors;
103139
}
104-
return {
105-
...prevErrors,
106-
[ property ]: {
107-
...prevErrors[ property ],
108-
...newError,
109-
},
140+
const updatedError = {
141+
...prevErrors[ property ],
142+
...newError,
110143
};
144+
return isShallowEqual( prevErrors[ property ], updatedError )
145+
? prevErrors
146+
: {
147+
...prevErrors,
148+
[ property ]: updatedError,
149+
};
111150
} );
112151
};
113152

@@ -118,11 +157,10 @@ export const ValidationContextProvider = ( { children } ) => {
118157
* @param {string} property The name of the property to set the `hidden`
119158
* value to true.
120159
*/
121-
const hideValidationError = ( property ) => {
122-
updateValidationError( property, {
160+
const hideValidationError = ( property ) =>
161+
void updateValidationError( property, {
123162
hidden: true,
124163
} );
125-
};
126164

127165
/**
128166
* Given a property name and if an associated error exists, it sets its
@@ -131,43 +169,36 @@ export const ValidationContextProvider = ( { children } ) => {
131169
* @param {string} property The name of the property to set the `hidden`
132170
* value to false.
133171
*/
134-
const showValidationError = ( property ) => {
135-
updateValidationError( property, {
172+
const showValidationError = ( property ) =>
173+
void updateValidationError( property, {
136174
hidden: false,
137175
} );
138-
};
139176

140177
/**
141178
* Sets the `hidden` value of all errors to `false`.
142179
*/
143-
const showAllValidationErrors = () => {
144-
updateValidationErrors( ( prevErrors ) => {
145-
const newErrors = {};
180+
const showAllValidationErrors = () =>
181+
void updateValidationErrors( ( prevErrors ) => {
182+
const updatedErrors = {};
183+
146184
Object.keys( prevErrors ).forEach( ( property ) => {
147-
newErrors[ property ] = {
148-
...prevErrors[ property ],
149-
hidden: false,
150-
};
185+
if ( prevErrors[ property ].hidden ) {
186+
updatedErrors[ property ] = {
187+
...prevErrors[ property ],
188+
hidden: false,
189+
};
190+
}
151191
} );
152-
return newErrors;
153-
} );
154-
};
155192

156-
/**
157-
* Provides an id for the validation error that can be used to fill out
158-
* aria-describedby attribute values.
159-
*
160-
* @param {string} errorId The input css id the validation error is related
161-
* to.
162-
* @return {string} The id to use for the validation error container.
163-
*/
164-
const getValidationErrorId = ( errorId ) => {
165-
const error = getValidationError( errorId );
166-
if ( ! error || error.hidden ) {
167-
return '';
168-
}
169-
return `validate-error-${ errorId }`;
170-
};
193+
if ( Object.values( updatedErrors ).length === 0 ) {
194+
return prevErrors;
195+
}
196+
197+
return {
198+
...prevErrors,
199+
...updatedErrors,
200+
};
201+
} );
171202

172203
const context = {
173204
getValidationError,
@@ -180,6 +211,7 @@ export const ValidationContextProvider = ( { children } ) => {
180211
hasValidationErrors: Object.keys( validationErrors ).length > 0,
181212
getValidationErrorId,
182213
};
214+
183215
return (
184216
<ValidationContext.Provider value={ context }>
185217
{ children }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './use-checkout-redirect-url';
2+
export * from './use-checkout-address';
23
export * from './use-checkout-submit';
34
export * from './use-emit-response';

0 commit comments

Comments
 (0)