taming forms with react

Post on 22-Jan-2018

16 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

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

top related