taming forms with react
TRANSCRIPT
REACT FORMS... a crime scene ...
CRIME SCENE - DO NOT CROSS
mehiel@DOM
React, since 2015Logicea, since 2013CanJS, since 2011ExtJS, since 2010jQuery, since 2008Web, since 2004
Forms: why bother?
InevitableStill a PainBusiness InterestFun?
Forms: pwn factor
|-------------------------------------------∞
^ ^ ^ ^
html react TODAY pwned
Forms: watch for
the Designerthe Userthe APIthe Browser
Forms: ingredients
Flexible LayoutVariety of InputsDynamic FieldsDynamic ValuesField DependenciesLookup DataValidationHints & ErrorsField StatusRequests & Responses
React: the weapon
Declarative
Painless interactive UIs.Design views for each state in your appPredictable and easier to debug
Component Based
Encapsulated with own stateComposable to make complex UIsKeep state out of the DOM
// the simplest react component as of React 16
function WorldGreeter() {
return "Hello, World"
}
<WorldGreeter /> => "Hello, World"
// with some JSX sauce
function FriendGreeter({ name }) {
return <div>Hello {name}!</div>
}
<FriendGreeter name="Bob" /> => "Hello Bob!"
class NobleGreeter extends React.Component {
// componentLifecycleFunctions()
// this.setState()
render() {
return (
<div>
Hello {this.props.title} {this.props.name}
</div>
)
}
}
<NobleGreeter title="sir" name="William" /> => "Hello sir William"
REACT FORMSa crime in 3 acts
ACT 1the victim
Act 1 : Scene 1 : ■
function ProfileForm1() {
return (
<form>
<input type="text" name="username" />
<input type="text" name="email" />
<input type="password" name="password" />
</form>
)
}
Act 1 : Scene 1 : ▸
Act 1 : Scene 2 : ■
<div>
<label htmlFor="username">Username</label>
<input type="text" name="username" id="username" />
</div>
<div>
<label htmlFor="email">E-mail</label>
<input type="text" name="email" id="email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" name="password" id="password" />
</div>
Act 1 : Scene 2 : ▸
UsernameE-mail
Password
Act 1 : Scene 3 : ■
export class ProfileForm3 extends Component {
onSubmit = (ev) => {
ev.preventDefault() // disable browser post
const data = { ... }
fetch('/api/me', { method: 'POST', body: ... })
}
inputs = {}
render() {
return (<form onSubmit={this.onSubmit}> ... </form>)
}
}
Act 1 : Scene 3 : ■
inputs = {}
onSubmit = (ev) => {...
const data = {
username: this.inputs.username.value,
}
}
<form onSubmit={this.onSubmit}>...
<input type="text" name="username" id="username"
ref={(el) => this.inputs.username = el} />
</form>
Act 1 : Scene 3 : ▸
UsernameE-mail
PasswordSubmit
Act 1 : Scene 4 : ■
export class ProfileForm4 extends Component {
onSubmit = (ev) => {...
const data = {}
new FormData(ev.target).forEach(
(value, name) => data[name] = value
)
...}
render() {
...
<input type="text" name="username" id="username" />
}
}
Act 1 : Scene 4 : ▸
UsernameE-mail
PasswordSubmit
Break : Controlled Input : ⚛
With controlled components react state is the “single source of truth”.
<input type="text" value="GreeceJS" />
GreeceJS
<input type="text" value={name} onChange={handleNameChange} />
Break : Controlled Input : ⚛
feature uncontrolled controlledon submit value retrieval ✅ ✅
validating on submit ✅ ✅instant �eld validation ❌ ✅
conditionally disabling inputs ❌ ✅enforcing input format ❌ ✅
dynamic inputs ❌ ✅
Act 1 : Scene 5 : ■
export class ProfileForm5 extends Component {
state = {}
onUsernameChange = (ev) => {
const value = ev.target.value
this.setState(prevState =>
({ ...prevState, username: value })) // async
}
onEmailChange = (ev) => { ... }
onPasswordChange = (ev) => { ... }
onSubmit = (ev) => {
const data = this.state
post('/api/me', data)
}
render() { ... }
}
Act 1 : Scene 5 : ■
render() {
const { username = '', email = '', password = '' } = this.state
// undefined values will mark inputs as uncontrolled
// -> unintented use -> maybe bugs -> react warns
return (
<form onSubmit={this.onSubmit}>
<div>
<label htmlFor="username">Username</label>
<input type="text" name="username" id="username"
value={username} onChange={this.onUsernameChange} />
</div>
...
}
Act 1 : Scene 5 : ▸
UsernameE-mail
PasswordSubmit
Act 1 : Scene 6 : ■
export class ProfileForm6 extends Component {
state = { username: '', email: '', password: '' }
onSubmit = (ev) => {...}
onChange = (ev) => {
const { name, value } = ev.target
this.setState(prevState => ({ ...prevState, [name]: value }))
}
render() {
const { username, email, password } = this.state
...
<input type="text" name="username" id="username"
value={username} onChange={this.onChange} />
}
}
Act 1 : Scene 6 : ▸
UsernameE-mail
PasswordSubmit
ACT 2set the stage
Break : Container Components : ⚛
You’ll �nd your components much easier to reuse and reason about if you dividethem into two categories.
I call them Container and Presentational components- Dan Abramov
Break : Container Components : ⚛
Concerned with how things work
Provide the data and behavior
Call �ux actions and provide callbacks
Often stateful, as they tend to serve as data sources
Usually generated using higher order components
Act 2 : Scene 1 : ■
export const ProfileContainer = Child => {
class Container extends React.Component {
state = { values: {}, errors: {} };
onSubmit = ev => post('/api/me', this.state.values)
onChange = (values, errors) => {
this.setState(prevState => ({ ...prevState, values, errors }));
};
render() {
const { children, ...childProps } = this.props;
const { values, errors } = this.state;
const { onChange, onSubmit } = this;
return (
<Child {...childProps}
form={{values, errors, onChange, onSubmit}}>
{children}</Child>
)}}
return Container;
}}
Act 2 : Scene 1 : ■
class _ProfileForm7 extends Component {
onChange = (ev) => {
const { values, onChange } = this.props.form
const { name, value } = ev.target
const newValues = { ...values, [name]: value }
onChange(newValues)
}
render() {
const { values, onSubmit } = this.props.form
return <form onSubmit={onSubmit}>...
<input type="text" name="username" id="username"
value={values.username || ''} onChange={this.onChange} />
}}
const ProfileForm7 = ProfileContainer(_ProfileForm7)
export default ProfileForm7
Act 2 : Scene 1 : ▸
UsernameE-mail
PasswordSubmit
Act 2 : Scene 2 : ■
<form onSubmit={onSubmit}>
<div>
<label htmlFor="username">Username</label>
<input type="text" name="username" id="username"
value={values.username || ''} onChange={this.onChange} />
</div>
<div>
<label htmlFor="email">E-mail</label>
<input type="text" name="email" id="email"
value={values.email || ''} onChange={this.onChange} />
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" name="password" id="password"
value={values.password || ''} onChange={this.onChange} />
</div>
<input type="submit" />
</form>
Act 2 : Scene 2 : ■
class Fields extends Component {
...
}
Act 2 : Scene 2 : ■
class _ProfileForm8 extends Component {
schema = [{...}, {...}, {...}]
render() {
const { values, onChange, onSubmit } = this.props.form
return (
<form onSubmit={onSubmit}>
<Fields schema={this.schema}
values={values} onChange={onChange} />
<input type="submit" />
</form>
)
}
}
const ProfileForm8 = ProfileContainer(_ProfileForm8)
export { ProfileForm8 }
Act 2 : Scene 2 : ■
class Fields extends Component {
onChangeHandler = ev => {
const { values, onChange } = this.props;
const { name, value } = ev.target;
const newValues = {...values, [name]: (value || null) };
onChange(newValues);
};
render() {
const { schema, values } = this.props;
return (<div>...</div>);
}
}
Act 2 : Scene 2 : ■
schema = [{
path: "username",
label: "Username",
renderer: (path, value, label, onChange) => {
return (
<div>
<label htmlFor="username">Username</label>
<input type="text" name="username" id="username"
value={value} onChange={onChange} />
</div>
)
}
}]
Act 2 : Scene 2 : ■
class Fields extends Component {
onChangeHandler = ...
render() {
const { schema, values } = this.props;
return (<div>
{schema.map(field =>
field.renderer(
field.path,
values[field.path] || '',
field.label,
this.onChangeHandler)
)}
</div>);
}}
Act 2 : Scene 2 : ■
class _ProfileForm8 extends Component {
schema = [{...}, {...}, {...}]
render() {
const { values, onChange, onSubmit } = this.props.form
return (
<form onSubmit={onSubmit}>
<Fields schema={this.schema}
values={values} onChange={onChange} />
<input type="submit" />
</form>
)
}
}
const ProfileForm8 = ProfileContainer(_ProfileForm8)
export { ProfileForm8 }
Act 2 : Scene 2 : ▸
UsernameE-mail
PasswordSubmit
Act 2 : Scene 3 : ■
schema = [{
path: "username",
label: "Username",
renderer: (path, value, label, onChange) => {
return (
<div>
<label htmlFor="username">Username</label>
<input type="text" name="username" id="username"
value={value} onChange={onChange} />
</div>
)
}
}]
Act 2 : Scene 3 : ■
const textRenderer = (path, value, label, onChange) => (
<div>
<label htmlFor={path}>{label}</label>
<input type="text" name={path} id={path}
value={value} onChange={onChange} />
</div>
)
schema = [
{ path: "username", label: "Username", renderer: textRenderer }
]
Act 2 : Scene 3 : ■
const textRenderer = (type = 'text') =>
(path, value, label, onChange) => (
<div>
<label htmlFor={path}>{label}</label>
<input type={type} name={path} id={path}
value={value} onChange={onChange} />
</div>
)
const passwordRenderer = () => textRenderer('password')
schema = [
{path: "username", label: "Username", renderer: textRenderer()},
{path: "email", label: "E-mail", renderer: textRenderer()},
{path: "password", label: "Password", renderer: passwordRenderer()}
]
Act 2 : Scene 3 : ■
class _ProfileForm9 extends Component {
schema = [
{ path: "username", label: ..., renderer: textRenderer() },
{ path: "email", label: ..., renderer: textRenderer() },
{ path: "password", label: ..., renderer: passwordRenderer() }
]
render() {
const { values, onChange, onSubmit } = this.props.form
return (
<form onSubmit={onSubmit}>
<Fields schema={this.schema}
values={values} onChange={onChange} />
<input type="submit" />
</form>
)
}
}
const ProfileForm9 = ProfileContainer(_ProfileForm9)
export { ProfileForm9 }
Act 2 : Scene 3 : ▸
UsernameE-mail
PasswordSubmit
Act 2 : Scene 4 : ■
class _ProfileForm10 extends Component {
schema = (values) => {
const _schema = [
{ path: "username", label: ..., renderer: textRenderer() },
{ path: "email", label: ..., renderer: textRenderer() }
]
if (values.email) _schema.push(
{ path: "password", label: ..., renderer: passwordRenderer() }
)
return _schema;
}
render() {
const { values, onChange, onSubmit } = this.props.form
return ( ... <Fields schema={this.schema(values)} ... )
}
}
Act 2 : Scene 4 : ▸
UsernameE-mail
Submit
ACT 3the perfect crime!
yeah, well! Not yet in the public but...yarn add @logicea/react-forms
Act 3 : Scene 1 : ■
export const ProfileContainer = Child => {
class Container extends React.Component {
state = { values: {}, errors: {} };
onSubmit = ev => post('/api/me', this.state.values)
onChange = (values, errors) => {
this.setState(prevState => ({ ...prevState, values, errors }));
};
render() {
const { children, ...childProps } = this.props;
const { values, errors } = this.state;
const { onChange, onSubmit } = this;
return (
<Child {...childProps}
form={{values, errors, onChange, onSubmit}}>
{children}</Child>
)}}
return Container;
}}
Act 3 : Scene 1 : ■
onChange = ev => {
const { values, errors } = ev.detail
this.setState(prevState => ({ ...prevState, values, errors }));
};
Act 3 : Scene 1 : ■
import Fields from '@logicea/react-forms/Fields'
class _ProfileForm11 extends Component {
schema = (values) => {
const _schema = [
{ row: 1, path: "username", ... },
{ row: 2, path: "email", ... }
]
if(...) _schema.push({ row: 3, path: "password", ... }) })
}
render() { ... }
}
Act 3 : Scene 1 : ▸
UsernameE-mail
Submit
Act 3 : Scene 2 : ■
import Fields from '@logicea/react-forms/Fields'
import yup from 'yup'
class _ProfileForm12 extends Component {
schema = [
{ path: "username", validator: yup.string().required(), ... },
{ path: "email", validator: yup.string().email(), ... },
{ path: "password", validator: yup.string().min(4), ... },
]
render() {
const { values, errors, onChange, onSubmit } = this.props.form
return ... <Fields schema={this.schema(values)}
values={values} errors={errors} onChange={onChange} />
}
}
Act 3 : Scene 2 : ■
const textRenderer = (type = 'text') =>
(path, value, label, onChange, error) => (
<div>
<label>...</label>
<input ... />
{error && <div style={{ color: 'red' }}>{error}</div>}
</div>
)
const passwordRenderer = () => textRenderer('password')
Act 3 : Scene 2 : ▸
UsernameE-mail
PasswordSubmit
Act 3 : Scene 3 : ■
class _ProfileForm13 extends Component {
schema = (values) => {
const _schema = [
...
{ row: 4, path: "country", label: "Country",
dependands: ["city"], renderer: textRenderer() },
{ row: 5, path: "city", label: "City",
renderer: textRenderer() },
]
return _schema;
}
render() { ... }
}
Act 3 : Scene 3 : ▸
UsernameE-mail
PasswordCountry
CitySubmit
Act 3 : Scene 4 : ■
const COUNTRIES = [
'Greece',
'Burundi',
'Democratic Republic of Congo',
'Central African Republic'
]
const CITIES = {
'Greece': ['Athens', 'Thessaloniki'],
'Burundi': ['Bujumbura', 'Muyinga'],
'Democratic Republic of Congo': ['Kinshasa', 'Lubumbashi'],
'Central African Republic': ['Bangui', 'Bimbo']
}
Act 3 : Scene 4 : ■
const selectRenderer = (options = [])
=> (path, value, label, onChange, error) => (
<div>
<label htmlFor={path}>{label}</label>
<select name={path} id={path}
value={value} onChange={onChange}>
<option value=''></option>
{options.map(o => <option value={o} key={o}>{o}</option>)}
</select>
{error && <div style={{ color: 'red' }}>{error}</div>}
</div>
)
}
Act 3 : Scene 4 : ■
class _ProfileForm14 extends Component {
schema = (values, allCountries, allCities) => {
const cities = values.country ? allCities[values.country] : []
const _schema = [...
{ path: "country", renderer: selectRenderer(allCountries) ...
{ path: "city", renderer: selectRenderer(cities) ...
]
return _schema;
}
render() {
const schema = this.schema(values, COUNTRIES, CITIES)
...
}
}
Act 3 : Scene 4 : ▸
UsernameE-mail
PasswordCountry
CitySubmit
Act 3 : Scene 5 : ■
const getCountries = (props) => {
return new Promise(r => setTimeout(() => r(COUNTRIES), 200))
}
const getCities = (props) => {
const selectedCountry = props.form.values.country
const citiesResponse = CITIES[selectedCountry] || []
return new Promise(r => setTimeout(() => r(citiesResponse), 200))
}
Act 3 : Scene 5 : ■
class _ProfileForm15 extends Component {
schema = (values, countries, countryCities) => [...
{ path: "country", renderer: selectRenderer(countries) ...
{ path: "city", renderer: selectRenderer(countryCities) ...
]
render() {...
const { countries, cities } = this.props.lookups
const schema = this.schema(values, countries, cities)
}
}
const ProfileForm15 = compose(ProfileContainer(), Lookups({
initial: ['countries'],
resolver: (lookupField) => { ... },
rules: (oldProps, newProps) => { ... },
}))(_ProfileForm15)
export { ProfileForm15 }
Act 3 : Scene 5 : ■
const lookupsResolver = (lookupField) => {
switch (lookupField) { // eslint-disable-line default-case
case 'countries': return getCountries;
case 'cities': return getCities;
}
}
const lookupRules = (oldProps, newProps) => {
let lookups = []
const countryChanged =
oldProps.form.values.country !== newProps.form.values.country
if (countryChanged) lookups.push('cities')
return { lookups }
}
Act 3 : Scene 5 : ▸
UsernameE-mail
PasswordCountry
CitySubmit
Forms: other libs
complexfeature fullbloated
simplestate mgmtredux-free our inspiration
joi basednot mature
react-joi-forms
Performance
Performance - @logicea/react-forms
Performance - Formik
Performance - Formik vs Redux Form