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

Add revalidateDependencies to ValidatedTextInput #9611

Closed
wants to merge 12 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
ShippingAddress,
} from '@woocommerce/settings';
import { useSelect, useDispatch, dispatch } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { FieldValidationStatus } from '@woocommerce/types';

/**
Expand Down Expand Up @@ -96,11 +96,26 @@ const AddressForm = ( {
const { setValidationErrors, clearValidationError } =
useDispatch( VALIDATION_STORE_KEY );

const countryValidationError = useSelect( ( select ) => {
const store = select( VALIDATION_STORE_KEY );
return store.getValidationError( validationErrorId );
const { country, countryValidationError } = useSelect( ( select ) => {
const validationStore = select( VALIDATION_STORE_KEY );
const cartStore = select( CART_STORE_KEY );
return {
countryValidationError:
validationStore.getValidationError( validationErrorId ),
country:
cartStore.getCartData()?.[
type === 'shipping' ? 'shippingAddress' : 'billingAddress'
]?.country,
};
} );

// This object is used to revalidate the specified fields when any dependency changes.
const revalidateDependencies: {
[ K in keyof AddressFields ]?: unknown[];
} = {
postcode: [ country ],
};

const currentFields = useShallowEqual( fields );

const addressFormFields = useMemo( () => {
Expand Down Expand Up @@ -269,6 +284,9 @@ const AddressForm = ( {
label={
field.required ? field.label : field.optionalLabel
}
revalidateDependencies={
revalidateDependencies[ field.key ]
}
value={ values[ field.key ] }
autoCapitalize={ field.autocapitalize }
autoComplete={ field.autocomplete }
Expand Down
8 changes: 0 additions & 8 deletions assets/js/data/cart/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,13 +479,6 @@ export const updateCustomerData =
}
};

export const setFullShippingAddressPushed = (
fullShippingAddressPushed: boolean
) => ( {
type: types.SET_FULL_SHIPPING_ADDRESS_PUSHED,
fullShippingAddressPushed,
} );

type Actions =
| typeof addItemToCart
| typeof applyCoupon
Expand All @@ -506,7 +499,6 @@ type Actions =
| typeof setShippingAddress
| typeof shippingRatesBeingSelected
| typeof updateCustomerData
| typeof setFullShippingAddressPushed
| typeof updatingCustomerData;

export type CartAction = ReturnOrGeneratorYieldUnion< Actions | Thunks >;
1 change: 0 additions & 1 deletion assets/js/data/cart/default-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ export const defaultCartState: CartState = {
applyingCoupon: '',
removingCoupon: '',
isCartDataStale: false,
fullShippingAddressPushed: false,
},
errors: EMPTY_CART_ERRORS,
};
6 changes: 0 additions & 6 deletions assets/js/data/cart/push-changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import isShallowEqual from '@wordpress/is-shallow-equal';
import { STORE_KEY } from './constants';
import { VALIDATION_STORE_KEY } from '../validation';
import { processErrorResponse } from '../utils';
import { shippingAddressHasValidationErrors } from './utils';

type CustomerData = {
billingAddress: CartBillingAddress;
Expand Down Expand Up @@ -212,11 +211,6 @@ const updateCustomerData = debounce( (): void => {
) as BaseAddressKey[] ),
];
}
} )
.finally( () => {
if ( ! shippingAddressHasValidationErrors() ) {
dispatch( STORE_KEY ).setFullShippingAddressPushed( true );
}
} );
}
}, 1000 );
Expand Down
9 changes: 0 additions & 9 deletions assets/js/data/cart/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,6 @@ const reducer: Reducer< CartState > = (
action: Partial< CartAction >
) => {
switch ( action.type ) {
case types.SET_FULL_SHIPPING_ADDRESS_PUSHED:
state = {
...state,
metaData: {
...state.metaData,
fullShippingAddressPushed: action.fullShippingAddressPushed,
},
};
break;
case types.SET_ERROR_DATA:
if ( action.error ) {
state = {
Expand Down
5 changes: 0 additions & 5 deletions assets/js/data/cart/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { CartResponse } from '@woocommerce/types';
*/
import { CART_API_ERROR } from './constants';
import type { CartDispatchFromMap, CartResolveSelectFromMap } from './index';
import { shippingAddressHasValidationErrors } from './utils';

/**
* Resolver for retrieving all cart data.
Expand All @@ -28,10 +27,6 @@ export const getCartData =
receiveError( CART_API_ERROR );
return;
}

if ( ! shippingAddressHasValidationErrors() ) {
dispatch.setFullShippingAddressPushed( true );
}
receiveCart( cartData );
};

Expand Down
7 changes: 0 additions & 7 deletions assets/js/data/cart/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,3 @@ export const getItemsPendingQuantityUpdate = ( state: CartState ): string[] => {
export const getItemsPendingDelete = ( state: CartState ): string[] => {
return state.cartItemsPendingDelete;
};

/**
* Whether the address has changes that have not been synced with the server.
*/
export const getFullShippingAddressPushed = ( state: CartState ): boolean => {
return state.metaData.fullShippingAddressPushed;
};
2 changes: 0 additions & 2 deletions assets/js/types/type-defs/cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,6 @@ export interface CartMeta {
isCartDataStale: boolean;
applyingCoupon: string;
removingCoupon: string;
/* Whether the full address has been previously pushed to the server */
fullShippingAddressPushed: boolean;
}
export interface ExtensionCartUpdateArgs {
data: Record< string, unknown >;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,5 +303,61 @@ describe( 'ValidatedTextInput', () => {
await expect( textInputElement ).toHaveFocus();
await expect( setValidationErrors ).not.toHaveBeenCalled();
} );

it( 'revalidates when revalidateDependencies value changes', async () => {
const setValidationErrors = jest.fn();
wpData.useDispatch.mockImplementation( ( storeName: string ) => {
if ( storeName === VALIDATION_STORE_KEY ) {
return {
...jest
.requireActual( '@wordpress/data' )
.useDispatch( storeName ),
setValidationErrors,
};
}
return jest
.requireActual( '@wordpress/data' )
.useDispatch( storeName );
} );

const TestComponent = ( {
dependencies,
}: {
dependencies: unknown[];
} ) => {
const [ inputValue, setInputValue ] = useState( '' );
return (
<ValidatedTextInput
instanceId={ '6' }
id={ 'test-input' }
onChange={ ( value ) => setInputValue( value ) }
value={ inputValue }
label={ 'Test Input' }
required={ true }
customValidation={ ( inputObject ) => {
return inputObject.value === 'Valid Value';
} }
focusOnMount={ true }
revalidateDependencies={ dependencies }
validateOnMount={ false }
/>
);
};
let dependencyToTrack = 'Test';
const { rerender } = await render(
<TestComponent dependencies={ [ dependencyToTrack ] } />
);
await expect( setValidationErrors ).not.toHaveBeenCalled();
dependencyToTrack = 'Changed';
await rerender(
<TestComponent dependencies={ [ dependencyToTrack ] } />
);
await expect( setValidationErrors ).toHaveBeenCalled();
dependencyToTrack = 'Changed again';
await rerender(
<TestComponent dependencies={ [ dependencyToTrack ] } />
);
await expect( setValidationErrors ).toHaveBeenCalledTimes( 2 );
} );
} );
} );
32 changes: 25 additions & 7 deletions packages/checkout/components/text-input/validated-text-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ interface ValidatedTextInputProps
| undefined;
// Whether validation should run when focused - only has an effect when focusOnMount is also true.
validateOnMount?: boolean | undefined;
// A set of dependencies to watch, and revalidate if they change.
revalidateDependencies?: unknown[] | undefined;
}

const ValidatedTextInput = ( {
Expand All @@ -67,6 +69,7 @@ const ValidatedTextInput = ( {
customValidation,
label,
validateOnMount = true,
revalidateDependencies = [],
...rest
}: ValidatedTextInputProps ): JSX.Element => {
const [ isPristine, setIsPristine ] = useState( true );
Expand Down Expand Up @@ -99,10 +102,6 @@ const ValidatedTextInput = ( {
inputObject.value = inputObject.value.trim();
inputObject.setCustomValidity( '' );

if ( previousValue === inputObject.value ) {
return;
}

const inputIsValid = customValidation
? inputObject.checkValidity() && customValidation( inputObject )
: inputObject.checkValidity();
Expand All @@ -122,7 +121,6 @@ const ValidatedTextInput = ( {
} );
},
[
previousValue,
clearValidationError,
customValidation,
errorIdString,
Expand All @@ -131,6 +129,15 @@ const ValidatedTextInput = ( {
]
);

useEffect( () => {
if ( isPristine ) {
return;
}
validateInput( value === '' );
// Purposely skip running this unless any of the revalidateDependencies change. Also don't run it on mount (isPristine).
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ...revalidateDependencies ] );

/**
* Handle browser autofill / changes via data store.
*
Expand Down Expand Up @@ -170,7 +177,7 @@ const ValidatedTextInput = ( {

// if validateOnMount is false, only validate input if focusOnMount is also false
if ( validateOnMount || ! focusOnMount ) {
validateInput( true );
validateInput();
}

setIsPristine( false );
Expand Down Expand Up @@ -220,12 +227,23 @@ const ValidatedTextInput = ( {
hideValidationError( errorIdString );

// Revalidate on user input so we know if the value is valid.
validateInput( true );
validateInput();

// Push the changes up to the parent component if the value is valid.
onChange( val );
} }
onBlur={ () => {
// Don't validate on blur if the value is unchanged and the field is not required.
const inputObject = inputRef.current || null;

if (
inputObject &&
inputObject.value === previousValue &&
! inputObject.required &&
inputObject.value !== ''
) {
return;
}
validateInput( false );
} }
ariaDescribedBy={ describedBy }
Expand Down