Boris K
Boris K

Reputation: 3580

React Redux-dynamically creating Redux Forms

I'm building a component which will allow users to invite their friends.

The spec for the component is that it will have several input forms for their friends' emails and companies, a button which will add more input forms, and a button which submits all the forms remotely. When forms are submitted, a spinner appears in each form until a response is received from the server, at which point, if the submit was successful, the form disappears, and if there was an error, the error is displayed.

I'm stuck on the following: in order to submit a form remotely with Redux Form, you need to pass its name to the submitting component. I want to create the forms programatically. The names will be auto-incremented integers, created by the form management component, and passed to the child forms as props. However, when I try to export the form, referencing the name as this.props.name, I get an error: 'Uncaught TypeError: Cannot read property 'props' of undefined'-"this" is undefined.

Questions:

  1. Is my approach to solving this problem valid, or should I do something differently on a basic level?
  2. How do I get around this? I assume it's a scoping error?

My components:

The management component (creates and deletes forms, submits them, etc.):

import React, { Component } from 'react';
import { connect } from 'react-redux'
import { submit } from 'redux-form'
import * as actions from '../../actions';
import InviteForm from './inviteForm';

class InvitationFormManager extends Component {

  const buildForms = (length) =>{
    for (let i = 0; i< length; i++)
        {
          this.setState({forms:[...this.state.forms, <InviteForm key={i} name={i}>]};
        }
  }

  const addForm = () =>{
    this.setState({forms:[...this.state.forms, <InviteForm key={(this.state.forms.length + 1)} name={(this.state.forms.length + 1)}>]});
  }

  const formSubmit = (form) =>
  {
    dispatch(submit(form.name))
    .then(this.setState({
      forms: this.state.forms.filter(f => f.name !== form.name)
    }))
  }
  const submitForms = (){
    for(let form of this.state.forms){formSubmit(form)}
  }

  constructor(props) {
      super(props);
      this.state = {forms:[]};
  }

  componentWillMount(){
    buildForms(3)
  }

  render() {
    return (<div>
              <h5 className="display-6 text-center">Invite your team</h5>
              {this.state.forms}
              <br />
              <button
                type="button"
                className="btn btn-primary"
                onClick={submitForms}
              >
                Invite
              </button>
              <button
                type="button"
                className="btn btn-primary"
                onClick={addForm}
              >
                +
              </button>
          </div>
      );
    }
}


export default connect(actions)(InvitationFormManager)

The form component:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import * as actions from '../../actions';
import { Link } from 'react-router';

const renderField = ({
    input,
    label,
    type,
    meta: { touched, error, warning }
}) => (
    <fieldset className="form-group">
        <label htmlFor={input.name}>{label}</label>
        <input className="form-control" {...input} type={type} />
        {touched && error && <span className="text-danger">{error}</span>}
    </fieldset>
);

class InviteForm extends Component {
    constructor(props) {
        super(props);
        this.name = this.name.bind(this);
    }

    handleFormSubmit(props) {
        this.props.sendInvitation(props);
    }

    render() {
        if (this.props.submitting) {
            return (
                <div className="dashboard loading">
                    <Spinner name="chasing-dots" />
                </div>
            );
        }
        const { formName, handleSubmit } = this.props;
        return (
            <div className="form-container text-center">
                <form
                    className="form-inline"
                    onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
                    <div className="form-group">
                        <Field
                            name="email"
                            component={renderField}
                            type="email"
                            label="Email"
                        />
                        <Field
                            name="company"
                            component={renderField}
                            type="text"
                            label="Company"
                        />
                    </div>
                </form>
                <div>
                    {this.props.errorMessage &&
                        this.props.errorMessage.invited && (
                            <div className="error-container">
                                Oops! {this.props.errorMessage.invited}
                            </div>
                        )}
                </div>
            </div>
        );
    }
}
function validate(values) {
    let errors = {};

    if (values.password !== values.password_confirmation) {
        errors.password = "Password and password confirmation don't match!";
    }

    return errors;
}
function mapStateToProps(state) {
    return {
        errorMessage: state.invite.error,
        submitting: state.invite.submitting
    };
}

InviteForm = reduxForm({
    form: this.props.name,
    validate
})(InviteForm);
export default connect(mapStateToProps, actions)(InviteForm);

Upvotes: 0

Views: 445

Answers (1)

Boris K
Boris K

Reputation: 3580

The answer is, RTFM. Redux Form has this functionality as FieldArrays.

The example for Redux Form 7.0.4 is here: https://redux-form.com/7.0.4/examples/fieldarrays/

In case they move it later, here it is:

FieldArraysForm.js

import React from 'react'
import { Field, FieldArray, reduxForm } from 'redux-form'
import validate from './validate'

const renderField = ({ input, label, type, meta: { touched, error } }) =>
  <div>
    <label>
      {label}
    </label>
    <div>
      <input {...input} type={type} placeholder={label} />
      {touched &&
        error &&
        <span>
          {error}
        </span>}
    </div>
  </div>

const renderHobbies = ({ fields, meta: { error } }) =>
  <ul>
    <li>
      <button type="button" onClick={() => fields.push()}>
        Add Hobby
      </button>
    </li>
    {fields.map((hobby, index) =>
      <li key={index}>
        <button
          type="button"
          title="Remove Hobby"
          onClick={() => fields.remove(index)}
        />
        <Field
          name={hobby}
          type="text"
          component={renderField}
          label={`Hobby #${index + 1}`}
        />
      </li>
    )}
    {error &&
      <li className="error">
        {error}
      </li>}
  </ul>

const renderMembers = ({ fields, meta: { error, submitFailed } }) =>
  <ul>
    <li>
      <button type="button" onClick={() => fields.push({})}>
        Add Member
      </button>
      {submitFailed &&
        error &&
        <span>
          {error}
        </span>}
    </li>
    {fields.map((member, index) =>
      <li key={index}>
        <button
          type="button"
          title="Remove Member"
          onClick={() => fields.remove(index)}
        />
        <h4>
          Member #{index + 1}
        </h4>
        <Field
          name={`${member}.firstName`}
          type="text"
          component={renderField}
          label="First Name"
        />
        <Field
          name={`${member}.lastName`}
          type="text"
          component={renderField}
          label="Last Name"
        />
        <FieldArray name={`${member}.hobbies`} component={renderHobbies} />
      </li>
    )}
  </ul>

const FieldArraysForm = props => {
  const { handleSubmit, pristine, reset, submitting } = props
  return (
    <form onSubmit={handleSubmit}>
      <Field
        name="clubName"
        type="text"
        component={renderField}
        label="Club Name"
      />
      <FieldArray name="members" component={renderMembers} />
      <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: 'fieldArrays', // a unique identifier for this form
  validate
})(FieldArraysForm)

validate.js

const validate = values => {
  const errors = {}
  if (!values.clubName) {
    errors.clubName = 'Required'
  }
  if (!values.members || !values.members.length) {
    errors.members = { _error: 'At least one member must be entered' }
  } else {
    const membersArrayErrors = []
    values.members.forEach((member, memberIndex) => {
      const memberErrors = {}
      if (!member || !member.firstName) {
        memberErrors.firstName = 'Required'
        membersArrayErrors[memberIndex] = memberErrors
      }
      if (!member || !member.lastName) {
        memberErrors.lastName = 'Required'
        membersArrayErrors[memberIndex] = memberErrors
      }
      if (member && member.hobbies && member.hobbies.length) {
        const hobbyArrayErrors = []
        member.hobbies.forEach((hobby, hobbyIndex) => {
          if (!hobby || !hobby.length) {
            hobbyArrayErrors[hobbyIndex] = 'Required'
          }
        })
        if (hobbyArrayErrors.length) {
          memberErrors.hobbies = hobbyArrayErrors
          membersArrayErrors[memberIndex] = memberErrors
        }
        if (member.hobbies.length > 5) {
          if (!memberErrors.hobbies) {
            memberErrors.hobbies = []
          }
          memberErrors.hobbies._error = 'No more than five hobbies allowed'
          membersArrayErrors[memberIndex] = memberErrors
        }
      }
    })
    if (membersArrayErrors.length) {
      errors.members = membersArrayErrors
    }
  }
  return errors
}

export default validate

Upvotes: 1

Related Questions