Add form component for handling form state (https://github.com/woocommerce/woocommerce-admin/pull/2742)
* Add Form component to packages * Add form example to devdocs * Fix form validation check * Update store details to use Form component * Fix indentation issues
This commit is contained in:
parent
1a479eefea
commit
919ce11b57
|
@ -7,14 +7,13 @@ import { Button, SelectControl, TextControl, CheckboxControl } from 'newspack-co
|
|||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { pickBy } from 'lodash';
|
||||
import { withDispatch } from '@wordpress/data';
|
||||
import { recordEvent } from 'lib/tracks';
|
||||
|
||||
/**
|
||||
* Internal depdencies
|
||||
*/
|
||||
import { H, Card } from '@woocommerce/components';
|
||||
import { H, Card, Form } from '@woocommerce/components';
|
||||
import withSelect from 'wc-api/with-select';
|
||||
|
||||
class StoreDetails extends Component {
|
||||
|
@ -23,19 +22,18 @@ class StoreDetails extends Component {
|
|||
|
||||
this.state = {
|
||||
countryStateOptions: [],
|
||||
errors: {},
|
||||
fields: {
|
||||
};
|
||||
|
||||
this.initialValues = {
|
||||
addressLine1: '',
|
||||
addressLine2: '',
|
||||
city: '',
|
||||
countryState: '',
|
||||
postCode: '',
|
||||
isClient: false,
|
||||
},
|
||||
};
|
||||
|
||||
this.onContinue = this.onContinue.bind( this );
|
||||
this.updateValue = this.updateValue.bind( this );
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -43,49 +41,26 @@ class StoreDetails extends Component {
|
|||
this.setState( { countryStateOptions } );
|
||||
}
|
||||
|
||||
validateField( name ) {
|
||||
const { errors, fields } = this.state;
|
||||
validate( values ) {
|
||||
const errors = {};
|
||||
|
||||
switch ( name ) {
|
||||
case 'addressLine1':
|
||||
errors.addressLine1 = fields.addressLine1.length
|
||||
? null
|
||||
: __( 'Please add an address', 'woocommerce-admin' );
|
||||
break;
|
||||
case 'countryState':
|
||||
errors.countryState = fields.countryState.length
|
||||
? null
|
||||
: __( 'Please select a country and state', 'woocommerce-admin' );
|
||||
break;
|
||||
case 'city':
|
||||
errors.city = fields.city.length ? null : __( 'Please add a city', 'woocommerce-admin' );
|
||||
break;
|
||||
case 'postCode':
|
||||
errors.postCode = fields.postCode.length
|
||||
? null
|
||||
: __( 'Please add a post code', 'woocommerce-admin' );
|
||||
break;
|
||||
if ( ! values.addressLine1.length ) {
|
||||
errors.addressLine1 = __( 'Please add an address', 'woocommerce-admin' );
|
||||
}
|
||||
if ( ! values.countryState.length ) {
|
||||
errors.countryState = __( 'Please select a country and state', 'woocommerce-admin' );
|
||||
}
|
||||
if ( ! values.city.length ) {
|
||||
errors.city = __( 'Please add a city', 'woocommerce-admin' );
|
||||
}
|
||||
if ( ! values.postCode.length ) {
|
||||
errors.postCode = __( 'Please add a post code', 'woocommerce-admin' );
|
||||
}
|
||||
|
||||
this.setState( { errors: pickBy( errors ) } );
|
||||
}
|
||||
|
||||
updateValue( name, value ) {
|
||||
const fields = { ...this.state.fields, [ name ]: value };
|
||||
this.setState( { fields }, () => this.validateField( name ) );
|
||||
}
|
||||
|
||||
async validateForm() {
|
||||
const { fields } = this.state;
|
||||
Object.keys( fields ).forEach( fieldName => this.validateField( fieldName ) );
|
||||
}
|
||||
|
||||
async onContinue() {
|
||||
await this.validateForm();
|
||||
if ( Object.keys( this.state.errors ).length ) {
|
||||
return;
|
||||
return errors;
|
||||
}
|
||||
|
||||
async onContinue( values ) {
|
||||
const {
|
||||
createNotice,
|
||||
goToNextStep,
|
||||
|
@ -94,31 +69,23 @@ class StoreDetails extends Component {
|
|||
updateProfileItems,
|
||||
isProfileItemsError,
|
||||
} = this.props;
|
||||
const {
|
||||
addressLine1,
|
||||
addressLine2,
|
||||
city,
|
||||
countryState,
|
||||
postCode,
|
||||
isClient,
|
||||
} = this.state.fields;
|
||||
|
||||
recordEvent( 'storeprofiler_store_details_continue', {
|
||||
store_country: countryState.split( ':' )[ 0 ],
|
||||
setup_client: isClient,
|
||||
store_country: values.countryState.split( ':' )[ 0 ],
|
||||
setup_client: values.isClient,
|
||||
} );
|
||||
|
||||
await updateSettings( {
|
||||
general: {
|
||||
woocommerce_store_address: addressLine1,
|
||||
woocommerce_store_address_2: addressLine2,
|
||||
woocommerce_default_country: countryState,
|
||||
woocommerce_store_city: city,
|
||||
woocommerce_store_postcode: postCode,
|
||||
woocommerce_store_address: values.addressLine1,
|
||||
woocommerce_store_address_2: values.addressLine2,
|
||||
woocommerce_default_country: values.countryState,
|
||||
woocommerce_store_city: values.city,
|
||||
woocommerce_store_postcode: values.postCode,
|
||||
},
|
||||
} );
|
||||
|
||||
await updateProfileItems( { setup_client: isClient } );
|
||||
await updateProfileItems( { setup_client: values.isClient } );
|
||||
|
||||
if ( ! isSettingsError && ! isProfileItemsError ) {
|
||||
goToNextStep();
|
||||
|
@ -161,8 +128,7 @@ class StoreDetails extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { countryStateOptions, errors, fields } = this.state;
|
||||
const { addressLine1, addressLine2, city, countryState, postCode } = fields;
|
||||
const { countryStateOptions } = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
@ -174,60 +140,55 @@ class StoreDetails extends Component {
|
|||
</H>
|
||||
|
||||
<Card>
|
||||
<Form
|
||||
initialValues={ this.initialValues }
|
||||
onSubmitCallback={ this.onContinue }
|
||||
validate={ this.validate }
|
||||
>
|
||||
{ ( { getInputProps, handleSubmit } ) => (
|
||||
<Fragment>
|
||||
<TextControl
|
||||
label={ __( 'Address line 1', 'woocommerce-admin' ) }
|
||||
onChange={ value => this.updateValue( 'addressLine1', value ) }
|
||||
required
|
||||
value={ addressLine1 }
|
||||
help={ errors.addressLine1 }
|
||||
className={ errors.addressLine1 ? 'has-error' : null }
|
||||
{ ...getInputProps( 'addressLine1' ) }
|
||||
/>
|
||||
|
||||
<TextControl
|
||||
label={ __( 'Address line 2 (optional)', 'woocommerce-admin' ) }
|
||||
onChange={ value => this.updateValue( 'addressLine2', value ) }
|
||||
required
|
||||
value={ addressLine2 }
|
||||
help={ errors.addressLine2 }
|
||||
className={ errors.addressLine2 ? 'has-error' : null }
|
||||
{ ...getInputProps( 'addressLine2' ) }
|
||||
/>
|
||||
|
||||
<SelectControl
|
||||
label={ __( 'Country / State', 'woocommerce-admin' ) }
|
||||
onChange={ value => this.updateValue( 'countryState', value ) }
|
||||
options={ countryStateOptions }
|
||||
value={ countryState }
|
||||
required
|
||||
help={ errors.countryState }
|
||||
className={ errors.countryState ? 'has-error' : null }
|
||||
options={ countryStateOptions }
|
||||
{ ...getInputProps( 'countryState' ) }
|
||||
/>
|
||||
|
||||
<TextControl
|
||||
label={ __( 'City', 'woocommerce-admin' ) }
|
||||
onChange={ value => this.updateValue( 'city', value ) }
|
||||
required
|
||||
value={ city }
|
||||
help={ errors.city }
|
||||
className={ errors.city ? 'has-error' : null }
|
||||
{ ...getInputProps( 'city' ) }
|
||||
/>
|
||||
|
||||
<TextControl
|
||||
label={ __( 'Post code', 'woocommerce-admin' ) }
|
||||
onChange={ value => this.updateValue( 'postCode', value ) }
|
||||
required
|
||||
value={ postCode }
|
||||
help={ errors.postCode }
|
||||
className={ errors.postCode ? 'has-error' : null }
|
||||
{ ...getInputProps( 'postCode' ) }
|
||||
/>
|
||||
|
||||
<CheckboxControl
|
||||
label={ __( 'This store is being set up for a client', 'woocommerce-admin' ) }
|
||||
onChange={ value => this.updateValue( 'isClient', value ) }
|
||||
{ ...getInputProps( 'isClient' ) }
|
||||
/>
|
||||
|
||||
<Button isPrimary onClick={ this.onContinue }>
|
||||
<Button isPrimary onClick={ handleSubmit }>
|
||||
{ __( 'Continue', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
</Fragment>
|
||||
) }
|
||||
</Form>
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
{ "component": "EmptyContent" },
|
||||
{ "component": "Filters", "render": "MyReportFilters" },
|
||||
{ "component": "Flag" },
|
||||
{ "component": "Form" },
|
||||
{ "component": "Gravatar" },
|
||||
{ "component": "ImageAsset" },
|
||||
{ "component": "Link" },
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
```jsx
|
||||
import { Button TextControl } from '@wordpress/components';
|
||||
import { Form } from '@woocommerce/components';
|
||||
|
||||
const validate = ( values ) => {
|
||||
const errors = {};
|
||||
if ( ! values.firstName ) {
|
||||
errors.firstName = 'First name is required';
|
||||
}
|
||||
if ( values.lastName.length < 3 ) {
|
||||
errors.lastName = 'Last name must be at least 3 characters';
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
const onSubmitCallback = ( values ) => console.log( values );
|
||||
const initialValues = { firstName: '', lastName: '' };
|
||||
|
||||
const MyForm = () => (
|
||||
<Form validate={ validate } onSubmitCallback={ onSubmitCallback } initialValues={ initialValues }>
|
||||
{ ( {
|
||||
getInputProps,
|
||||
values,
|
||||
errors,
|
||||
setValue,
|
||||
handleSubmit,
|
||||
} ) => (
|
||||
<div>
|
||||
<TextControl
|
||||
label={ 'First Name' }
|
||||
{ ...getInputProps( 'firstName' ) }
|
||||
/>
|
||||
<TextControl
|
||||
label={ 'Last Name' }
|
||||
{ ...getInputProps( 'lastName' ) }
|
||||
/>
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={ handleSubmit }
|
||||
disabled={ Object.keys( errors ).length }
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<br />
|
||||
<br />
|
||||
Values: { JSON.stringify( values ) }<br />
|
||||
Errors: { JSON.stringify( errors ) }<br />
|
||||
</div>
|
||||
) }
|
||||
</Form>
|
||||
);
|
||||
```
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { cloneElement, Component } from '@wordpress/element';
|
||||
import { noop } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* A form component to handle form state and provide input helper props.
|
||||
*/
|
||||
class Form extends Component {
|
||||
constructor( props ) {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
values: props.initialValues,
|
||||
errors: props.errors,
|
||||
touched: props.touched,
|
||||
};
|
||||
|
||||
this.getInputProps = this.getInputProps.bind( this );
|
||||
this.handleSubmit = this.handleSubmit.bind( this );
|
||||
this.setValue = this.setValue.bind( this );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.validate();
|
||||
}
|
||||
|
||||
async isValidForm() {
|
||||
await this.validate();
|
||||
return ! Object.keys( this.state.errors ).length;
|
||||
}
|
||||
|
||||
validate() {
|
||||
const { values } = this.state;
|
||||
const errors = this.props.validate( values );
|
||||
this.setState( { errors } );
|
||||
}
|
||||
|
||||
setValue( name, value ) {
|
||||
this.setState( prevState => ( {
|
||||
values: { ...prevState.values, [ name ]: value },
|
||||
} ), this.validate );
|
||||
}
|
||||
|
||||
handleBlur( name ) {
|
||||
this.setState( prevState => ( {
|
||||
touched: { ...prevState.touched, [ name ]: true },
|
||||
} ) );
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
const { values } = this.state;
|
||||
const touched = {};
|
||||
Object.keys( values ).map( name => touched[ name ] = true );
|
||||
this.setState( { touched } );
|
||||
|
||||
if ( await this.isValidForm() ) {
|
||||
this.props.onSubmitCallback( values );
|
||||
}
|
||||
}
|
||||
|
||||
getInputProps( name ) {
|
||||
const { errors, touched, values } = this.state;
|
||||
|
||||
return {
|
||||
value: values[ name ],
|
||||
onChange: ( value ) => this.setValue( name, value ),
|
||||
onBlur: () => this.handleBlur( name ),
|
||||
className: touched[ name ] && errors[ name ] ? 'has-error' : null,
|
||||
help: touched[ name ] ? errors[ name ] : null,
|
||||
};
|
||||
}
|
||||
|
||||
getStateAndHelpers() {
|
||||
const { values, errors, touched } = this.state;
|
||||
|
||||
return {
|
||||
values,
|
||||
errors,
|
||||
touched,
|
||||
setValue: this.setValue,
|
||||
handleSubmit: this.handleSubmit,
|
||||
getInputProps: this.getInputProps,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const element = this.props.children( this.getStateAndHelpers() );
|
||||
return cloneElement( element );
|
||||
}
|
||||
}
|
||||
|
||||
Form.propTypes = {
|
||||
/**
|
||||
* A renderable component in which to pass this component's state and helpers.
|
||||
* Generally a number of input or other form elements.
|
||||
*/
|
||||
children: PropTypes.any,
|
||||
/**
|
||||
* Object of all initial errors to store in state.
|
||||
*/
|
||||
errors: PropTypes.object,
|
||||
/**
|
||||
* Object key:value pair list of all initial field values.
|
||||
*/
|
||||
initialValues: PropTypes.object.isRequired,
|
||||
/**
|
||||
* Function to call when a form is submitted with valid fields.
|
||||
*/
|
||||
onSubmitCallback: PropTypes.func,
|
||||
/**
|
||||
* A function that is passed a list of all values and
|
||||
* should return an `errors` object with error response.
|
||||
*/
|
||||
validate: PropTypes.func,
|
||||
};
|
||||
|
||||
Form.defaultProps = {
|
||||
errors: {},
|
||||
initialValues: {},
|
||||
touched: {},
|
||||
onSubmitCallback: noop,
|
||||
};
|
||||
|
||||
export default Form;
|
|
@ -20,6 +20,7 @@ export { default as DropdownButton } from './dropdown-button';
|
|||
export { default as EllipsisMenu } from './ellipsis-menu';
|
||||
export { default as EmptyContent } from './empty-content';
|
||||
export { default as Flag } from './flag';
|
||||
export { default as Form } from './form';
|
||||
export { default as FilterPicker } from './filters/filter';
|
||||
export { default as Gravatar } from './gravatar';
|
||||
export { H, Section } from './section';
|
||||
|
|
Loading…
Reference in New Issue