Agenda
class SimpleForm extends React.PureComponent {state = { name: "" };render() {const { name } = this.state;return (<form><div><label htmlFor="name">First name:</label><inputtype="text"id="name"value={name}onChange={e => this.setState({ name: e.target.value })}/></div><input type="submit" value="send" /><hr />Your name is {name}</form>);}}
Using state is easy!
function SimpleFormHooks() {const [name, setName] = useState("");return (<form><div><label htmlFor="name">First name:</label><inputtype="text"id="name"value={name}onChange={e => setName(e.target.value)}/></div><input type="submit" value="send" /><hr />Your name is {name}</form>);}
Using hooks is easier!
function Input({ label, value, onChange }) {return (<div><label>{label}</label><input type="text" value={value} onChange={onChange} /></div>);}class SimpleForm extends React.Component {render() {const { name } = this.state;return (<Inputlabel="Enter your name:"value={name}onChange={e => this.setState({ name: e.target.value })}/>);}}
<input type='text'placeholder='Enter your name please...'/>
Scaling up!
import React from 'react';export default class SimpleForm2 extends React.PureComponent {state = { name: '', email: '', address: '', quantity: 0 };render() {const { name, email, address, quantity } = this.state;return (<form onSubmit={this.submit}><div><label htmlFor="quantity">Quantity:</label><inputtype="number"id="quantity"min={0}max={5}step={1}value={quantity}onChange={e =>this.setState({ quantity: parseInt(e.target.value, 10) })}/></div><div><label htmlFor="firstName">First name:</label><inputtype="text"id="firstName"value={name}maxLength={10}onChange={e => this.setState({ name: e.target.value })}/></div><div><label htmlFor="email">Email:</label><inputtype="email"id="email"value={email}onChange={e => this.setState({ email: e.target.value })}/></div><div><label htmlFor="address">Address:</label><inputtype="text"id="address"value={address}requiredonChange={e => this.setState({ address: e.target.value })}/></div><input type="submit" value="send" /><hr />User: {name}, {email}, {address}.</form>);}submit = e => {e.preventDefault();alert('huh?');};}
More realistic example!
But before we start simplifying, let's handle requests...
Now we'll handle it!
import React from 'react';import { sendRequest } from './sendRequest';export default class SimpleForm4 extends React.PureComponent {state = {isSending: false,name: '',email: 'john@smith.com',address: '1st street',quantity: 1,};render() {const { isSending, name, email, address, quantity } = this.state;return (<form onSubmit={this.submit}><div><label htmlFor="quantity">Quantity:</label><inputtype="number"id="quantity"min={0}max={5}step={1}value={quantity}onChange={e =>this.setState({ quantity: parseInt(e.target.value, 10) })}/></div><div><label htmlFor="name">First name:</label><inputtype="text"id="name"value={name}maxLength={10}onChange={e => this.setState({ name: e.target.value })}/></div><div><label htmlFor="email">First name:</label><inputtype="email"id="email"value={email}onChange={e => this.setState({ email: e.target.value })}/></div><div><label htmlFor="address">Address:</label><inputtype="text"id="address"value={address}requiredonChange={e => this.setState({ address: e.target.value })}/></div><input type="submit" value="send" disabled={isSending} /><hr />User: {name}, {email}, {address}.</form>);}submit = e => {e.preventDefault();this.setState({ isSending: true });sendRequest().then(() => {}).catch(() => {}).then(() => this.setState({ isSending: false }));};}
Async is annoying!
Our forms are a mix of:
...and there is more
function Input({ className, label, id, placeholder, type, value, onChange }) {return (<div className={className || ''}><label htmlFor={id}>{label}</label><inputid={id}value={value}onChange={onChange}placeholder={placeholder}type={type}/></div>);}export const Usage = () => (<Inputid="firstName"label="Enter your name:"value={name}placeholder="Your name goes here..."type="text"onChange={e => this.setState({ name: e.target.value })}/>);
function Input({ className, children, id, label, ...rest }) {return (<div className={className || ''}><label htmlFor={id}>{label}</label><input {...rest} /></div>);}export const Usage = () => (<Inputid="firstName"label="Enter your name:"value={name}placeholder="Your name goes here..."type="text"onChange={e => this.setState({ name: e.target.value })}/>);
We have several kind
Define a set of rules, check form input and notify in case of any errors.
import React from 'react';const ErrorField = ({ error }) => {if (!error) return null;return <span style={{ color: 'red' }}>{error}</span>;};export default class ValidationForm1 extends React.PureComponent {state = {field1: '',field2: '',field3: '',field4: '',errors: { field1: '', field2: '', field3: '', field4: '' },};render() {return (<form onSubmit={this.submit} style={{ textAlign: 'left' }}><div><label>I am always ok<inputtype="text"value={this.state.field1}onChange={e => this.setState({ field1: e.target.value })}/><ErrorField error={this.state.errors.field1} /></label></div><div><label>I will let you know if things fail<inputtype="text"value={this.state.field2}onChange={e => this.setState({ field2: e.target.value })}/><ErrorField error={this.state.errors.field2} /></label></div><div><label>I am eager to let you know I am ok<inputtype="text"value={this.state.field3}onChange={e => this.setState({ field3: e.target.value })}onBlur={this.validateField3}/><ErrorField error={this.state.errors.field3} /></label></div><div><label>Don't leave me!<inputtype="text"value={this.state.field4}onChange={e => this.setState({ field4: e.target.value })}onBlur={this.validateField4}/><ErrorField error={this.state.errors.field4} /></label></div><div><input type="submit" disabled={!this.canSubmit()} /></div></form>);}canSubmit = () =>!this.state.errors.field1 &&!this.state.errors.field2 &&!this.state.errors.field3 &&!this.state.errors.field4;submit = e => {e.preventDefault();if (this.state.field2.indexOf('u') !== -1) {this.setError('field2', "We don't like u");} else {this.setError('field2', '');}};validateField3 = e => {const value = e.target.value;if (value.indexOf('u') !== -1) {this.setError('field3', "We don't like u");} else {this.setError('field3', '');}};validateField4 = e => {const value = e.target.value;if (!value) {this.setError('field4', 'This value is required');} else if (value.indexOf('u') !== -1) {this.setError('field4', 'This value has invalid elements: u');} else {this.setError('field4', '');}};setError = (field, error) =>this.setState(({ errors }) => ({errors: { ...errors, [field]: error },}));}
Client validation involves multiple parts
Once the server responds with an error, it can be
{ 'name': 'email', 'type: 'email', 'rules': {'required', 'email regex', 'maxLength=50'} }
TextInput
, PasswordInput
React.Children.map
Field
component with custom rendering
<BindingContext data={this.state} set={this.set}><TextInput name="appAlias" /><TextInput name="deviceToken" /><Select name="mobileDeviceType"><option value="1">Android</option><option value="2">iOS</option></Select><Checkbox name="isDemo" /><Checkbox name="isDev" /></BindingContext>export const BindingContext = ({ data, set, children }) => (<>{transformChildren(children, child =>// skip built in types!!child && !!child.type && typeof child.type !== 'string'? React.cloneElement(child, {data,set,}): child)}</>);function transformChildren(children, apply) {return React.Children.map(children, (child, index) => {let newChild = apply(child, index);if (newChild.props && newChild.props.children)return React.cloneElement(newChild, {children: transformChildren(newChild.props.children, apply),});return newChild;});}
Simple usage
import React from 'react';import Dialog from 'MySuperDialog';import { Formik, Field, Form, ErrorMessage } from 'formik';export const EditUserDialog = ({ user, updateUser, onClose }) => {return (<Dialog onClose={onClose}><h1>Edit User</h1><FormikinitialValues={user /** { email, social } */}onSubmit={(values, actions) => {MyImaginaryRestApiCall(user.id, values).then(updatedUser => {actions.setSubmitting(false);updateUser(updatedUser);onClose();},error => {actions.setSubmitting(false);actions.setErrors(transformMyRestApiErrorsToAnObject(error));actions.setStatus({ msg: 'Set some arbitrary status or data' });});}}render={({ errors, status, touched, isSubmitting }) => (<Form><Field type="email" name="email" /><ErrorMessage name="email" component="div" /><Field type="text" className="error" name="social.facebook" /><ErrorMessage name="social.facebook">{errorMessage => <div className="error">{errorMessage}</div>}</ErrorMessage><Field type="text" name="social.twitter" /><ErrorMessagename="social.twitter"className="error"component="div"/>{status && status.msg && <div>{status.msg}</div>}<button type="submit" disabled={isSubmitting}>Submit</button></Form>)}/></Dialog>);};
Formik
import React from 'react';import { Field, reduxForm } from 'redux-form';const validate = values => {const errors = {};if (!values.username) {errors.username = 'Required';} else if (values.username.length > 15) {errors.username = 'Must be 15 characters or less';}if (!values.email) {errors.email = 'Required';} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {errors.email = 'Invalid email address';}if (!values.age) {errors.age = 'Required';} else if (isNaN(Number(values.age))) {errors.age = 'Must be a number';} else if (Number(values.age) < 18) {errors.age = 'Sorry, you must be at least 18 years old';}return errors;};const warn = values => {const warnings = {};if (values.age < 19) {warnings.age = 'Hmm, you seem a bit young...';}return warnings;};const renderField = ({input,label,type,meta: { touched, error, warning },}) => (<div><label>{label}</label><div><input {...input} placeholder={label} type={type} />{touched &&((error && <span>{error}</span>) ||(warning && <span>{warning}</span>))}</div></div>);const SyncValidationForm = props => {const { handleSubmit, pristine, reset, submitting } = props;return (<form onSubmit={handleSubmit}><Fieldname="username"type="text"component={renderField}label="Username"/><Field name="email" type="email" component={renderField} label="Email" /><Field name="age" type="number" component={renderField} label="Age" /><div><button type="submit" disabled={submitting}>Submit</button><button type="button" disabled={pristine || submitting} onClick={reset}>Clear Values</button></div></form>);};export default reduxForm({form: 'syncValidation', // a unique identifier for this formvalidate, // <--- validation function given to redux-formwarn, // <--- warning function given to redux-form})(SyncValidationForm);
Redux form
import { useForm, useField } from 'react-final-form-hooks';export const MyForm = () => {const { form, handleSubmit, values, pristine, submitting } = useForm({onSubmit, // the function to call with your form values upon valid submitvalidate, // a record-level validation function to check all form values});const firstName = useField('firstName', form);const lastName = useField('lastName', form);return (<form onSubmit={handleSubmit}><div><label>First Name</label><input {...firstName.input} placeholder="First Name" />{firstName.meta.touched && firstName.meta.error && (<span>{firstName.meta.error}</span>)}</div><div><label>Last Name</label><input {...lastName.input} placeholder="Last Name" />{lastName.meta.touched && lastName.meta.error && (<span>{lastName.meta.error}</span>)}</div><button type="submit" disabled={pristine || submitting}>Submit</button></form>);};
React Final Form
Conclusion