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

Forms with React

Forms with React

Tutorial on how to handle forms with React, presented at React.Sofia

Radoslav Stankov

March 07, 2017
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. Forms with React
    Radoslav Stankov 07/03/2017

    View full-size slide

  2. Radoslav Stankov
    @rstankov

    http://rstankov.com

    http://github.com/rstankov

    View full-size slide

  3. ! 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
    Libraries

    View full-size slide













  4. View full-size slide













  5. View full-size slide













  6. View full-size slide













  7. View full-size slide













  8. View full-size slide













  9. View full-size slide













  10. View full-size slide













  11. View full-size slide













  12. View full-size slide













  13. View full-size slide













  14. View full-size slide

  15. export default class SubmissionForm extends React.Component {
    render() {
    return (

    Speaker

    Name:



    Email:




    );
    }
    }

    View full-size slide

  16. export default class SubmissionForm extends React.Component {
    render() {
    return (

    Speaker

    Name:



    Email:




    );
    }
    handleSubmit = (e) => {
    e.preventDefault();
    const allValues = Array.from(e.target.elements).reduce((data, input) => {
    console.log(input);
    data[input.name] = input.value;
    return data;
    }, {});
    remoteCall({
    speakerName: allValues.speakerName,
    speakerEmail: allValues.speakerEmail,
    });
    };
    }

    View full-size slide

  17. export default class SubmissionForm extends React.Component {
    render() {
    return (

    Speaker

    Name:
    this.speakerName = ref} type="text" id="speakerName" name="speakerName" defaultV


    Email:
    this.speakerEmail = ref} type="email" id="speakerEmail" name="speakerEmail" defa



    );
    }
    handleSubmit = (e) => {
    e.preventDefault();
    remoteCall({
    speakerName: this.speakerName.value,
    speakerEmail: this.speakerEmail.value,
    });
    };
    }

    View full-size slide

  18. export default class SubmissionForm extends React.Component {
    render() {
    return (

    Speaker

    Name:
    this.speakerName = ref} type="text" id="speakerName" name="speakerName" defaultV


    Email:
    this.speakerEmail = ref} type="email" id="speakerEmail" name="speakerEmail" defa



    );
    }
    handleSubmit = (e) => {
    e.preventDefault();
    remoteCall({
    speakerName: this.speakerName.value,
    speakerEmail: this.speakerEmail.value,
    });
    };
    }

    View full-size slide

  19. export default class SubmissionForm extends React.Component {
    render() {
    return (

    Speaker

    Name:
    this.speakerName = ref} type="text" id="speakerName" name="speakerName" defaultV


    Email:
    this.speakerEmail = ref} type="email" id="speakerEmail" name="speakerEmail" defa



    );
    }
    handleSubmit = (e) => {
    e.preventDefault();
    remoteCall({
    speakerName: this.speakerName.value,
    speakerEmail: this.speakerEmail.value,
    });
    };
    }
    &

    View full-size slide

  20. Uncontrolled Components
    • fast
    • simple
    • integration with external libraries
    • no much control
    • gets messy quite easy

    View full-size slide

  21. Controlled Components
    • you have more control
    • easier to reason about
    • needed for complex form interactions
    • recommend by react documentation

    View full-size slide

  22. class ExampleForm extends React.Component {
    state = { value: '' }
    handleChange = (event) => {
    this.setState({value: event.target.value});
    }
    handleSubmit = (event) => {
    e.preventDefault();
    remoteCall(this.target.value)/
    };
    render() {
    return (

    value={this.state.value}
    onChange={this.handleChange} />


    );
    }
    }

    View full-size slide

  23. class ExampleForm extends React.Component {
    state = { value: '' }
    handleChange = (event) => {
    this.setState({value: event.target.value});
    }
    handleSubmit = (event) => {
    e.preventDefault();
    remoteCall(this.target.value)/
    };
    render() {
    return (

    value={this.state.value}
    onChange={this.handleChange} />


    );
    }
    }
    Input change

    View full-size slide

  24. class ExampleForm extends React.Component {
    state = { value: '' }
    handleChange = (event) => {
    this.setState({value: event.target.value});
    }
    handleSubmit = (event) => {
    e.preventDefault();
    remoteCall(this.target.value)/
    };
    render() {
    return (

    value={this.state.value}
    onChange={this.handleChange} />


    );
    }
    }
    Input change

    handleChange
    setState

    View full-size slide

  25. class ExampleForm extends React.Component {
    state = { value: '' }
    handleChange = (event) => {
    this.setState({value: event.target.value});
    }
    handleSubmit = (event) => {
    e.preventDefault();
    remoteCall(this.target.value)/
    };
    render() {
    return (

    value={this.state.value}
    onChange={this.handleChange} />


    );
    }
    }
    Input change

    handleChange
    setState

    render
    New value

    View full-size slide













  26. View full-size slide

  27. export default class SubmissionForm extends React.Component {
    state = {
    fields: {
    speakerName: '',
    speakerEmail: '',
    },
    };
    render() {
    return (

    Speaker

    Name:
    Email:


    );
    }
    handleChange = (e) => {
    const name = e.target.name;
    const value = e.target.value;
    this.setState({
    fields: {

    ...this.state.fields,
    [name]: value
    }
    });
    };
    handleSubmit = (e) => {
    e.preventDefault();
    remoteCall(this.state.fields);
    };
    }

    View full-size slide

  28. export default class SubmissionForm extends React.Component {
    state = {
    fields: {
    speakerName: '',
    speakerEmail: '',
    },
    isSumitting: false,
    };
    render() {
    return (

    Speaker

    Name:
    Email:


    );
    }
    handleChange = (e) => { /* … */ };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) { return; }
    this.setState({ isSubmitting: true });
    await remoteCall(this.state.fields);
    };
    }

    View full-size slide

  29. export default class SubmissionForm extends React.Component {
    state = {
    fields: {
    speakerName: '',
    speakerEmail: '',
    },
    isSumitting: false,
    };
    render() {
    return (

    Speaker

    Name:
    Email:


    );
    }
    handleChange = (e) => { /* … */ };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) { return; }
    this.setState({ isSubmitting: true });
    await remoteCall(this.state.fields);
    };
    }

    View full-size slide


















  30. View full-size slide


















  31. View full-size slide


















  32. View full-size slide


















  33. View full-size slide


















  34. View full-size slide


















  35. View full-size slide


















  36. View full-size slide

  37. export default class SubmissionForm extends React.Component {
    state = {
    fields: {
    speakerName: '',
    speakerEmail: '',
    talkTitle: '',
    },
    isSumitting: false,
    };
    render() {
    return (

    Speaker

    Name:
    Email:
    Title:



    );
    }
    handleChange = (e) => { /* … */ };
    handleSubmit = (e) => { /* … */ };
    }

    View full-size slide

  38. export default class SubmissionForm extends React.Component {
    state = {
    fields: {
    speakerName: '',
    speakerEmail: '',
    talkTitle: '',
    },
    isSumitting: false,
    };
    render() {
    return (

    Speaker

    Name:
    Email:
    Title:



    );
    }
    handleChange = (e) => { /* … */ };
    handleSubmit = (e) => { /* … */ };
    }

    View full-size slide

  39. export default class SubmissionForm extends React.Component {
    state = {
    fields: {
    speakerName: '',
    speakerEmail: '',
    talkTitle: '',
    },
    isSumitting: false,
    };
    render() {
    return (

    Speaker

    Name:
    Email:
    Title:



    );
    }
    handleChange = (e) => { /* … */ };
    handleSubmit = (e) => { /* … */ };
    }

    View full-size slide

  40. let Field = ({ name, type, label, ...props }) => {
    return (

    {label}:


    );
    };

    View full-size slide

  41. export default class SubmissionForm extends React.Component {
    state = { /* … */ };
    render() {
    return (

    Speaker
    Talk


    );
    }
    handleChange = (e) => { /* … */ };
    handleSubmit = (e) => { /* … */ };
    }

    View full-size slide

  42. export default class SubmissionForm extends React.Component {
    state = { /* … */ };
    render() {
    return (

    Speaker
    Talk


    );
    }
    handleChange = (e) => { /* … */ };
    handleSubmit = (e) => { /* … */ };
    }

    View full-size slide









  43. View full-size slide










  44. View full-size slide










  45. View full-size slide










  46. '

    View full-size slide










  47. (

    View full-size slide

  48. let Field = ({ name, input, label, ...props }) => {
    if (input === 'textarea') {
    return (

    {label}:


    );
    }
    return (

    {label}:


    );
    };

    View full-size slide

  49. export default class SubmissionForm extends React.Component {
    state = {
    fields: {
    speakerName: '',
    speakerEmail: '',
    talkTitle: '',
    talkDescription: '',
    },
    isSumitting: false,
    };
    render() {
    return (

    Speaker
    Talk


    );
    }
    handleChange = (e) => { /* … */ };
    handleSubmit = (e) => { /* … */ };
    }

    View full-size slide










  50. View full-size slide











  51. View full-size slide











  52. View full-size slide

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

    View full-size slide

  54. name="talkLength"
    label="Length"
    value={this.state.fields.talkLength}
    onChange={this.handleChange}
    input="select"
    options={LENGTH_OPTIONS} />

    View full-size slide

  55. let Select = ({ options, ...props }) => (

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

    );

    View full-size slide

  56. let Field = ({ name, input, label, ...props }) => {
    if (input === 'textarea') {
    return (

    {label}:


    );
    }
    if (input === 'select') {
    return (

    {label}:


    );
    }
    return (

    {label}:


    );
    };

    View full-size slide

  57. let Input = ({ ...props }) => (

    );

    View full-size slide

  58. const INPUTS = {
    'textarea': Textarea,
    'select': Select,
    'text': Input,
    };
    let Field = ({ name, input = 'text', label, ...props }) => {
    const Component = INPUTS[input] || Input;
    const inputProps = Component === Input ? { type: input, ...props } : props;
    return (

    {label}:


    );
    };

    View full-size slide

  59. const INPUTS = {
    'textarea': Textarea,
    'select': Select,
    'text': Input,
    };
    let Field = ({ name, input = 'text', label, ...props }) => {
    const Component = typeof input === 'function' ? input : INPUTS[input] || Input;
    const inputProps = Component === Input ? { type: input, ...props } : props;
    return (

    {label}:


    );
    };

    View full-size slide

  60. const INPUTS = {
    'textarea': Textarea,
    'select': Select,
    'text': Input,
    };
    let Field = ({ name, input = 'text', label, ...props }) => {
    const Component = typeof input === 'function' ? input : INPUTS[input] || Input;
    const inputProps = Component === Input ? { type: input, ...props } : props;
    return (

    {label}:


    );
    };

    View full-size slide











  61. View full-size slide












  62. View full-size slide












  63. View full-size slide

  64. let RadioGroup = ({ value: fValue, options, name, id, ...props }) => (

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



    {label || value}


    ))}

    );

    View full-size slide

  65. const INPUTS = {
    'textarea': Textarea,
    'select': Select,
    'text': Input,
    'radioGroup': RadioGroup,
    };

    View full-size slide


  66. Component

    View full-size slide


  67. Component
    Child Component

    View full-size slide


  68. Component
    Child Component
    Child Child Component

    View full-size slide


  69. Component
    Child Component
    Child Child Component

    Child Child … Child Component

    View full-size slide


  70. Component
    Child Component
    Child Child Component

    Child Child … Child Component
    Context

    View full-size slide


  71. Component
    Child Component
    Child Child Component

    Child Child … Child Component
    Context
    Context

    View full-size slide

  72. export default class SubmissionForm extends React.Component {
    state = {
    fields: {
    speakerName: '',
    speakerEmail: '',
    talkTitle: '',
    talkDescription: '',
    talkLength: '15',
    },
    isSumitting: false,
    };
    static childContextTypes = {
    formState: React.PropTypes.object.isRequired,
    formHandleChange: React.PropTypes.func.isRequired,
    };
    getChildContext() {
    return {
    formState: this.state,
    formHandleChange: this.handleChange,
    };
    }
    render() { /* … */ };
    handleChange = (e) => { /* … */ };
    handleSubmit = (e) => { /* … */ };
    }

    View full-size slide

  73. let Field = ({ name, input, label, ...props }, { formState: { fields }, formHandleChange }) => {
    const value = fields[name];
    const Component = typeof input === 'function' ? input : INPUTS[input] || Input;
    const inputProps = Component === Input ? { type: input, ...props } : props;
    return (

    {label}:


    );
    };
    Field.contextTypes = {
    formState: React.PropTypes.object.isRequired,
    formHandleChange: React.PropTypes.func.isRequired,
    };

    View full-size slide

  74. let Field = ({ name, input, label, ...props }, { formState: { fields }, formHandleChange }) => {
    const value = fields[name];
    const Component = typeof input === 'function' ? input : INPUTS[input] || Input;
    const inputProps = Component === Input ? { type: input, ...props } : props;
    return (

    {label}:


    );
    };
    Field.contextTypes = {
    formState: React.PropTypes.object.isRequired,
    formHandleChange: React.PropTypes.func.isRequired,
    };

    View full-size slide

  75. let SubmitButton = ({ children }, { formState: { isSubmitting }}) => (

    );
    SubmitButton.contextTypes = {
    formState: React.PropTypes.object.isRequired,
    };

    View full-size slide


  76. Speaker


    Talk




    Submit

    View full-size slide

  77. export default class SubmissionForm extends React.Component {
    state = {
    fields: {
    speakerName: '',
    speakerEmail: '',
    talkTitle: '',
    talkDescription: '',
    talkLength: '15',
    },
    isSumitting: false,
    };
    static childContextTypes = {
    formState: React.PropTypes.object.isRequired,
    formHandleChange: React.PropTypes.func.isRequired,
    };
    getChildContext() {
    return {
    formState: this.state,
    formHandleChange: this.handleChange,
    };
    }
    render() {
    return (

    Speaker


    Talk




    Submit

    View full-size slide


  78. Speaker


    Talk




    Submit

    );
    }
    handleChange = (e) => {
    const name = e.target.name;
    const value = e.target.value;
    const fields = {
    ...this.state.fields,
    [name]: value
    };
    this.setState({ fields });
    };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) {
    return;
    }
    this.setState({ isSubmitting: true });
    await remoteCall(this.state.fields);
    };
    }

    View full-size slide

  79. export default class SubmissionForm extends React.Component {
    state = {
    fields: {
    speakerName: '',
    speakerEmail: '',
    talkTitle: '',
    talkDescription: '',
    talkLength: '15',
    },
    isSumitting: false,
    };
    static childContextTypes = {
    formState: React.PropTypes.object.isRequired,
    formHandleChange: React.PropTypes.func.isRequired,
    };
    getChildContext() {
    return {
    formState: this.state,
    formHandleChange: this.handleChange,
    };
    }
    render() {
    return (

    Speaker


    Talk




    Submit

    );
    }
    handleChange = (e) => {
    const name = e.target.name;
    const value = e.target.value;
    const fields = {
    ...this.state.fields,
    [name]: value
    };
    this.setState({ fields });
    };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) {
    return;
    }
    this.setState({ isSubmitting: true });
    await remoteCall(this.state.fields);
    };
    }

    View full-size slide

  80. export default class SubmissionForm extends React.Component {
    state = {
    fields: {
    speakerName: '',
    speakerEmail: '',
    talkTitle: '',
    talkDescription: '',
    talkLength: '15',
    },
    isSumitting: false,
    };
    static childContextTypes = {
    formState: React.PropTypes.object.isRequired,
    formHandleChange: React.PropTypes.func.isRequired,
    };
    getChildContext() {
    return {
    formState: this.state,
    formHandleChange: this.handleChange,
    };
    }
    render() {
    return (

    Speaker


    Talk




    Submit

    );
    }
    handleChange = (e) => {
    const name = e.target.name;
    const value = e.target.value;
    const fields = {
    ...this.state.fields,
    [name]: value
    };
    this.setState({ fields });
    };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) {
    return;
    }
    this.setState({ isSubmitting: true });
    await remoteCall(this.state.fields);
    };
    }

    View full-size slide

  81. class Form extends React.Component {
    state = {
    fields: this.props.fields,
    isSumitting: false,
    };
    static childContextTypes = {
    formState: React.PropTypes.object.isRequired,
    formHandleChange: React.PropTypes.func.isRequired,
    };
    getChildContext() {
    return {
    formState: this.state,
    formHandleChange: this.handleChange,
    };
    }
    render() {
    return (

    {this.props.children}

    );
    }
    handleInputChange = (e) => {
    const name = e.target.name;
    const value = e.target.value;
    const fields = {
    ...this.state.fields,
    [name]: value
    };
    this.setState({ fields });
    };

    View full-size slide

  82. return {
    formState: this.state,
    formHandleChange: this.handleChange,
    };
    }
    render() {
    return (

    {this.props.children}

    );
    }
    handleInputChange = (e) => {
    const name = e.target.name;
    const value = e.target.value;
    const fields = {
    ...this.state.fields,
    [name]: value
    };
    this.setState({ fields });
    };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) {
    return;
    }
    this.setState({ isSubmitting: true });
    await this.props.onSubmit(this.state.fields);
    };
    }

    View full-size slide

  83. const FIELDS = {
    speakerName: '',
    speakerEmail: '',
    talkTitle: '',
    talkDescription: '',
    talkLength: '15',
    };
    export default class SubmissionForm extends React.Component {
    render() {
    return (

    Speaker


    Talk




    Submit

    );
    }
    }

    View full-size slide

  84. const FIELDS = {
    speakerName: '',
    speakerEmail: '',
    talkTitle: '',
    talkDescription: '',
    talkLength: '15',
    };
    let SubmissionForm = () => (

    Speaker


    Talk




    Submit

    );

    View full-size slide

  85. Submit Server

    View full-size slide

  86. Submit Server
    Success

    View full-size slide

  87. Submit Server
    Success
    Errors

    View full-size slide

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

    View full-size slide

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

    errors: {
    field1: [ 'error1', 'error2
    field2: [ 'error1']

    }

    }

    View full-size slide

  90. { 

    errors: {
    field1: [ 'error1', 'error2' ],

    field2: [ 'error1']

    }

    }

    View full-size slide

  91. class Form extends React.Component {
    state = {
    fields: this.props.fields,
    isSumitting: false,
    };
    static childContextTypes = { /* … */ };
    getChildContext() = { /* … */ }
    render() = { /* … */ }
    handleInputChange = (e) => { /* … */ };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) {
    return;
    }
    await this.props.onSubmit(this.state.fields);
    };
    }

    View full-size slide

  92. class Form extends React.Component {
    state = {
    fields: this.props.fields,
    errors: {},
    isSumitting: false,
    };
    static childContextTypes = { /* … */ };
    getChildContext() = { /* … */ }
    render() = { /* … */ }
    handleInputChange = (e) => { /* … */ };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) {
    return;
    }
    this.setState({ isSubmitting: true, errors: {} });
    const { errors } = await this.props.onSubmit(this.state.fields);
    this.setState({ isSubmitting: false, errors: errors || {} });
    };
    }

    View full-size slide

  93. class Form extends React.Component {
    state = {
    fields: this.props.fields,
    errors: {},
    isSumitting: false,
    };
    static childContextTypes = { /* … */ };
    getChildContext() = { /* … */ }
    render() = { /* … */ }
    handleInputChange = (e) => { /* … */ };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) {
    return;
    }
    this.setState({ isSubmitting: true, errors: {} });
    const { errors } = await this.props.onSubmit(this.state.fields);
    this.setState({ isSubmitting: false, errors: errors || {} });
    };
    }

    View full-size slide

  94. let Field = ({ name, input, label, ...props }, { formState: { fields, errors }, formHandleInputChange })
    const value = fields[name];
    const error = errors[name] && errors[name][0];
    const Component = typeof input === 'function' ? input : INPUTS[input] || Input;
    const inputProps = Component === Input ? { type: input, ...props } : props;
    return (

    {label}:

    { error && {error}}

    );
    };

    View full-size slide

  95. let Field = ({ name, input, label, ...props }, { formState: { fields, errors }, formHandleInputChange })
    const value = fields[name];
    const error = errors[name] && errors[name][0];
    const Component = typeof input === 'function' ? input : INPUTS[input] || Input;
    const inputProps = Component === Input ? { type: input, ...props } : props;
    return (

    {label}:

    { error && {error}}

    );
    };

    View full-size slide

  96. class Form extends React.Component {
    state = { /* … */ };
    static childContextTypes = { /* … */ };
    getChildContext() = { /* … */ }
    render() = { /* … */ }
    handleInputChange = (e) => { /* … */ };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) {
    return;
    }
    this.setState({ isSubmitting: true, errors: {} });
    const { errors } = await this.props.onSubmit(this.state.fields);
    this.setState({ isSubmitting: false, errors: errors || {} });
    };
    }

    View full-size slide

  97. class Form extends React.Component {
    state = { /* … */ };
    static childContextTypes = { /* … */ };
    getChildContext() = { /* … */ }
    isUnmounted: boolean = false;
    componentWillUnmount() {
    this.isUnmounted = true;
    }
    render() = { /* … */ }
    handleInputChange = (e) => { /* … */ };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) {
    return;
    }
    this.setState({ isSubmitting: true, errors: {} });
    const { errors } = await this.props.onSubmit(this.state.fields);
    if (!this.isUnmounted) {
    this.setState({ isSubmitting: false, errors: errors || {} });
    }
    };
    }

    View full-size slide

  98. class Form extends React.Component {
    state = { /* … */ };
    static childContextTypes = { /* … */ };
    getChildContext() { /* … */ }
    isUnmounted: boolean = false;
    componentWillUnmount() {
    this.isUnmounted = true;
    }
    render() { /* … */ }
    handleChange = (e) => { /* … */ };
    handleSubmit = async (e) => {
    e.preventDefault();
    if (this.state.isSubmitting) {
    return;
    }
    this.setState({ isSubmitting: true, errors: {} });
    const { errors } = await this.props.onSubmit(this.state.fields);
    if (!this.isUnmounted) {
    this.setState({ isSubmitting: false, errors: errors || {} });
    }
    };
    }

    View full-size slide


  99. • simple form interface
    • extensible fields
    • protection for double submit
    • standardized form layout
    • standardized server responses

    View full-size slide

  100. What is missing?
    • client side validations
    • theming
    • handling of deep nested forms
    • tests!
    • fun features ¯\_(ϑ)_/¯

    View full-size slide

  101. ! 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
    Libraries

    View full-size slide

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

    View full-size slide

  103. https://speakerdeck.com/rstankov/forms-with-react

    View full-size slide

  104. https://www.meetup.com/React-Sofia/

    View full-size slide