React Redux-dynamically creating Redux Forms - javascript

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:
Is my approach to solving this problem valid, or should I do something differently on a basic level?
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);

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

Related

I'm using react-hook-form to validate my input but I can't get it to work after extracting the input to its own component

I have a Form.js component which returns a form element. This form element contains a FormGroup component which takes props such as inputType, inputName, inputPlaceholder and in return renders an input field with a label. I am using react-hook-form for the validation of the input but I can't get it to work after extracting the input to a separate component. In my original code, I had my errors appear when the validation failed, but after extracting the label and input into their own component, this stopped working.
My original working code looks like this:
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import FormGroup from './FormGroup';
const Form = () => {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = async (data) => {
console.log(data)
};
return (
<form className="form-container" onSubmit={handleSubmit(onSubmit)} autoComplete="off" noValidate>
<div className="form-group">
<label className="form-label" htmlFor="firstName">
<p>First Name</p>
<p className="input-error">{errors.firstName && errors.firstName.message}</p>
</label>
<input type="text" name="firstName" placeholder="First Name" {...register("firstName", { required: 'Required ' })} />
</div>
<div className="form-group">
<button type="submit">Submit</button>
</div>
</form>
);
}
export default Form
Then I changed it to:
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import FormGroup from './FormGroup';
const Form = () => {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = async (data) => {
console.log(data)
};
return (
<form className="form-container" onSubmit={handleSubmit(onSubmit)} autoComplete="off" noValidate>
<FormGroup
inputType="text"
inputName="firstName"
inputPlaceholder="First Name">
</FormGroup>
<div className="form-group">
<button type="submit">Submit</button>
</div>
</form>
);
}
export default Form
And I extracted the label and input into:
import { useForm } from 'react-hook-form';
const FormGroup = (props) => {
const { register, formState: { errors } } = useForm();
return (
<div className="form-group">
<label className="form-label" htmlFor={props.inputName}>
<p>{ props.inputPlaceholder }</p>
<p className="input-error">{errors.firstName && errors.firstName.message}</p>
</label>
<input type={props.inputType} name={props.inputName} placeholder={props.inputPlaceholder} {...register(props.inputName, { required: true })} />
</div>
);
}
export default FormGroup
The Problem
You have multiple instances of the useForm hook.
Solution
Use the useForm hook only on the Form component and pass the errors object and register method as props to FormGroup
Form Component
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import FormGroup from './FormGroup';
const Form = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = async (data) => {
console.log(data);
};
return (
<form
className="form-container"
onSubmit={handleSubmit(onSubmit)}
autoComplete="off"
noValidate
>
<FormGroup
inputType="text"
inputName="firstName"
inputPlaceholder="First Name"
register={register}
errors={errors}
></FormGroup>
<div className="form-group">
<button type="submit">Submit</button>
</div>
</form>
);
};
export default Form;
FormGroup Component
import { useForm } from 'react-hook-form';
const FormGroup = (props) => {
return (
<div className="form-group">
<label className="form-label" htmlFor={props.inputName}>
<p>{props.inputPlaceholder}</p>
<p className="input-error">
{props.errors.firstName && props.errors.firstName.message}
</p>
</label>
<input
type={props.inputType}
name={props.inputName}
placeholder={props.inputPlaceholder}
{...props.register(props.inputName, { required: true })}
/>
</div>
);
};
export default FormGroup;
The recommended approach from the Documentation is as follows:
useFormContext This custom hook allows you to access the form context. useFormContext is intended to be used in deeply nested structures, where it would become inconvenient to pass the context as a prop.
https://react-hook-form.com/api/useformcontext
const Form = () => {
const methods = useForm();
const {
register,
handleSubmit,
formState: { errors },
} = methods;
const onSubmit = async (data) => {
console.log(data);
};
return (
<FormProvider {...methods} >
<form
className="form-container"
onSubmit={handleSubmit(onSubmit)}
autoComplete="off"
noValidate
>
<FormGroup
inputType="text"
inputName="firstName"
inputPlaceholder="First Name">
</FormGroup>
<div className="form-group">
<button type="submit">Submit</button>
</div>
</form>
</FormProvider>
);
};
const FormGroup = (props) => {
const { register, errors } = useFormContext(); // retrieve all hook methods from parent
return (
<div className="form-group">
<label className="form-label" htmlFor={props.inputName}>
<p>{props.inputPlaceholder}</p>
<p className="input-error">
{errors.firstName && errors.firstName.message}
</p>
</label>
<input
type={props.inputType}
name={props.inputName}
placeholder={props.inputPlaceholder}
{register(props.inputName, { required: true })}
/>
</div>
);
};
export default FormGroup;

How I can add multiple same fields form in reactJS?

I want to add multiple persons dynamically in my form. Like I have Person 1 username and email then when I click Add Person it should make same fields for person 2 on the same page. When I click the Submit button it should give me the object of all persons.
App.js
import './App.css';
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class App extends Component {
state = {
fields:[]
};
addPerson() {
this.setState({fields:[...this.state.fields, ""]})
};
handleChange(e, index) {
this.state.fields[index] = e.target.value;
this.setState({fields: this.state.fields});
}
handleSubmit(e) {
console.log(this.state,"$$")
}
render() {
return (
<div className="App">
<header className="App-header">
<div>
<h1>The Form</h1>
{
this.state.fields.map((field, index) => {
return(
<div key={index}>
<input onChange={(e)=>this.handleChange(e, index)} value={field}/>
</div>
)
}
)
}
<button onClick={(e) => this.addPerson(e)}>Add Person</button>
<button onClick={(e) => this.handleSubmit(e)}>Submit</button>
</div>
</header>
</div>
)
}
}
I want my state would be like this...
state = {
fields:[
{
id: 1,
name: 'Max',
email: 'max.max#max.in'
}
]
};
Demo of my current page.
This is my solution codesandbox
You need to have two inputs, for email and name, and depending on which input is updated, update the value of person in array.
import React, { Component } from "react";
import "./styles.css";
export default class App extends Component {
state = {
fields: []
};
addPerson() {
const newPerson = {
id: Math.random(),
name: "",
email: ""
};
this.setState({ fields: [...this.state.fields, newPerson] });
}
handleChange(e, index) {
const fieldsCopy = [...this.state.fields];
fieldsCopy.forEach(item => {
if (item.id === index) {
item[e.target.name] = e.target.value;
}
});
this.setState({ fields: fieldsCopy }, () => console.log(this.state.fields));
}
handleSubmit(e) {
console.log(this.state, "$$");
}
render() {
return (
<div className="App">
<header className="App-header">
<div>
<h1>The Form</h1>
{this.state.fields.map(field => {
return (
<div key={field.id}>
<input
onChange={e => this.handleChange(e, field.id)}
name="name"
/>
<input
onChange={e => this.handleChange(e, field.id)}
name="email"
/>
</div>
);
})}
<button onClick={e => this.addPerson(e)}>Add Person</button>
<button onClick={e => this.handleSubmit(e)}>Submit</button>
</div>
</header>
</div>
);
}
}
Edited:
Here is my version of it:
import './App.css';
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class App extends Component {
index = 0;
state = {
fields: []
};
handleChange(e, idx) {
const { name, value } = e.target;
this.setState(state => {
return state.fields[idx][name] = value;
});
}
addPerson = () => {
const person = { id: this.index, name: '', email: '' };
this.index++;
this.setState({ fields: [ ...this.state.fields, person ] })
}
handleSubmit = () => {
console.log(this.state.fields);
}
render() {
const { fields } = this.state;
return (
<div className="App">
<header className="App-header">
<div>
<h1>The Form</h1>
{fields.length
? fields.map((field, idx) => (
<div key={idx}>
<label>Name:</label>
<input type="text" onChange={(e)=>this.handleChange(e, idx)} name="name" value={field.name}/>
<label>Email:</label>
<input type="email" onChange={(e)=>this.handleChange(e, idx)} name="email" value={field.email}/>
</div>
))
: null
}
<button onClick={this.handleSubmit}>Submit</button>
<button onClick={() => this.addPerson()}>Add Person</button>
</div>
</header>
</div>
)
}
}
If you are using the person id as unique identifier outside this component's state, I would suggest using some id generator library like uuid.
I hope this helps!

React.js Form - How To Include All Values On Final Page?

So I have built a Wizard Form using React-Final-Form that I am using in my sign-up page. I am trying to figure out how I can display all user inputs on the final page as a way for the user to double-check/verify their inputs before submitting. Any help would be greatly appreciated!
(P.S. - I tried researching this before posting, but all I was able to find was storing user inputs in Redux and accessing them from there, which I'd like to avoid, if at all possible.)
Here is an example link that shows what I want to do - Please feel free to fork and play around with it if you are trying to figure out a solution! https://codesandbox.io/s/0332k02x0v
Here is the code, shortened to include only the relevant bits:
My Wizard.js page:
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Form } from "react-final-form";
class Wizard extends Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired
};
static Page = ({ children }) => children;
constructor(props) {
super(props);
this.state = {
page: 0,
values: props.initialValues || {}
};
}
next = values =>
this.setState(state => ({
page: Math.min(state.page + 1, this.props.children.length - 1),
values
}));
previous = () =>
this.setState(state => ({
page: Math.max(state.page - 1, 0)
}));
validate = values => {
const activePage = React.Children.toArray(this.props.children)[
this.state.page
];
return activePage.props.validate ? activePage.props.validate(values) : {};
};
handleSubmit = values => {
const { children, onSubmit } = this.props;
const { page } = this.state;
const isLastPage = page === React.Children.count(children) - 1;
if (isLastPage) {
return onSubmit(values);
} else {
this.next(values);
}
};
render() {
const { children } = this.props;
const { page, values } = this.state;
const activePage = React.Children.toArray(children)[page];
const isLastPage = page === React.Children.count(children) - 1;
return (
<Form
initialValues={values}
validate={this.validate}
onSubmit={this.handleSubmit}
>
{({ handleSubmit, submitting, values }) => (
<form onSubmit={handleSubmit}>
{activePage}
<div className="buttons">
{page > 0 && (
<button type="button" onClick={this.previous}>
« Previous
</button>
)}
{!isLastPage && <button type="submit">Next »</button>}
{isLastPage && (
<button type="submit" disabled={submitting}>
Submit
</button>
)}
</div>
{/* <pre>{JSON.stringify(values, 0, 2)}</pre> */}
</form>
)}
</Form>
);
}
}
export default Wizard;
My index.js page:
import React, { Component } from "react";
import { Field } from "react-final-form";
import formatString from "format-string-by-pattern";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import Wizard from "./Wizard";
import Styles from "./Styles";
import { addUser } from "../../../actions/authActions";
class ReactFinalForm2 extends Component {
state = {};
render() {
const onSubmit = async values => {
this.props.addUser(values);
// API query here
};
const Error = ({ name }) => (
// Error handing here
);
return (
<Styles>
<div>
<Wizard initialValues={{}} onSubmit={onSubmit}>
<Wizard.Page
validate={values => {
// Page validation here
}}
>
// Page inputs here
</Wizard.Page>
<Wizard.Page
validate={values => {
// Page validation here
}}
>
// Page inputs here
</Wizard.Page>
<Wizard.Page
validate={values => {
// Page validation here
}}
>
// Page inputs here
</Wizard.Page>
<Wizard.Page>
{/* *** THIS IS WHERE I WOULD LIKE TO DISPLAY ALL PREVIOUS VALUES (SO THE USER CAN CONFIRM / DOUBLE-CHECK THEIR INPUTS BEFORE SUBMITTING) *** */}
</Wizard.Page>
</Wizard>
</div>
</Styles>
);
}
}
ReactFinalForm2.propTypes = {
addUser: PropTypes.func.isRequired
};
export default connect(
null,
{ addUser }
)(ReactFinalForm2);
I have added a state to the parent component. Changing this state on every submit from the child. I have JSON stringify the state in parent component. As you said no need to use redux, this is the workaround I came with. Still your code has a potential for improvements. Please check this working sandbox:
[ https://codesandbox.io/s/zrvloq4o6x ]
Wizard.js change
handleSubmit = values => {
const { children, onSubmit } = this.props;
const { page } = this.state;
const isLastPage = page === React.Children.count(children) - 1;
if (isLastPage) {
return onSubmit(values);
} else {
this.next(values);
}
// Change added
this.props.onStateChange(values);
};
Index.js
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Field } from "react-final-form";
import Wizard from "./Wizard";
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const onSubmit = async values => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const Error = ({ name }) => (
<Field
name={name}
subscribe={{ touched: true, error: true }}
render={({ meta: { touched, error } }) =>
touched && error ? <span>{error}</span> : null
}
/>
);
const required = value => (value ? undefined : "Required");
let data = {};
class App extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.onStateChange = this.onStateChange.bind(this);
}
onStateChange = values => {
this.setState({ data: values });
console.log(values);
};
render() {
return (
<Styles>
<h1>🏁 React Final Form Example</h1>
<h2>Wizard Form</h2>
<a href="https://github.com/erikras/react-final-form#-react-final-form">
Read Docs
</a>
<p>
Notice the mixture of field-level and record-level (or{" "}
<em>page-level</em> in this case) validation.
</p>
<Wizard
initialValues={{}}
onStateChange={this.onStateChange}
onSubmit={onSubmit}
>
<Wizard.Page>
<div>
<label>First Name</label>
<Field
name="firstName"
component="input"
type="text"
placeholder="First Name"
validate={required}
/>
<Error name="firstName" />
</div>
<div>
<label>Last Name</label>
<Field
name="lastName"
component="input"
type="text"
placeholder="Last Name"
validate={required}
/>
<Error name="lastName" />
</div>
</Wizard.Page>
<Wizard.Page
validate={values => {
const errors = {};
if (!values.notes) {
errors.notes = "Required";
}
return errors;
}}
>
<div>
<label>Best Stooge?</label>
<div>
<label>
<Field
name="stooge"
component="input"
type="radio"
value="larry"
/>{" "}
Larry
</label>
<label>
<Field
name="stooge"
component="input"
type="radio"
value="moe"
/>{" "}
Moe
</label>
<label>
<Field
name="stooge"
component="input"
type="radio"
value="curly"
/>{" "}
Curly
</label>
</div>
</div>
<div>
<label>Notes</label>
<Field name="notes" component="textarea" placeholder="Notes" />
<Error name="notes" />
</div>
</Wizard.Page>
<Wizard.Page>
<div>
<p>
<b>Display all previous values here for user verification </b>
<br />
<i>{JSON.stringify(this.state.data, 0, 2)}</i>
</p>
</div>
</Wizard.Page>
</Wizard>
</Styles>
);
}
}
render(<App />, document.getElementById("root"));

Redux-form validations not getting caught

im fairly new to redux.
What im trying to validate is, two key fields cannot hold the same value and all fields are required. For the required part, i am using Field-Level Validations and that seems to work fine. For deciding whether an element already exists, i am using Sync Validation.
When the sync validation works, i checked it with console log. It catching the error and adding it to the errors object. But my form is not showing that.. is it not binded? What am i missing here?
I have added 'onFocus' and 'onBlur' mouse events to the text-fields, to make then readonly onBlur. They seem to be working fine. But the moment i added that, my {touched && error && <span>{error}</span>} error stops getting displayed. What am i doing wrong here?
my form
const required = value => (value ? "" : "required")
class CreateObject extends React.Component {
enableTextField = (e) => {
document.getElementById(e.target.id).removeAttribute("readonly");
}
disableTextField = (e) => {
document.getElementById(e.target.id).setAttribute("readonly", true);
}
renderField = ({ input, label, type, id, meta: { touched, error } }) => (
<React.Fragment>
{touched && error && <span>{error}</span>}
<FormControl {...input} type={type} placeholder={label} id={id}
className={`align-inline object-field-length ${error ? 'error' : ''}`}
onFocus={this.enableTextField.bind(this)}
onBlur={this.disableTextField.bind(this)}
/>
</React.Fragment>
);
renderObjects = ({ fields, meta: { touched, error, submitFailed, errors } }) => {
return (
<ul>
<li>
<center>
<Button bsStyle="success" onClick={() => fields.push({})}>Add New Object</Button>
</center>
</li>
{fields.map((object, index) => (
<li key={index}>
<br />
<center>
<Field
name={`${object}.key`}
type='text'
component={this.renderField}
validate={required}
label="Key"
id={`${object}.key`}
/>
<div className="divider" />
<Field
name={`${object}.method`}
type='text'
component={this.renderField}
label="Method"
validate={required}
id={`${object}.key` + `${object}.method`}
/>
<div className="divider" />
<Field
name={`${object}.value`}
type='text'
component={this.renderField}
label="Value"
validate={required}
id={`${object}.key` + `${object}.value`}
/>
<div className="divider" />
<span
className="align-inline"
onClick={() => fields.remove(index)}
className="allIcons mdi mdi-delete-forever"
/>
</center>
</li>
)
)}
</ul>
);
}
submit() {
//this
}
render() {
const { handleSubmit, pristine, reset, submitting, invalid } = this.props;
console.log(this.props);
return (
<form onSubmit={handleSubmit(this.submit.bind(this))}>
<FieldArray name='objects' component={this.renderObjects} />
<center>
<Button className="align-inline" type="submit" disabled={pristine || submitting || invalid}>Submit</Button>
<div className="divider" />
<Button className="align-inline" disabled={pristine || submitting} onClick={reset}> Clear All Values </Button>
</center>
</form>
);
}
}
export default reduxForm({
form: 'ObjectRepo',
validate
})(CreateObject);
validate.js
const validate = values => {
const error = {}
if (!values.objects || !values.objects.length) {
error.objects = { _error: 'At least one object must be entered' }
} else {
const objectArrayErrors = []
values.objects.forEach((object, objectIndex) => {
const objectErrors = { _error: 'Object Key should be unique' }
if (values.objects.filter(item => item.key == object.key).length == 2) {
objectArrayErrors[objectIndex] = objectErrors
}
})
if (objectArrayErrors.length) {
error.objects = objectArrayErrors
}
}
console.log(error)
return error
}
export default validate
Thanks a lot in advance!
You might want to look at the code below which works for me. This is your container (or smart component if you wish)
export const validateProps = {
name: [required],
value: [required, intOrFloat, maxPercent],
someId: [required],
}
export const transformer = new TypesModel({
name: String,
value: Number,
someId: Number,
})
export default createFormContainer(
formName,
'your_form',
transformer,
validateProps,
mapStateToProps,
mapDispatchToProps,
null,
false,
null,
onSuccessSubmit)(YourFormContainer)
And the code for types model is
class TypesModel {
constructor(schema) {
this.schema = schema
}
transform(data) {
const keys = Object.keys(this.schema)
const result = {}
for (let index = 0, len = keys.length; index < len; index += 1) {
const keyName = keys[index]
try {
result[keyName] = this.schema[keyName](data[keyName])
} catch (e) {
throw new Error(`Type conversion for field "${keyName}" failed`)
}
}
return result
}
}
export default TypesModel
You want validators to look like:
export const required = value => isEmpty(value) &&
'Required field'
export const intOrFloat = value => (!isInt(`${value}`) && !isFloat(`${value}`)) &&
'Must be an integer of float'

Submitting a Redux Form

I've got a Redux form which I've attempted to break into several subcomponents as a wizard, like this: https://redux-form.com/7.0.4/examples/wizard/
However, I'm having trouble properly wiring the form into my actions in order to submit the form data. The example actually passes the onSubmit method into the form from the router, which I don't want to do; rather, I'd like to connect my form to my actions, and then pass the signUpUser method into the last of the three components making up the wizard. My current attempt to do so is throwing two errors:
Uncaught TypeError: handleSubmit is not a function
Warning: Failed prop type: The prop `onSubmit` is marked as required in `SignUp`, but its value is `undefined`.
My original form component worked fine, but the same logic does not work in the new component. I think it's a question of scoping, but not sure. I'm relatively new to React and Redux-Form, so am finding this hard to reason through. Ideas?
New (broken) component:
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import SignupFirstPage from './signupComponents/signupFirstPage';
import SignupSecondPage from './signupComponents/signupSecondPage';
import SignupThirdPage from './signupComponents/SignupThirdPage';
class SignUp extends Component {
constructor(props) {
super(props);
this.nextPage = this.nextPage.bind(this);
this.previousPage = this.previousPage.bind(this);
this.state = {
page: 1
};
this.handleFormSubmit = this.handleFormSubmit.bind(this);
}
nextPage() {
this.setState({ page: this.state.page + 1 });
}
previousPage() {
this.setState({ page: this.state.page - 1 });
}
handleFormSubmit(props) {
this.props.signUpUser(props);
}
render() {
const { handleSubmit } = this.props;
const { page } = this.state;
return (
<div>
{page === 1 && <SignupFirstPage onSubmit={this.nextPage} />}
{page === 2 && (
<SignupSecondPage
previousPage={this.previousPage}
onSubmit={this.nextPage}
/>
)}
{page === 3 && (
<SignupThirdPage
previousPage={this.previousPage}
onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}
/>
)}
<div>
{this.props.errorMessage &&
this.props.errorMessage.signup && (
<div className="error-container">
Oops! {this.props.errorMessage.signup}
</div>
)}
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
errorMessage: state.auth.error
};
}
SignUp.propTypes = {
onSubmit: PropTypes.func.isRequired
};
export default connect(mapStateToProps, actions)(SignUp);
New subcomponent (the final one):
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import validate from './validate';
import renderField from '../../renderField';
const SignupThirdPage = props => {
const { handleSubmit, pristine, previousPage, submitting } = props;
return (
<form onSubmit={handleSubmit}>
<Field
name="password"
type="password"
component={renderField}
label="Password"
/>
<Field
name="passwordConfirm"
type="text"
component={renderField}
label="Confirm Password"
/>
<div>
<button
type="button"
className="previous btn btn-primary"
onClick={previousPage}>
Previous
</button>
<button
className="btn btn-primary"
type="submit"
disabled={pristine || submitting}>
Submit
</button>
</div>
</form>
);
};
export default reduxForm({
form: 'wizard', //Form name is same
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
validate
})(SignupThirdPage);
Old 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 SignUp extends Component {
constructor(props)
{
super(props);
this.handleFormSubmit = this.handleFormSubmit.bind(this);
}
handleFormSubmit(props) {
// Sign user up
this.props.signUpUser(props);
}
render() {
const { handleSubmit } = this.props;
return (
<div className="form-container">
<form onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
<Field name="firstName" component={renderField} type="text" label="First Name" />
<Field name="lastName" component={renderField} type="text" label="Last name" />
<Field name="email" component={renderField} type="email" label="Email" />
<Field name="company" component={renderField} type="text" label="Company"/>
<Field name="password" component={renderField} type="password" label="Password" />
<Field name="password_confirmation" component={renderField} type="password" label="Password Confirmation" />
<div>
{this.props.errorMessage && this.props.errorMessage.signup &&
<div className="error-container">Oops! {this.props.errorMessage.signup}</div>}
</div>
<button type="submit" className="btn btn-primary">Sign Up</button>
</form>
</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.auth.error
}
}
SignUp = reduxForm({ form: 'signup', validate })(SignUp);
export default connect(mapStateToProps, actions)(SignUp);
The answer:
Redux-Form says:
You can access your form's input values via the aptly-named values
prop provided by the redux-form Instance API.
This is how it works: since my component is connected, I have access to my actions in its props. Therefore, in my render method, I can simply pass the signUpUser method into the subcomponent, like this:
<SignupThirdPage
previousPage={this.previousPage}
signingUp={this.signingUp}
onSubmit={values => this.props.signUpUser(values)}
/>
Your problem is this:
{page === 3 && (
<SignupThirdPage
previousPage={this.previousPage}
onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}
/>
)}
If you carefully look at your code you will find that the function handleSubmit does not exist.
I think you were meaning to write this:
<SignupThirdPage
previousPage={this.previousPage}
onSubmit={this.handleFormSubmit.bind(this)}
/>

Categories

Resources