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

Commit

Permalink
Convert validation context to data store (#6402)
Browse files Browse the repository at this point in the history
* Add validation reducers, actions, and action types

* Add selector for getValidationErrors

* Export store key and register store

* Export validation store key

* Move TextInput files to checkout package

* Export ValidatedTextInput from blocks-checkout package

* Update imports of ValidatedTextInput to reflect new location

* Use the validation wp-data store for showing error messages

* Export getValidationError in checkout package

* Move validation store to checkout package

* Move ValidationInputError to blocks-checkout package

* Only export "exposedSelectors" from validation

* Convert validation context to data store

* Fixed linting error

* Fixed linting error

* Change the validation selectors to return a function

* Convert reducer and selectors to TS

* Remove superfluous comments and improve test titles

* Test to ensure visible errors remain visible

* Make test for hasValidationErrors more robust

* Augment the wp-data module to include our selectors and actions

* Removed unused `exposedSelectors` variable

* Remove TS error because of `instanceId` on props

* Remove unnecessary as const

* Use function returned by getValidationError

* Use correct selector/action names now context has been decoupled

* hide validation error when input value changes

* Add correct aria-describedBy now we can get error id from store

* Clear validation error from store when component unmounts

* Clear validation error if input is valid

* convert ValidationInputError to TS and get correct id/error from store

* Ensure checkout block doesn't break when there are no errors

* Get validation data from the store instead of context

* Update country input to remove validation context

* Move validation store out of checkout package

* Move TextInput and ValidationInputError back out of the checkout package

* Remove duplicate internal styles comment

* Remove exports that no longer exist

* Get validation store key from block-data

* Make attribute-select-control use validation data store

* Export FieldValidationStatus type

* Make combobox use validation store not context

* Make Address use validation store not context

* Make Address use validation store not context

* Use hasValidationErrors selector as a function in shipping calculator

* Remove validation context from coupon story

* Import VALIDATION_STORE_KEY from correct location

* Stop coupon story from erroring

* Update useStoreCartCoupons to use validation store not context

* Make TotalsCoupon use validation store instead of context

* Make AddToCartFormContext use validation store not context

* Remove ValidationContext

* Import FieldValidationStatus from correct location

* Import ValidatedTextInput and ValidatedTextInput from correct location

* Remove ValidationContextProvider

* Update components to use validation store not context

* Update useValidation to use the data store

* Replace the validation context in checkout-events file

* Use the re-mapped path for the store key import

* Use "register" instead of the deprecated "registerStore"

* Fix import error of the "FieldValidationStatus" type

* Use TS instead of React's "PropTypes"

* Fix the type of "ValidationInputError" in the "payment-method-interface"

* Fix error not showing on the first place order click bug

We were mutating the state in the reducer, which prevented re-rendering
on state change

* Fix state mutation issue in the Validation reducer

Co-authored-by: Thomas Roberts <[email protected]>
Co-authored-by: Saad Tarhi <[email protected]>
  • Loading branch information
3 people authored and alexflorisca committed Sep 28, 2022
1 parent 408bf0e commit 8b6a861
Show file tree
Hide file tree
Showing 40 changed files with 749 additions and 462 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import { decodeEntities } from '@wordpress/html-entities';
import { SelectControl } from 'wordpress-components';
import { useEffect } from 'react';
import classnames from 'classnames';
import {
ValidationInputError,
useValidationContext,
} from '@woocommerce/base-context';
import { ValidationInputError } from '@woocommerce/base-components/validation-input-error';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { useDispatch, useSelect } from '@wordpress/data';

// Default option for select boxes.
const selectAnOption = {
Expand All @@ -32,8 +31,16 @@ const AttributeSelectControl = ( {
'woo-gutenberg-products-block'
),
} ) => {
const { getValidationError, setValidationErrors, clearValidationError } =
useValidationContext();
const { setValidationErrors, clearValidationError } = useDispatch(
VALIDATION_STORE_KEY
);

const { getValidationError } = useSelect( ( select ) => {
const store = select( VALIDATION_STORE_KEY );
return {
getValidationError: store.getValidationError(),
};
} );
const errorId = attributeName;
const error = getValidationError( errorId ) || {};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
BillingStateInput,
ShippingStateInput,
} from '@woocommerce/base-components/state-input';
import { useValidationContext } from '@woocommerce/base-context';
import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { withInstanceId } from '@wordpress/compose';
Expand All @@ -22,6 +21,11 @@ import {
defaultAddressFields,
EnteredAddress,
} from '@woocommerce/settings';
import { useSelect, useDispatch } from '@wordpress/data';
import {
VALIDATION_STORE_KEY,
FieldValidationStatus,
} from '@woocommerce/block-data';

/**
* Internal dependencies
Expand All @@ -32,7 +36,9 @@ import prepareAddressFields from './prepare-address-fields';
// values without having set the country first, show an error.
const validateShippingCountry = (
values: EnteredAddress,
setValidationErrors: ( errors: Record< string, unknown > ) => void,
setValidationErrors: (
errors: Record< string, FieldValidationStatus >
) => void,
clearValidationError: ( error: string ) => void,
hasValidationError: boolean
): void => {
Expand Down Expand Up @@ -87,8 +93,14 @@ const AddressForm = ( {
type = 'shipping',
values,
}: AddressFormProps ): JSX.Element => {
const { getValidationError, setValidationErrors, clearValidationError } =
useValidationContext();
const { setValidationErrors, clearValidationError } = useDispatch(
VALIDATION_STORE_KEY
);

const getValidationError = useSelect( ( select ) => {
const store = select( VALIDATION_STORE_KEY );
return store.getValidationError();
} );

const currentFields = useShallowEqual( fields );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { __ } from '@wordpress/i18n';
import Button from '@woocommerce/base-components/button';
import { useState } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { useValidationContext } from '@woocommerce/base-context';
import type { EnteredAddress, AddressFields } from '@woocommerce/settings';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { useDispatch, useSelect } from '@wordpress/data';

/**
* Internal dependencies
Expand All @@ -25,12 +26,18 @@ const ShippingCalculatorAddress = ( {
addressFields,
}: ShippingCalculatorAddressProps ): JSX.Element => {
const [ address, setAddress ] = useState( initialAddress );
const { hasValidationErrors, showAllValidationErrors } =
useValidationContext();
const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY );

const { hasValidationErrors } = useSelect( ( select ) => {
const store = select( VALIDATION_STORE_KEY );
return {
hasValidationErrors: store.hasValidationErrors,
};
} );

const validateSubmit = () => {
showAllValidationErrors();
return ! hasValidationErrors;
return ! hasValidationErrors();
};

return (
Expand Down
22 changes: 15 additions & 7 deletions assets/js/base/components/cart-checkout/totals/coupon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
import { __ } from '@wordpress/i18n';
import { useState, useEffect, useRef } from '@wordpress/element';
import Button from '@woocommerce/base-components/button';
import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
import { Panel } from '@woocommerce/blocks-checkout';
import Label from '@woocommerce/base-components/label';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { withInstanceId } from '@wordpress/compose';
import {
ValidationInputError,
useValidationContext,
} from '@woocommerce/base-context';
import { Panel } from '@woocommerce/blocks-checkout';
import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
import ValidationInputError from '@woocommerce/base-components/validation-input-error';
import { useSelect } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';

/**
* Internal dependencies
Expand Down Expand Up @@ -46,7 +45,16 @@ export const TotalsCoupon = ( {
}: TotalsCouponProps ): JSX.Element => {
const [ couponValue, setCouponValue ] = useState( '' );
const currentIsLoading = useRef( false );
const { getValidationError, getValidationErrorId } = useValidationContext();
const { getValidationError, getValidationErrorId } = useSelect(
( select ) => {
const store = select( VALIDATION_STORE_KEY );
return {
getValidationError: store.getValidationError(),
getValidationErrorId: store.getValidationErrorId(),
};
}
);

const validationError = getValidationError( 'coupon' );

useEffect( () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
*/
import { useArgs } from '@storybook/client-api';
import { Story, Meta } from '@storybook/react';
import {
useValidationContext,
ValidationContextProvider,
} from '@woocommerce/base-context';
import { INTERACTION_TIMEOUT } from '@woocommerce/storybook-controls';
import { useDispatch } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';

/**
* Internal dependencies
Expand Down Expand Up @@ -52,7 +50,7 @@ LoadingState.args = {
};

export const ErrorState: Story< TotalsCouponProps > = ( args ) => {
const { setValidationErrors } = useValidationContext();
const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );

setValidationErrors( { coupon: INVALID_COUPON_ERROR } );

Expand All @@ -61,10 +59,6 @@ export const ErrorState: Story< TotalsCouponProps > = ( args ) => {

ErrorState.decorators = [
( StoryComponent ) => {
return (
<ValidationContextProvider>
<StoryComponent />
</ValidationContextProvider>
);
return <StoryComponent />;
},
];
16 changes: 10 additions & 6 deletions assets/js/base/components/combobox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import { __ } from '@wordpress/i18n';
import { useEffect, useRef } from '@wordpress/element';
import { withInstanceId } from '@wordpress/compose';
import { ComboboxControl } from 'wordpress-components';
import {
ValidationInputError,
useValidationContext,
} from '@woocommerce/base-context';
import { ValidationInputError } from '@woocommerce/base-components/validation-input-error';
import { isObject } from '@woocommerce/types';
import { useDispatch, useSelect } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';

/**
* Internal dependencies
Expand Down Expand Up @@ -55,8 +54,13 @@ const Combobox = ( {
instanceId = '0',
autoComplete = 'off',
}: ComboboxProps ): JSX.Element => {
const { getValidationError, setValidationErrors, clearValidationError } =
useValidationContext();
const { setValidationErrors, clearValidationError } = useDispatch(
VALIDATION_STORE_KEY
);
const getValidationError = useSelect( ( select ) => {
const store = select( VALIDATION_STORE_KEY );
return store.getValidationError();
} );

const controlRef = useRef< HTMLDivElement >( null );
const controlId = id || 'control-' + instanceId;
Expand Down
19 changes: 6 additions & 13 deletions assets/js/base/components/country-input/stories/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
* External dependencies
*/
import { Story, Meta } from '@storybook/react';
import {
useValidationContext,
ValidationContextProvider,
} from '@woocommerce/base-context';
import { useDispatch } from '@wordpress/data';
import { useState, useEffect } from '@wordpress/element';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';

/**
* Internal dependencies
Expand All @@ -31,21 +29,16 @@ export default {
options: { table: { disable: true } },
value: { control: false },
},
decorators: [
( StoryComponent ) => (
<ValidationContextProvider>
<StoryComponent />
</ValidationContextProvider>
),
],
decorators: [ ( StoryComponent ) => <StoryComponent /> ],
} as Meta< CountryInputWithCountriesProps >;

const Template: Story< CountryInputWithCountriesProps > = ( args ) => {
const [ selectedCountry, selectCountry ] = useState< CountryCode | '' >(
''
);
const { clearValidationError, showValidationError } =
useValidationContext();
const { clearValidationError, showValidationError } = useDispatch(
VALIDATION_STORE_KEY
);

useEffect( () => {
showValidationError( 'country' );
Expand Down
2 changes: 1 addition & 1 deletion assets/js/base/components/state-input/state-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import { useCallback, useMemo, useEffect, useRef } from '@wordpress/element';
import classnames from 'classnames';
import { ValidatedTextInput } from '@woocommerce/base-components/text-input';

/**
* Internal dependencies
*/
import { ValidatedTextInput } from '../text-input';
import Combobox from '../combobox';
import './style.scss';
import type { StateInputWithStatesProps } from './StateInputProps';
Expand Down
59 changes: 25 additions & 34 deletions assets/js/base/components/text-input/validated-text-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,21 @@
import { __ } from '@wordpress/i18n';
import { useCallback, useRef, useEffect, useState } from 'react';
import classnames from 'classnames';
import {
ValidationInputError,
useValidationContext,
} from '@woocommerce/base-context';
import { withInstanceId } from '@wordpress/compose';
import { isString } from '@woocommerce/types';
import { dispatch, useSelect } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';

/**
* Internal dependencies
*/
import TextInput from './text-input';
import './style.scss';
import { ValidationInputError } from '../validation-input-error';

interface ValidatedTextInputPropsWithId {
instanceId?: string;
id: string;
}

interface ValidatedTextInputPropsWithInstanceId {
instanceId: string;
interface ValidatedTextInputProps {
id?: string;
}

type ValidatedTextInputProps = (
| ValidatedTextInputPropsWithId
| ValidatedTextInputPropsWithInstanceId
) & {
instanceId: string;
className?: string;
ariaDescribedBy?: string;
errorId?: string;
Expand All @@ -39,7 +27,7 @@ type ValidatedTextInputProps = (
errorMessage?: string;
onChange: ( newValue: string ) => void;
value: string;
};
}

const ValidatedTextInput = ( {
className,
Expand All @@ -53,20 +41,26 @@ const ValidatedTextInput = ( {
errorMessage: passedErrorMessage = '',
value = '',
...rest
}: ValidatedTextInputProps ) => {
}: ValidatedTextInputProps ): JSX.Element => {
const [ isPristine, setIsPristine ] = useState( true );
const inputRef = useRef< HTMLInputElement >( null );
const {
getValidationError,
hideValidationError,
setValidationErrors,
clearValidationError,
getValidationErrorId,
} = useValidationContext();

const { setValidationErrors, hideValidationError, clearValidationError } =
dispatch( VALIDATION_STORE_KEY );
const textInputId =
typeof id !== 'undefined' ? id : 'textinput-' + instanceId;
const errorIdString = errorId !== undefined ? errorId : textInputId;

const { getValidationError, getValidationErrorId } = useSelect(
( select ) => {
const store = select( VALIDATION_STORE_KEY );
return {
getValidationError: store.getValidationError(),
getValidationErrorId: store.getValidationErrorId(),
};
}
);

const validateInput = useCallback(
( errorsHidden = true ) => {
const inputObject = inputRef.current || null;
Expand All @@ -79,7 +73,7 @@ const ValidatedTextInput = ( {
if ( inputIsValid ) {
clearValidationError( errorIdString );
} else {
setValidationErrors( {
const validationErrors = {
[ errorIdString ]: {
message:
inputObject.validationMessage ||
Expand All @@ -89,7 +83,8 @@ const ValidatedTextInput = ( {
),
hidden: errorsHidden,
},
} );
};
setValidationErrors( validationErrors );
}
},
[ clearValidationError, errorIdString, setValidationErrors ]
Expand Down Expand Up @@ -129,17 +124,13 @@ const ValidatedTextInput = ( {
};
}, [ clearValidationError, errorIdString ] );

// @todo - When useValidationContext is converted to TypeScript, remove this cast and use the correct type.
const errorMessage = ( getValidationError( errorIdString ) || {} ) as {
message?: string;
hidden?: boolean;
};
const errorMessage = getValidationError( errorIdString );

if ( isString( passedErrorMessage ) && passedErrorMessage !== '' ) {
errorMessage.message = passedErrorMessage;
}

const hasError = errorMessage.message && ! errorMessage.hidden;
const hasError = errorMessage?.message && ! errorMessage?.hidden;
const describedBy =
showError && hasError && getValidationErrorId( errorIdString )
? getValidationErrorId( errorIdString )
Expand Down
Loading

0 comments on commit 8b6a861

Please sign in to comment.