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 { Component, Fragment } from '@wordpress/element';
|
||||||
import { compose } from '@wordpress/compose';
|
import { compose } from '@wordpress/compose';
|
||||||
import { decodeEntities } from '@wordpress/html-entities';
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
import { pickBy } from 'lodash';
|
|
||||||
import { withDispatch } from '@wordpress/data';
|
import { withDispatch } from '@wordpress/data';
|
||||||
import { recordEvent } from 'lib/tracks';
|
import { recordEvent } from 'lib/tracks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal depdencies
|
* Internal depdencies
|
||||||
*/
|
*/
|
||||||
import { H, Card } from '@woocommerce/components';
|
import { H, Card, Form } from '@woocommerce/components';
|
||||||
import withSelect from 'wc-api/with-select';
|
import withSelect from 'wc-api/with-select';
|
||||||
|
|
||||||
class StoreDetails extends Component {
|
class StoreDetails extends Component {
|
||||||
|
@ -23,19 +22,18 @@ class StoreDetails extends Component {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
countryStateOptions: [],
|
countryStateOptions: [],
|
||||||
errors: {},
|
};
|
||||||
fields: {
|
|
||||||
addressLine1: '',
|
this.initialValues = {
|
||||||
addressLine2: '',
|
addressLine1: '',
|
||||||
city: '',
|
addressLine2: '',
|
||||||
countryState: '',
|
city: '',
|
||||||
postCode: '',
|
countryState: '',
|
||||||
isClient: false,
|
postCode: '',
|
||||||
},
|
isClient: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onContinue = this.onContinue.bind( this );
|
this.onContinue = this.onContinue.bind( this );
|
||||||
this.updateValue = this.updateValue.bind( this );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
|
@ -43,49 +41,26 @@ class StoreDetails extends Component {
|
||||||
this.setState( { countryStateOptions } );
|
this.setState( { countryStateOptions } );
|
||||||
}
|
}
|
||||||
|
|
||||||
validateField( name ) {
|
validate( values ) {
|
||||||
const { errors, fields } = this.state;
|
const errors = {};
|
||||||
|
|
||||||
switch ( name ) {
|
if ( ! values.addressLine1.length ) {
|
||||||
case 'addressLine1':
|
errors.addressLine1 = __( 'Please add an address', 'woocommerce-admin' );
|
||||||
errors.addressLine1 = fields.addressLine1.length
|
}
|
||||||
? null
|
if ( ! values.countryState.length ) {
|
||||||
: __( 'Please add an address', 'woocommerce-admin' );
|
errors.countryState = __( 'Please select a country and state', 'woocommerce-admin' );
|
||||||
break;
|
}
|
||||||
case 'countryState':
|
if ( ! values.city.length ) {
|
||||||
errors.countryState = fields.countryState.length
|
errors.city = __( 'Please add a city', 'woocommerce-admin' );
|
||||||
? null
|
}
|
||||||
: __( 'Please select a country and state', 'woocommerce-admin' );
|
if ( ! values.postCode.length ) {
|
||||||
break;
|
errors.postCode = __( 'Please add a post code', 'woocommerce-admin' );
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState( { errors: pickBy( errors ) } );
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateValue( name, value ) {
|
async onContinue( values ) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
createNotice,
|
createNotice,
|
||||||
goToNextStep,
|
goToNextStep,
|
||||||
|
@ -94,31 +69,23 @@ class StoreDetails extends Component {
|
||||||
updateProfileItems,
|
updateProfileItems,
|
||||||
isProfileItemsError,
|
isProfileItemsError,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
|
||||||
addressLine1,
|
|
||||||
addressLine2,
|
|
||||||
city,
|
|
||||||
countryState,
|
|
||||||
postCode,
|
|
||||||
isClient,
|
|
||||||
} = this.state.fields;
|
|
||||||
|
|
||||||
recordEvent( 'storeprofiler_store_details_continue', {
|
recordEvent( 'storeprofiler_store_details_continue', {
|
||||||
store_country: countryState.split( ':' )[ 0 ],
|
store_country: values.countryState.split( ':' )[ 0 ],
|
||||||
setup_client: isClient,
|
setup_client: values.isClient,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await updateSettings( {
|
await updateSettings( {
|
||||||
general: {
|
general: {
|
||||||
woocommerce_store_address: addressLine1,
|
woocommerce_store_address: values.addressLine1,
|
||||||
woocommerce_store_address_2: addressLine2,
|
woocommerce_store_address_2: values.addressLine2,
|
||||||
woocommerce_default_country: countryState,
|
woocommerce_default_country: values.countryState,
|
||||||
woocommerce_store_city: city,
|
woocommerce_store_city: values.city,
|
||||||
woocommerce_store_postcode: postCode,
|
woocommerce_store_postcode: values.postCode,
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await updateProfileItems( { setup_client: isClient } );
|
await updateProfileItems( { setup_client: values.isClient } );
|
||||||
|
|
||||||
if ( ! isSettingsError && ! isProfileItemsError ) {
|
if ( ! isSettingsError && ! isProfileItemsError ) {
|
||||||
goToNextStep();
|
goToNextStep();
|
||||||
|
@ -161,8 +128,7 @@ class StoreDetails extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { countryStateOptions, errors, fields } = this.state;
|
const { countryStateOptions } = this.state;
|
||||||
const { addressLine1, addressLine2, city, countryState, postCode } = fields;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -174,60 +140,55 @@ class StoreDetails extends Component {
|
||||||
</H>
|
</H>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<TextControl
|
<Form
|
||||||
label={ __( 'Address line 1', 'woocommerce-admin' ) }
|
initialValues={ this.initialValues }
|
||||||
onChange={ value => this.updateValue( 'addressLine1', value ) }
|
onSubmitCallback={ this.onContinue }
|
||||||
required
|
validate={ this.validate }
|
||||||
value={ addressLine1 }
|
>
|
||||||
help={ errors.addressLine1 }
|
{ ( { getInputProps, handleSubmit } ) => (
|
||||||
className={ errors.addressLine1 ? 'has-error' : null }
|
<Fragment>
|
||||||
/>
|
<TextControl
|
||||||
|
label={ __( 'Address line 1', 'woocommerce-admin' ) }
|
||||||
|
required
|
||||||
|
{ ...getInputProps( 'addressLine1' ) }
|
||||||
|
/>
|
||||||
|
|
||||||
<TextControl
|
<TextControl
|
||||||
label={ __( 'Address line 2 (optional)', 'woocommerce-admin' ) }
|
label={ __( 'Address line 2 (optional)', 'woocommerce-admin' ) }
|
||||||
onChange={ value => this.updateValue( 'addressLine2', value ) }
|
required
|
||||||
required
|
{ ...getInputProps( 'addressLine2' ) }
|
||||||
value={ addressLine2 }
|
/>
|
||||||
help={ errors.addressLine2 }
|
|
||||||
className={ errors.addressLine2 ? 'has-error' : null }
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectControl
|
<SelectControl
|
||||||
label={ __( 'Country / State', 'woocommerce-admin' ) }
|
label={ __( 'Country / State', 'woocommerce-admin' ) }
|
||||||
onChange={ value => this.updateValue( 'countryState', value ) }
|
required
|
||||||
options={ countryStateOptions }
|
options={ countryStateOptions }
|
||||||
value={ countryState }
|
{ ...getInputProps( 'countryState' ) }
|
||||||
required
|
/>
|
||||||
help={ errors.countryState }
|
|
||||||
className={ errors.countryState ? 'has-error' : null }
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextControl
|
<TextControl
|
||||||
label={ __( 'City', 'woocommerce-admin' ) }
|
label={ __( 'City', 'woocommerce-admin' ) }
|
||||||
onChange={ value => this.updateValue( 'city', value ) }
|
required
|
||||||
required
|
{ ...getInputProps( 'city' ) }
|
||||||
value={ city }
|
/>
|
||||||
help={ errors.city }
|
|
||||||
className={ errors.city ? 'has-error' : null }
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextControl
|
<TextControl
|
||||||
label={ __( 'Post code', 'woocommerce-admin' ) }
|
label={ __( 'Post code', 'woocommerce-admin' ) }
|
||||||
onChange={ value => this.updateValue( 'postCode', value ) }
|
required
|
||||||
required
|
{ ...getInputProps( 'postCode' ) }
|
||||||
value={ postCode }
|
/>
|
||||||
help={ errors.postCode }
|
|
||||||
className={ errors.postCode ? 'has-error' : null }
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CheckboxControl
|
<CheckboxControl
|
||||||
label={ __( 'This store is being set up for a client', 'woocommerce-admin' ) }
|
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' ) }
|
{ __( 'Continue', 'woocommerce-admin' ) }
|
||||||
</Button>
|
</Button>
|
||||||
|
</Fragment>
|
||||||
|
) }
|
||||||
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
{ "component": "EmptyContent" },
|
{ "component": "EmptyContent" },
|
||||||
{ "component": "Filters", "render": "MyReportFilters" },
|
{ "component": "Filters", "render": "MyReportFilters" },
|
||||||
{ "component": "Flag" },
|
{ "component": "Flag" },
|
||||||
|
{ "component": "Form" },
|
||||||
{ "component": "Gravatar" },
|
{ "component": "Gravatar" },
|
||||||
{ "component": "ImageAsset" },
|
{ "component": "ImageAsset" },
|
||||||
{ "component": "Link" },
|
{ "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 EllipsisMenu } from './ellipsis-menu';
|
||||||
export { default as EmptyContent } from './empty-content';
|
export { default as EmptyContent } from './empty-content';
|
||||||
export { default as Flag } from './flag';
|
export { default as Flag } from './flag';
|
||||||
|
export { default as Form } from './form';
|
||||||
export { default as FilterPicker } from './filters/filter';
|
export { default as FilterPicker } from './filters/filter';
|
||||||
export { default as Gravatar } from './gravatar';
|
export { default as Gravatar } from './gravatar';
|
||||||
export { H, Section } from './section';
|
export { H, Section } from './section';
|
||||||
|
|
Loading…
Reference in New Issue