* 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:
Joshua T Flowers 2019-08-05 09:41:47 +08:00 committed by GitHub
parent 1a479eefea
commit 919ce11b57
5 changed files with 259 additions and 116 deletions

View File

@ -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>
); );

View File

@ -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" },

View File

@ -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>
);
```

View File

@ -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;

View File

@ -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';