Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How not to hate your life when dealing with forms

How not to hate your life when dealing with forms

Radoslav Stankov

May 11, 2019
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. How not to hate your life
    when dealing with forms
    Radoslav Stankov 11/05/2019

    View Slide

  2. Radoslav Stankov
    @rstankov

    http://rstankov.com

    http://github.com/rstankov

    View Slide

  3. View Slide

  4. View Slide


  5. Who is developer worst enemy? !

    View Slide

  6. " The designer

    Who is developer worst enemy? !

    View Slide

  7. " The designer
    # The QA

    Who is developer worst enemy? !

    View Slide

  8. " The designer
    # The QA
    $ The sys admin

    Who is developer worst enemy? !

    View Slide

  9. " The designer
    # The QA
    $ The sys admin
    % The project owner

    Who is developer worst enemy? !

    View Slide

  10. " The designer
    # The QA
    $ The sys admin
    % The project owner
    & Santa

    Who is developer worst enemy? !

    View Slide

  11. " The designer
    # The QA
    $ The sys admin
    % The project owner
    & Santa
    ' Forms

    Who is developer worst enemy? !

    View Slide

  12. " The designer
    # The QA
    $ The sys admin
    % The project owner
    & Santa
    ( Form

    Who is developer worst enemy? !

    View Slide

  13. View Slide

  14. View Slide

  15. Form Basics

    View Slide

  16. View Slide

  17. View Slide

  18. View Slide








  19. View Slide

  20. function SubmitTalkForm() {
    const [state, setState] = useState({});
    const onSubmit = event => {
    event.preventDefault();
    console.log(state);
    };
    return (


    Title:
    id="title"
    type="text"
    name="title"
    value={state.title || ''}
    onChange={({ target }) =>
    setState({ ...state, title: target.value })
    }
    />



    );
    }

    View Slide

  21. function SubmitTalkForm() {
    const [state, setState] = useState({});
    const onSubmit = event => {
    event.preventDefault();
    console.log(state);
    };
    return (


    Title:
    id="title"
    type="text"
    name="title"
    value={state.title || ''}
    onChange={({ target }) =>
    setState({ ...state, title: target.value })
    }
    />



    );
    }

    View Slide

  22. Input change
    function SubmitTalkForm() {
    const [state, setState] = useState({});
    const onSubmit = event => {
    event.preventDefault();
    console.log(state);
    };
    return (


    Title:
    id="title"
    type="text"
    name="title"
    value={state.title || ''}
    onChange={({ target }) =>
    setState({ ...state, title: target.value })
    }
    />



    );
    }

    View Slide

  23. Input change

    onChange
    setState
    function SubmitTalkForm() {
    const [state, setState] = useState({});
    const onSubmit = event => {
    event.preventDefault();
    console.log(state);
    };
    return (


    Title:
    id="title"
    type="text"
    name="title"
    value={state.title || ''}
    onChange={({ target }) =>
    setState({ ...state, title: target.value })
    }
    />



    );
    }

    View Slide

  24. Input change

    onChange
    setState

    render
    New value
    function SubmitTalkForm() {
    const [state, setState] = useState({});
    const onSubmit = event => {
    event.preventDefault();
    console.log(state);
    };
    return (


    Title:
    id="title"
    type="text"
    name="title"
    value={state.title || ''}
    onChange={({ target }) =>
    setState({ ...state, title: target.value })
    }
    />



    );
    }

    View Slide

  25. View Slide

  26. Field
    Input Control
    Label

    View Slide

  27. Field
    Input Control
    Label ? Error
    Description

    View Slide








  28. View Slide






  29. View Slide

  30. function SubmitTalkForm() {
    const [state, setState] = useState({});
    const onSubmit = event => { /* */ };
    return (

    id="title"
    name="title"
    value={state.title || ''}
    onChange={({ target }) => setState({ ...state, title: target.value })}
    />


    );
    }
    function Field({ label, name, ...inputProps }) {
    return (

    {label || capitalize(name)}:


    );
    }

    View Slide

  31. function SubmitTalkForm() {
    const [state, setState] = useState({});
    const onSubmit = event => { /* */ };
    return (

    id="title"
    name="title"
    value={state.title || ''}
    onChange={({ target }) => setState({ ...state, title: target.value })}
    />


    );
    }
    function Field({ label, name, ...inputProps }) {
    return (

    {label || capitalize(name)}:


    );
    }

    View Slide


  32. Form

    View Slide


  33. Form
    Some Component

    View Slide


  34. Form
    Some Component
    Some Other Component

    View Slide


  35. Form
    Some Component
    Some Other Component

    Field

    View Slide


  36. Form
    Some Component
    Some Other Component

    Field
    Context

    View Slide


  37. Form
    Some Component
    Some Other Component

    Field
    Context
    Context

    View Slide

  38. const FormContext = React.createContext();
    function SubmitTalkForm() {
    const context = useState({});
    const onSubmit = event => { /* */ };
    return (






    );
    }
    function Field({ label, name, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    return (

    {label || capitalize(name)}:
    type="text"
    id={name}
    name={name}

    View Slide

  39. const FormContext = React.createContext();
    function SubmitTalkForm() {
    const context = useState({});
    const onSubmit = event => { /* */ };
    return (






    );
    }
    function Field({ label, name, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    return (

    {label || capitalize(name)}:
    type="text"
    id={name}
    name={name}

    View Slide

  40. const FormContext = React.createContext();
    function SubmitTalkForm() {
    const context = useState({});
    const onSubmit = event => { /* */ };
    return (






    );
    }
    function Field({ label, name, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    return (

    {label || capitalize(name)}:
    type="text"
    id={name}
    name={name}

    View Slide

  41. return (






    );
    }
    function Field({ label, name, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    return (

    {label || capitalize(name)}:
    type="text"
    id={name}
    name={name}
    value={state[name] || ''}
    onChange={({ target }) => setState({ ...state, [name]: target.value })}
    {...inputProps}
    />

    );
    }

    View Slide






  42. View Slide






  43. View Slide

  44. function SubmitTalkForm() {
    const onSubmit = values => {
    console.log(value);
    };
    return (




    );
    }

    View Slide

  45. const FormContext = React.createContext([{}, function() {}]);
    function Form({ onSubmit, initialValues, ...props }) {
    const context = useState(initialValues);
    function handleSubmit(e) {
    e.preventDefault();
    onSubmit(context[0]);
    }
    return (



    );
    }
    Form.Field = Field;
    Form.Submit = () => ;

    View Slide






  46. View Slide







  47. View Slide






  48. View Slide

  49. function Field({ label, name, control, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    return (

    {label || capitalize(name)}:
    type="text"
    id={name}
    name={name}
    value={state[name] || ''}
    onChange={({ target }) => setState({ ...state, [name]: target.value }) }
    {...inputProps}
    />

    );
    }

    View Slide

  50. function Field({ label, name, control, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    return (

    {label || capitalize(name)}:
    type={control || 'text'}
    id={name}
    name={name}
    value={state[name] || ''}
    onChange={({ target }) => setState({ ...state, [name]: target.value }) }
    {...inputProps}
    />

    );
    }

    View Slide







  51. View Slide








  52. View Slide







  53. View Slide

  54. function Field({ label, name, control, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    return (

    {label || capitalize(name)}:
    type={control || 'text'}
    id={name}
    name={name}
    value={state[name] || ''}
    onChange={({ target }) => setState({ ...state, [name]: target.value }) }
    {...inputProps}
    />

    );
    }

    View Slide

  55. function Field({ label, name, control, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    let Control = props => ;
    if (control === 'textarea') {
    Control = props => ;
    }
    return (

    {label || capitalize(name)}:
    id={name}
    name={name}
    value={state[name] || ''}
    onChange={({ target }) => setState({ ...state, [name]: target.value })}
    {...inputProps}
    />

    );
    }

    View Slide

  56. function Field({ label, name, control, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    let Control = props => ;
    if (control === 'textarea') {
    Control = props => ;
    }
    return (

    {label || capitalize(name)}:
    id={name}
    name={name}
    value={state[name] || ''}
    onChange={({ target }) => setState({ ...state, [name]: target.value })}
    {...inputProps}
    />

    );
    }

    View Slide








  57. View Slide









  58. View Slide

  59. const LENGTH_OPTIONS = [
    { value: 15, label: '15 minutes' },
    { value: 30, label: '30 minutes' },
    { value: 45, label: '45 minutes' },
    ];







    View Slide

  60. function Field({ label, name, control, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    let Control = props => ;
    if (control === 'textarea') {
    Control = props => ;
    }
    if (control === 'select') {
    Control = ({ options, ...props }) => (

    {options.map(({ label, value }, i) => (

    {label || value}

    ))}

    );
    }
    return (

    {label || capitalize(name)}:
    id={name}
    name={name}

    View Slide

  61. function Field({ label, name, control, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    let Control = props => ;
    if (control === 'textarea') {
    Control = props => ;
    }
    if (control === 'select') {
    Control = ({ options, ...props }) => (

    {options.map(({ label, value }, i) => (

    {label || value}

    ))}

    );
    }
    return (

    {label || capitalize(name)}:
    id={name}
    name={name}

    View Slide

  62. const CONTROLS = {
    undefined: props => ,
    text: props => ,
    email: props => ,
    textarea: props => ,
    select: ({ options, ...props }) => (

    {options.map(({ label, value }, i) => (

    {label || value}

    ))}

    ),
    };


    function Field({ label, name, control, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    const Control = CONTROLS[name];
    return (

    {label || capitalize(name)}:
    id={name}
    name={name}

    View Slide

  63. const CONTROLS = {
    undefined: props => ,
    text: props => ,
    email: props => ,
    textarea: props => ,
    select: ({ options, ...props }) => (

    {options.map(({ label, value }, i) => (

    {label || value}

    ))}

    ),
    };


    function Field({ label, name, control, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    const Control = CONTROLS[name];
    return (

    {label || capitalize(name)}:
    id={name}
    name={name}

    View Slide









  64. View Slide










  65. View Slide

  66. const CONTROLS = {
    /* */
    radioGroup: ({ value options, name, onChange, ...props }) => (

    {options.map((option, value }, i) => (


    type="radio"
    name={name}
    value={option.value}
    checked={option.value === value}
    onChange={onChange}
    {...props}
    />
    {option.label || option.value}


    ))}

    ),
    };

    View Slide










  67. View Slide











  68. View Slide










  69. View Slide

  70. function Field({ label, name, control, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    const Control = typeof control === 'function' ? control : CONTROLS[control];
    return (

    {label || capitalize(name)}:
    id={name}
    name={name}
    value={state[name] || ''}
    onChange={({ target }) => setState({ ...state, [name]: target.value })}
    {...inputProps}
    />

    );
    }

    View Slide

  71. function Field({ label, name, control, ...inputProps }) {
    const [state, setState] = useContext(FormContext);
    const Control = typeof control === 'function' ? control : CONTROLS[control];
    return (

    {label || capitalize(name)}:
    id={name}
    name={name}
    value={state[name] || ''}
    onChange={({ target }) => setState({ ...state, [name]: target.value })}
    {...inputProps}
    />

    );
    }

    View Slide

  72. View Slide

  73. View Slide

  74. View Slide

  75. Init

    View Slide

  76. Init Loading

    View Slide

  77. Init Loading
    Success

    View Slide

  78. Init Loading
    Success
    Errors

    View Slide

  79. Submit Server
    Success
    Errors

    View Slide

  80. remoteCall Server
    { result: 'ok' }
    { errors: {…} }

    View Slide

  81. remoteCall Server
    { result: 'ok' }
    { 

    errors: {
    field1: 'error1',

    field2: 'error2'

    }

    }

    View Slide

  82. { 

    errors: {
    field1: 'error1',

    field2: 'error2'

    }

    }

    View Slide

  83. { 

    errors: {
    field1: 'error1',

    field2: 'error2'

    }

    }

    View Slide

  84. View Slide

  85. • simple form interface
    • extensible fields
    • standardized form layout

    ) So far...

    View Slide

  86. • validations - client / server side
    • protection again double submit
    • nested fields
    • handle submit
    • ... more

    ! What is missing?

    View Slide

  87. Libraries

    View Slide

  88. * https://github.com/tannerlinsley/react-form
    + https://github.com/prometheusresearch/react-forms
    , https://github.com/codecks-io/react-reform
    - https://github.com/erikras/redux-form
    . https://github.com/Semantic-Org/Semantic-UI-React
    / https://github.com/jaredpalmer/formik
    0 https://github.com/final-form/final-form

    1 Libraries

    View Slide

  89. View Slide

  90. View Slide

  91. View Slide

  92. View Slide

  93. * https://github.com/tannerlinsley/react-form
    + https://github.com/prometheusresearch/react-forms
    , https://github.com/codecks-io/react-reform
    - https://github.com/erikras/redux-form
    . https://github.com/Semantic-Org/Semantic-UI-React
    / https://github.com/jaredpalmer/formik
    0 https://github.com/final-form/final-form
    Libraries

    View Slide

  94. Final Form Formik

    View Slide

  95. View Slide

  96. View Slide

  97. 2

    View Slide

  98. 2

    View Slide

  99. https://github.com/final-form/
    3

    View Slide

  100. ✅ Zero dependencies *
    ✅ Framework agnostic
    ✅ Opt-in subscriptions - only update on the state you need!
    ✅ 5 7.7k gzipped 5 - with React components

    View Slide

  101. import * as FinalForm from 'react-final-form';
    const SubmitTalkForm = () => (
    onSubmit={onSubmit}
    validate={validate}
    render={({ handleSubmit, pristine, invalid }) => (


    Title



    {({ input, meta }) => (

    Email

    {meta.touched && meta.error && {meta.error}}

    )}

    name="description"
    render={({ input, meta }) => (

    View Slide

  102. validate={validate}
    render={({ handleSubmit, pristine, invalid }) => (


    Title



    {({ input, meta }) => (

    Email

    {meta.touched && meta.error && {meta.error}}

    )}

    name="description"
    render={({ input, meta }) => (

    Description

    {meta.touched && meta.error && {meta.error}}

    )}
    />

    View Slide


  103. )}

    name="description"
    render={({ input, meta }) => (

    Description

    {meta.touched && meta.error && {meta.error}}

    )}
    />

    Length


    Level
    name=""
    component={CONTROLS.radioGroup}
    options={LEVEL_OPTIONS}
    />

    View Slide

  104. />

    Length


    Level
    name=""
    component={CONTROLS.radioGroup}
    options={LEVEL_OPTIONS}
    />


    Speakers



    Submit


    )}
    />
    );

    View Slide

  105. />

    Length


    Level
    name=""
    component={CONTROLS.radioGroup}
    options={LEVEL_OPTIONS}
    />


    Speakers



    Submit


    )}
    />
    );
    6

    View Slide










  106. View Slide

  107. View Slide










  108. import * as FinalForm from 'react-final-form';
    const SubmitTalkForm = () => (
    onSubmit={onSubmit}
    validate={validate}
    render={({ handleSubmit, pristine, invalid }) => (


    Title



    {({ input, meta }) => (

    Email

    {meta.touched && meta.error && {meta.error}}

    )}

    name="description"
    render={({ input, meta }) => (

    Description

    {meta.touched && meta.error && {meta.error}}

    )}
    />

    Length


    Level
    name=""
    component={CONTROLS.radioGroup}
    options={LEVEL_OPTIONS}
    />


    Speakers



    Submit


    )}
    />
    );

    View Slide










  109. View Slide

  110. function Form({ initialValues, onSubmit, children }) {
    return (

    {({ handleSubmit }) => {children}}

    );
    }

    View Slide










  111. View Slide

  112. export default function Field({ control, ...props }) {
    control = typeof control === 'function' ? control : CONTROLS[control];
    return ;
    }
    function FieldRow({
    control: Control,
    label,
    meta,
    input,
    fields,
    ...inputProps
    }) {
    const error = meta.error || meta.submitError;
    const name = input.name;
    return (


    {label || capitalize(name)}:
    {error && {error}}



    );
    }

    View Slide

  113. function FieldRow({
    control: Control,
    label,
    meta,
    input,
    fields,
    id,
    ...inputProps
    }) {
    const error = meta.error || meta.submitError;
    const name = input.name;
    id = React.useMemo(() => id || uniqueId(`form-${name}-`), [id, name]);
    return (


    {label || capitalize(name)}:
    {error && {error}}



    );
    }

    View Slide

  114. function FieldRow({
    control: Control,
    label,
    meta,
    input,
    fields,
    id,
    ...inputProps
    }) {
    const error = meta.error || meta.submitError;
    const name = input.name;
    id = React.useMemo(() => id || uniqueId(`form-${name}-`), [id, name]);
    return (


    {label || capitalize(name)}:
    {error && {error}}



    );
    }

    View Slide










  115. View Slide

  116. Form.Submit = () => (

    {form => (

    type="submit"
    disabled={form.submitting}
    value={`Submit${form.submitting ? '...' : ''}`}
    />
    {form.submitSucceeded && '7 Submitted'}
    {form.submitting && '8 Saving…'}
    {form.submitFailed && '9 Oh-oh! There has been an error…'}

    )}

    );

    View Slide

  117. Form.Submit = () => (

    {form => (

    type="submit"
    disabled={form.submitting}
    value={`Submit${form.submitting ? '...' : ''}`}
    />
    {form.submitSucceeded && '7 Submitted'}
    {form.submitting && '8 Saving…'}
    {form.submitFailed && '9 Oh-oh! There has been an error…'}

    )}

    );

    View Slide

  118. Form.Submit = () => (

    {form => (

    type="submit"
    disabled={form.submitting}
    value={`Submit${form.submitting ? '...' : ''}`}
    />
    {form.submitSucceeded && '7 Submitted'}
    {form.submitting && '8 Saving…'}
    {form.submitFailed && '9 Oh-oh! There has been an error…'}

    )}

    );

    View Slide


  119. {form => console.log(form)}

    {
    "dirty": false,
    "dirtyFields": {},
    "dirtySinceLastSubmit": false,
    "errors": {},
    "hasSubmitErrors": false,
    "hasValidationErrors": false,
    "initialValues": {},
    "invalid": false,
    "modified": {
    "title": false,
    "email": false,
    "description": false,
    "length": false,
    "level": false,
    },
    "pristine": true,
    "submitting": false,
    "submitFailed": false,
    "submitSucceeded": false,
    "touched": {
    "title": false,
    "email": false,
    "description": false,
    "length": false,
    "level": false,
    },
    "valid": true,
    "validating": false,
    "values": {},
    "visited": {
    "title": false,
    "email": false,
    "description": false,
    "length": false,
    "level": false,
    },
    "form": {
    "mutators": {}
    },
    "mutators": {}
    }

    View Slide


  120. {form => console.log(form)}

    {
    "dirty": false,
    "dirtyFields": {},
    "dirtySinceLastSubmit": false,
    "errors": {},
    "hasSubmitErrors": false,
    "hasValidationErrors": false,
    "initialValues": {},
    "invalid": false,
    "modified": {
    "title": false,
    "email": false,
    "description": false,
    "length": false,
    "level": false,
    },
    "pristine": true,
    "submitting": false,
    "submitFailed": false,
    "submitSucceeded": false,
    "touched": {
    "title": false,
    "email": false,
    "description": false,
    "length": false,
    "level": false,
    },
    "valid": true,
    "validating": false,
    "values": {},
    "visited": {
    "title": false,
    "email": false,
    "description": false,
    "length": false,
    "level": false,
    },
    "form": {
    "mutators": {}
    },
    "mutators": {}
    }
    2

    View Slide

  121. View Slide

  122. View Slide

  123. https://github.com/final-form/final-form-arrays
    [3]

    View Slide

  124. export default function Field({ control, ...props }) {
    control = typeof control === 'function' ? control : CONTROLS[control];
    const Field = control.isArray ? FieldArray : FinalForm.Field;
    return ;
    }
    function FieldRow({ control: Control, label, meta, input, fields, id, ...props}) {
    const error = meta.error || meta.submitError;
    const name = input ? input.name : fields.name;
    id = React.useMemo(() => id || uniqueId(`form-${name}-`), [id, name]);
    return (


    {label || capitalize(name)}:
    {error && {error}}

    {fields ? (

    ) : (

    )}

    );
    }

    View Slide

  125. export default function Field({ control, ...props }) {
    control = typeof control === 'function' ? control : CONTROLS[control];
    const Field = control.isArray ? FieldArray : FinalForm.Field;
    return ;
    }
    function FieldRow({ control: Control, label, meta, input, fields, id, ...props}) {
    const error = meta.error || meta.submitError;
    const name = input ? input.name : fields.name;
    id = React.useMemo(() => id || uniqueId(`form-${name}-`), [id, name]);
    return (


    {label || capitalize(name)}:
    {error && {error}}

    {fields ? (

    ) : (

    )}

    );
    }

    View Slide

  126. function SpeakersInput({ fields }) {
    return (

    {fields.map((name, index) => (

    {index + 1}
    name={`${name}.firstName`}
    placeholder="First Name"
    />
    name={`${name}.lastName`}
    placeholder="Last Name"
    />
    { e.preventDefault();fields.remove(index); }}>
    remove


    ))}
    { e.preventDefault(); fields.push(undefined); }}>
    add speaker


    );
    }
    SpeakersInput.isArray = true;

    View Slide

  127. View Slide










  128. View Slide










  129. View Slide

  130. Form.Mutation = ({ children, mutation, input, update, initialValues, onSubmit }) => {
    return (

    {(mutate) => {
    const submit = async (values) => {
    const response = await mutate({
    variables: buildVariables(values, input);
    });
    const errors = extractErrors(response);
    if (errors) {
    return errors;
    }
    if (onSubmit) {
    onSubmit(extractObject(response));
    }
    };
    return (

    {children}

    );
    }}

    );
    };

    View Slide

  131. https://github.com/RStankov/talks-code

    View Slide

  132. View Slide

  133. View Slide

  134. ⚛ basic of form handling in React
    0 build an extensible Form/Form.Field interface
    3 integrated with good battle tested form library

    ; Recap

    View Slide

  135. ⚛ basic of form handling in React
    0 build an extensible Form/Form.Field interface
    3 integrated with good battle tested form library
    < ...had some fun =

    ; Recap

    View Slide

  136. View Slide

  137. View Slide

  138. View Slide

  139. Thanks >

    View Slide

  140. View Slide