redux form dynamic categories - reactjs

i am using redux form and what i do here is when user selects category the next select field should have all subcategories depending of category selected.
what i did was i created api for fetching all categories, i trigger action via componentWillMount and load all categories in first categories select field, then i use formValueSelector of redux-form to get selected category to the state/this.props, then i use componentWillReceiveProps() to trigger fetching subcategories with that e.g. "this.props.categoryId" that i put to state with formValueSelector and that works.
and my question is, is this the right aporoach, is there a better way?
and the second question, is how do i reset categoryChildId field to, lets say blank when the categoryId field is changed?
import React from 'react';
import moment from 'moment';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { connect } from 'react-redux';
import { Link, NavLink } from 'react-router-dom';
import {CopyToClipboard} from 'react-copy-to-clipboard';
import * as actions from '../../actions/category';
const renderField = ({ input, label, type, meta: { touched, error } }) => (
<div>
<input {...input} placeholder={label} type={type} />
{touched &&
error &&
<div className="error">{error}</div>}
</div>
)
const renderTextArea = ({ input, label, type, meta: { touched, error } }) => (
<div>
<textarea {...input} placeholder={label} type={type} />
{touched &&
error &&
<div className="error">{error}</div>}
</div>
)
class AddProduct extends React.Component {
constructor(props) {
super(props);
this.state = {
value: `${process.env.SITE_URL}/user/${props.user.username}`,
copied: false,
isLoading: false
};
}
componentWillMount() {
this.props.startSetCategories()
}
componentWillReceiveProps(nextProps) {
this.props.startSetCategoryChildren(nextProps.categoryId)
console.log(nextProps)
}
renderCategorySelector = ({ input, meta: { touched, error } }) => {
return (
<div>
<select {...input}>
<option value="">select category</option>
{!this.props.categories ? (
<option value="">loading...</option>
) : (
this.props.categories.map(category => <option value={category._id} key={category._id}>{category.name}</option>)
)
}
</select>
{touched && error && <span>{error}</span>}
</div>
)
}
renderCategoryChildSelector = ({ input, meta: { touched, error } }) => {
return (
<div>
<select {...input}>
<option value="">select sub category</option>
{!this.props.categoryChildren ? (
<option value="">loading...</option>
) : (
this.props.categoryChildren.categoryChildren.map(categoryChild => <option value={categoryChild._id} key={categoryChild._id}>{categoryChild.name}</option>)
)
}
</select>
{touched && error && <span>{error}</span>}
</div>
)
}
submitForm = values => {
console.log(values)
}
render() {
const username = localStorage.getItem('username');
const { user } = this.props;
const { handleSubmit, pristine, submitting, categoryId } = this.props;
return (
<div className="profile-wrapper">
<div className="profile">
<form className="profile-addproduct-left" onSubmit={handleSubmit(this.submitForm.bind(this))}>
<div className="profile-addproduct-title">
<h2>New Product</h2>
<p>Fill out the form.</p>
</div>
<div className="profile-form-group">
<div className="profile-form-item">
<p>Title</p>
<Field
name="title"
type="text"
label="title of a product"
component={renderField}
/>
</div>
<div className="profile-form-item">
<p>Category</p>
<Field
name="categoryId"
type="text"
component={this.renderCategorySelector}
label="category"
/>
{this.props.categoryId ?
<Field
name="categoryChildId"
type="text"
component={this.renderCategoryChildSelector}
label="categoryChild"
/> :
''
}
</div>
<div className="profile-form-item">
<p>Description</p>
<Field
name="description"
type="text"
label="Write some interesting..."
component={renderTextArea}
/>
</div>
</div>
<div className="profile-addproduct-form-submit">
<button className="button button--register" type="submit" disabled={this.state.isLoading || pristine}>Submit New Product</button>
</div>
</form>
</div>
</div>
)
}
};
AddProduct = reduxForm({
form: 'addproduct-form'
})(AddProduct)
const selector = formValueSelector('addproduct-form')
AddProduct = connect(state => {
const categoryId = selector(state, 'categoryId')
return {
categoryId,
categories: state.category.categories,
categoryChildren: state.category.categoryChildren
}
}, actions)(AddProduct)
export default AddProduct

You should not call the startSetCategoryChildren (or any other api) in componentWillReceiveProps... because it would call every time whenever componentWillReceiveProps called
componentWillReceiveProps(nextProps) {
this.props.startSetCategoryChildren(nextProps.categoryId)
console.log(nextProps)
}
Instead of this you can to do this on handleChange of the Field
<Field
name="categoryId"
type="text"
component={this.renderCategorySelector}
label="category"
onChange={(e) => this.props.startSetCategoryChildren(e.target.value)}
/>

Related

How to change a field in a redux-form?

In my react component I am trying to set a field called 'total'. I have imported the change action as a prop into my component:
import React, { Component, Fragment } from 'react'
import { Field, FieldArray, reduxForm, getFormValues, change } from 'redux-form'
import { connect } from 'react-redux'
import { CalcTotal } from './calculationHelper';
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 renderMods = ({ fields, meta: { error, submitFailed } }) => (
<Fragment>
<ul>
<li>
<button type="button" onClick={() => fields.push({})}>
Add Modification
</button>
{submitFailed && error && <span>{error}</span>}
</li>
{fields.map((mod, index) => (
<li key={index}>
<button
type="button"
title="Remove Mod"
onClick={() => fields.remove(index)}
/>
<h4>Mod #{index + 1}</h4>
<Field
name={`${mod}.lastYear`}
type="number"
component={renderField}
label="Last Year"
/>
<Field
name={`${mod}.currentYear`}
type="number"
component={renderField}
label="Current Year"
/>
<Field name={`${mod}.type`} component="select" label="Type">
<option />
<option value="-">Expense</option>
<option value="+">Income</option>
<option value="-">Tax</option>
</Field>
</li>
))}
</ul>
<Field
name="total"
type="number"
component="input"
label="Total modifications"
text="0"
/>
</Fragment>
)
class FieldArraysForm extends Component {
render() {
const { handleSubmit, formValues, change } = this.props
if (formValues) {
console.log('formvalues', formValues);
const test = CalcTotal(2000);
console.log('calc=', test);
debugger
this.props.change('fieldArraysForm', 'total', 5000)
}
return (
<form onSubmit={handleSubmit}>
{/* <button onClick={this.changeStuff}>set total</button> */}
<FieldArray name="mods" component={renderMods} />
<div>
<button type="submit" >
Submit
</button>
</div>
</form>
)
}
}
const mapStateToProps = (state) => ({
formValues: getFormValues('fieldArraysForm')(state),
});
const mapDispatchToProps = {
change
};
// const Example = reduxForm({
// form: 'fieldArraysForm', // a unique identifier for this form
// })(FieldArraysForm)
// const ConnectedForm = connect(
// mapStateToProps,
// mapDispatchToProps,
// )(Example);
// export default ConnectedForm
export default reduxForm({
form: "fieldArraysForm"
})(
connect(
mapStateToProps,
mapDispatchToProps
)(FieldArraysForm)
);
The line where the code fall into an infinite loop:
this.props.change('fieldArraysForm', 'total', 5000)
How /where do I put this statement to make sure the 'total' field is changed and not get into a loop?Which React lifecycle event would suit? I want to fire this whenever there is a form change on any field.
You'll need to move your statement out of the render method and into the componentDidUpdate lifecycle method (you also need an if statement to prevent an infinite loop):
componentDidUpdate(prevProps) {
if (this.props.someValue !== prevProps.someValue) {
this.props.change("formName", "formField", "newFormValue");
}
}
Working example: https://codesandbox.io/s/r5zz36lqnn (selecting the Has Email? radio button populates the email field with test#example.com, unselecting the radio button resets the email field to "")

redux-form Dinamically inject objects into FieldArray `field` property

What I am trying to accomplish is a dynamic FieldArray similar to the one in the redux form example but the difference is that every object I am adding to the list already has all properties (which i have to display) but one that I need to calculate from other two of them.
Take a look at this example:
import React from 'react'
import { compose } from 'redux';
import { connect } from 'react-redux';
import { Field, FieldArray, reduxForm, formValueSelector } from 'redux-form'
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 renderSelect = (products) => (
<div>
<label>Select Product to Add</label>
<div>
<Field name="products" component="select">
<option></option>
{products.map(p => <option value={p.id}>{p.name}</option>)}
</Field>
</div>
</div>
);
const renderCompetences = ({ fields, meta: { touched, error, submitFailed }}) => {
return (
<div>
{/* <div>
<button type="button" onClick={() => fields.push({})}>Add Competence</button>
{(touched || submitFailed) && error && <span>{error}</span>}
</div> */}
<ul>
{fields.map((competence, index) =>
<li key={index}>
<h4>Competence #{index + 1}</h4>
<Field
name={`${competence}.singlePrice`}
type="number"
component={renderField}
label="Single Price"/>
<Field
name={`${competence}.standardQuantity`}
type="number"
component={renderField}
label="Standard Quantity"/>
<Field
name={`${competence}.totalPrice`}
type="number"
component={renderField}
label="Total Price"/>
<button
type="button"
onClick={() => fields.remove(index)}
style={{color: 'red'}}
>
✘
</button>
</li>
)}
</ul>
</div>
);
}
const FieldArraysForm = (props) => {
const { handleSubmit, pristine, reset, submitting, products, productsValue } = props;
return (
<form onSubmit={handleSubmit}>
<Field name="recipientName" type="text" component={renderField} label="Recipient Name"/>
<FieldArray name="competences" component={renderCompetences} />
{renderSelect(products)}
<div>
<button type="submit" disabled={submitting}>Submit</button>
<button type="button" disabled={pristine || submitting} onClick={reset}>Clear Values</button>
</div>
</form>
)
}
const selector = formValueSelector('fieldArrays');
const mapStateToProps = (state) => {
const productsValue = selector(state, 'products');
return { productsValue };
};
export default compose(
connect(mapStateToProps),
reduxForm({
form: 'fieldArrays'
})
)(FieldArraysForm);
I should display the products I selected form the select in my unordered list and every time I select a new object I should add it to the list.
Here you can see an example of the products array:
const products = [
{ id: '0', name: 'Product One', singlePrice: 101, standardQuantity: 1},
{ id: '1', name: 'Product Two', singlePrice: 39, standardQuantity: 6},
{ id: '2', name: 'Product Three', singlePrice: 85, standardQuantity: 4},
{ id: '3', name: 'Product Four', singlePrice: 1, standardQuantity: 20}
];
Here is a working example to test some solutions:
https://www.webpackbin.com/bins/-Kecgj77Gbxo_4am3WS1
Thank you so much for any help you can give!
This is the solution i came up with:
import React from 'react'
import { compose } from 'redux';
import { connect } from 'react-redux';
import { Field, FieldArray, reduxForm, formValueSelector } from 'redux-form'
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 renderCalcField = ({ input, label, type, meta: { touched, error }, calc }) => (
<div>
<label>{label}</label>
<div>
<input {...input} type={type} placeholder={label} value={calc()} />
{touched && error && <span>{error}</span>}
</div>
</div>
)
const renderCompetences = ({ fields, meta: { touched, error, submitFailed }, products, productSelectValue }) => {
return (
<div>
<ul>
{fields.map((competence, index) =>
<li key={index}>
<h4>Competence #{index + 1}</h4>
<Field
name={`${competence}.singlePrice`}
type="number"
component={renderField}
label="Single Price"
onChange={() => {
const current = fields.get(index);
current.totalPrice = current.singlePrice * current.standardQuantity;
fields.remove(index);
fields.insert(index, current);
}}
/>
<Field
name={`${competence}.standardQuantity`}
type="number"
component={renderField}
label="Standard Quantity"
/>
<Field
name={`${competence}.totalPrice`}
type="number"
component={renderCalcField}
props={{calc: () => {
const current = fields.get(index);
return current.singlePrice * current.standardQuantity;
}}}
label="Total Price"
/>
<button
type="button"
onClick={() => fields.remove(index)}
style={{color: 'red'}}
>
✘
</button>
</li>
)}
</ul>
<div>
<Field name="productSelect" component="select">
<option>Select product</option>
{products.map(p => <option value={p.id}>{p.name}</option>)}
</Field>
<button type="button" onClick={() => {
const selectedProduct = products.find(p => p.id === productSelectValue);
fields.push(selectedProduct);
}}>Add</button>
{(touched || submitFailed) && error && <span>{error}</span>}
</div>
</div>
);
}
const FieldArraysForm = (props) => {
const { handleSubmit, pristine, reset, submitting, products, productSelectValue } = props;
return (
<form onSubmit={handleSubmit}>
<Field name="recipientName" type="text" component={renderField} label="Recipient Name"/>
<FieldArray name="competences" component={renderCompetences} props={{products, productSelectValue}}/>
<div>
<button type="submit" disabled={submitting}>Submit</button>
<button type="button" disabled={pristine || submitting} onClick={reset}>Clear Values</button>
</div>
</form>
)
}
const selector = formValueSelector('fieldArrays');
const mapStateToProps = (state) => {
const productSelectValue = selector(state, 'productSelect');
return { productSelectValue };
};
export default compose(
connect(mapStateToProps),
reduxForm({
form: 'fieldArrays'
})
)(FieldArraysForm);
working here on webpackbin (sometimes it has loading problems but it does not depend on my example)

unable to set defaultValue in redux-form-material-ui TextField

I'm unable to set a default value for the textField component.
I tried using default value, but as long as I'm using the redux-form-material-ui it just doesn't work.
I really don't understand what am I doing wrong (seems pretty basic)...
exmaple (just changed their fieldArray example a little):
import React from 'react'
import { Field, FieldArray, reduxForm } from 'redux-form'
import validate from './validate'
import {TextField} from 'redux-form-material-ui'
const renderField = (props) => {
console.log(props);
const { input, label, type, meta: { touched, error } } = props;
console.log(input);
return <div>
<label>{label}</label>
<div>
// Will not show "someValue", it will just be blank
<TextField defaultValue="someValue" {...input} type={type} placeholder={label}/>
{touched && error && <span>{error}</span>}
</div>
</div>
}
const renderMembers = ({ fields, meta: { touched, error, submitFailed } }) => (
<ul>
<li>
<button type="button" onClick={() => fields.push({})}>Add Member</button>
{(touched || 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 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 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)
Thanks.
I was having the same issue. Just read through the documentation again and saw this
No Default Values
Because of the strict "controlled component" nature of redux-form, some of the Material UI functionality related to defaulting of values has been disabled e.g. defaultValue, defaultDate, defaultTime, defaultToggled, defaultChecked, etc. If you need a field to be initialized to a certain state, you should use the initialValues API of redux-form.
It's probably only one way to make form of editing.
It cannot be restored from initial redux Form values.
Input
export default function ({ input, label, meta: { touched, error }, ...custom }) {
if ( input.value === '' && custom.cvalue ) { // hack for redux form with material components
input.onChange(String(custom.cvalue));
}
return (
<TextField
{...input}
{...custom}
fullWidth={true}
hintText={label}
floatingLabelText={label}
errorText={touched && error}
/>
)
}
Select
export default function ({ input, label, meta: { touched, error }, onChange, children, ...custom }) {
if ( input.value === '' && custom.cvalue ) { // hack for redux form with material components
if ( is.function(onChange) ) {
onChange(custom.cvalue);
}
input.onChange(custom.cvalue);
}
return (
<SelectField
{...input}
{...custom}
fullWidth={true}
children={children}
floatingLabelText={label}
errorText={touched && error}
onChange={(event, index, value) => {
if ( is.function(onChange) ) { // and custom onChange for f....g ....
value = onChange(value);
}
input.onChange(value);
}}/>
)
}
then in template
its can be way to make form of editing existing entity ....
<Field
name="study"
label="Studies"
component={ FormSelect }
cvalue={this.state.study.id}
onChange={value => {
this.setState({study: _.find(studies, {id: value})||{id: 0}});
return value;
}}>
{studies.map( (study, key) => ( <MenuItem key={key} value={study.id} primaryText={study.officialTitle} /> ))}
</Field>

redux-form (version 6.4.3) does not display errors

below is the code for the redux-form. Everything works beautifully with the redux store but I can not display the error messages in the span element.
import React from 'react'
import { Button } from 'react-bootstrap'
import { Field, reduxForm } from 'redux-form'
const validate = (values) => {
const errors = {}
if (!values.firstname) {
errors.firstname = 'Required'
}
return errors
}
const renderInput = (field) => (
<div>
<label>{field.placeholder}</label>
<div>
<input {...field.input}/>
{field.error && <span>{field.error}</span>}
</div>
</div>
)
#reduxForm({
form: 'addUserForm',
validate
})
export default class CreateUserForm extends React.Component {
render() {
const {addUser, handleSubmit} = this.props
return (
<form onSubmit={handleSubmit(addUser)}>
<Field type="text" placeholder="First name" component={renderInput} name="firstname" />
<Button type="submit" className="btn btn-success">Submit</Button>
</form>
)
}
}
I can clearly see that the validation function works (see screen shot below)
but there is nothing in the <span></span> element, which means the field.error has no value. I also don't get an error message at all.
Does someone know what's going on here?
Thanks,
Thomas
Your renderInput is incomplete.
The official document shows:
const renderField = ({ input, label, type, meta: { touched, error, warning } }) => (
<div>
<label>{label}</label>
<input {...input} placeholder={label} type={type}/>
{
touched && (
(error && <span>{error}</span>) || (warning && <span>{warning}</span>)
)
}
</div>
)
Observe the object, parameters passed to renderField: meta: { touched, error, warning }
With that regards, shouldn't your renderInput be:
const renderInput = (field) => (
<div>
<label>{field.placeholder}</label>
<div>
<input {...field.input}/>
{field.meta.error && <span>{field.meta.error}</span>}
</div>
</div>
)
Missing => field.meta.error

Redux form with self creating inputs

I'm trying to create a redux form (using redux-form) that can dynamically create it's own inputs. I am having trouble figuring out how to make redux-form aware of the new fields that have been created. Is it possible to dynamically change the fields prop that redux-form passes in within the form component itself? Am I thinking about this wrong? Here is what I am working with.
class AddCustomer extends Component {
render() {
class Form extends Component {
constructor(props, context) {
super(props, context)
this.state = {
inputsToAdd: []
};
}
handleAddInput() {
let inputsToAdd = this.state.inputsToAdd.slice();
inputsToAdd.push(this.state.inputsToAdd.length);
this.setState({ inputsToAdd });
}
submitForm(data) {
console.log(data)
this.setState({inputsToAdd: []});
this.props.dispatch(initialize('addCustomer', {}))
}
render() {
const { fields, handleSubmit } = this.props;
return (
<div>
<form onSubmit={handleSubmit(this.submitForm.bind(this))}>
<Input type='text' label='Company name' {...fields['companyName']}/>
<Input type='email' label='Admin user email' {...fields['adminEmail']}/>
</form>
{this.state.inputsToAdd.map((element, index) => {
return (
<Input key={index} type='text' />
)
})}
<Button onClick={() => this.handleAddInput()}>Add Input</Button>
<Button onClick={handleSubmit(this.submitForm.bind(this))}>Submit</Button>
</div>
)
}
}
Form = connectReduxForm({
form: 'addCustomer',
fields: ['companyName', 'adminEmail']
})(Form);
return (
<div>
<h1>Add Customer</h1>
<Form />
</div>
)
}
}
As of Redux Form 6.* you can achieve what you are trying to do using <FieldArray/>
See the example below (taken from Redux documentation and slightly simplified)
import React from 'react'
import { Field, FieldArray, reduxForm } from 'redux-form'
import validate from './validate'
const renderMembers = ({ fields, meta: { touched, error } }) => (
<ul>
<li>
<button type="button" onClick={() => fields.push({})}>Add Member</button>
{touched && 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"/>
</li>
)}
</ul>
)
const FieldArraysForm = (props) => {
const { handleSubmit, submitting } = props
return (
<form onSubmit={handleSubmit}>
<FieldArray name="members" component={renderMembers}/>
<div>
<button type="submit" disabled={submitting}>Submit</button>
</div>
</form>
)
}
export default reduxForm({
form: 'fieldArrays', // a unique identifier for this form
validate
})(FieldArraysForm)
For more info checkout the documentation
http://redux-form.com/6.1.1/examples/fieldArrays/

Resources