Redux-Form: Change in FieldArray submits parent form - reactjs

I'm stuck on an issue with redux-form. I have a form which is being populated with initialValues. Inside the top level form I have a FieldArray which renders a list of input fields. Each input field has buttons to remove it (via fields.remove(index)) or insert another input right after it (via fields.insert(index+1)). My problem is: when I call fields.insert() the new field gets inserted and registered correctly, but the parent form gets submitted, which it shouldn't. Same happens on fields.push() when the data array has at least one item. But it does not on e.g. fields.remove() or fields.removeAll(). I cannot figure out why/where the submittal occurs. I've spend hours digging through the source and playing around with the official FieldArray example which does work. I couldn't find the issue online so I guess I have a bug somewhere, since this is independent of the redux-form version in use (tried >=6).
Thanks for your help, here's the code.
Parent Form
import React from 'react';
import { Field, FieldArray, reduxForm } from 'redux-form';
import Row from 'muicss/lib/react/row';
import Col from 'muicss/lib/react/col';
import Button from 'muicss/lib/react/button';
import MaterialInput from '../material/material-input';
import MaterialSelect from '../material/material-select';
import MaterialFieldArray from '../material/material-fieldarray';
import Cover from './cover';
import CheckboxGroup from './checkbox-group';
const MovieDetailsEditForm = props => {
const { handleSubmit, initialValues: { cover: { large: cover }, directors = [], actors = [] } } = props;
const getFSKOptions = () => {
return [
{value: 0, label: 0},
{value: 6, label: 6},
{value: 12, label: 12},
{value: 16, label: 16},
{value: 18, label: 18}
];
};
const getFormats = () => {
return [{value: 'DVD', label: 'DVD'}, {value: 'Blu-ray', label: 'Blu-Ray'}];
}
const getImages = () => props.initialValues.images.map( (image) => ({ value: image.large, checked: false }) );
return (
<Row>
<form className="mui-form" onSubmit={handleSubmit(props.handleFormSubmit)}>
<Col xs="12">
<Button type="submit" color="primary">Speichern</Button>
</Col>
<Col xs="12">
<Row>
<Col xs="12" md="6">
<Row>
<Col xs="12">
<Field name="title" id="title" component={MaterialInput} label="Filmtitel" type="text" />
</Col>
<Col xs="6">
<Field name="duration" id="duration" component={MaterialInput} label={props.initialValues.unit} type="text" />
</Col>
<Col xs="6">
<Field
name="format"
id="format"
options={getFormats()}
component={MaterialSelect}
label="Format"
parse={(value, name) => value ? value : null}
/>
</Col>
<Col xs="12">
<Field
name="fsk"
id="fsk"
options={getFSKOptions()}
component={MaterialSelect}
label="FSK Einstufung"
labelText="Freigegeben ab <<PLACEHOLDER>> Jahren"
parse={(value, name) => value ? value : null}
/>
</Col>
{ directors &&
<Col xs="12">
<h3>Regisseur{directors.length > 1 ? 'e' : ''}</h3>
<FieldArray component={MaterialFieldArray} name="directors" />
</Col>
}
{ actors &&
<Col xs="12">
<h3>Cast</h3>
<FieldArray component={MaterialFieldArray} name="actors" />
</Col>
}
</Row>
</Col>
<Col xs="12" md="6" className="cover">
<Field {...props} name="cover" id="cover" component={Cover} />
</Col>
</Row>
</Col>
<Col xs="12">
<Field {...props} name="images" component={CheckboxGroup} valueProperty="large" />
</Col>
</form>
</Row>
);
}
export default reduxForm({
form: 'MovieDetails',
enableReinitialize: true
})(MovieDetailsEditForm);
material-fieldarray.js
import React from 'react';
import { Field } from 'redux-form';
import Row from 'muicss/lib/react/row';
import Col from 'muicss/lib/react/col';
import Button from 'muicss/lib/react/button';
import MaterialInput from '../material/material-input';
export default props => {
const { fields } = props;
const addEntry = index => fields.insert(index + 1, '');
const removeEntry = index => fields.remove(index);
const renderEntries = () => {
return fields.map((field, index) => {
return (<li key={index}>
<Col xs="7" sm="8" md="7" lg="8">
<Field component={MaterialInput} name={`${field}`} id={`${field}`} />
</Col>
<Col xs="5" sm="4" md="5" lg="4" className="buttons">
<Button variant="fab" size="small" color="primary" onClick={() => addEntry(index)}>+</Button>
<Button variant="fab" size="small" color="danger" onClick={() => removeEntry(index)}>-</Button>
</Col>
</li>);
})
}
return (
fields.length &&
<ul className="inputfield-list">
{ renderEntries() }
</ul>
|| <Button onClick={() => fields.push('')}>Add</Button>
)
}

OK, the problem was a very subtle one and has nothing to do with React or Redux-Form. I forgot to put type="button" on the buttons that add/remove items to the FieldArray.
<Button variant="fab" size="small" color="primary" onClick={() => addEntry(index)}>+</Button>
That's how HTML forms work.

Related

React useState hook does not shows correct state of array

I have two functional components and from parent component I am creating set of controls dynamically. Based on each item created I want to delete them but every time last one is getting deleted. For example three rows created when I delete second or first on,last one was getting deleted.
Education.jsx
function Education(props) {
const blankEdu = { id: 0, name: "", percentage: "", year: "" };
const [eduState, setEduState] = useState([{ ...blankEdu }]);
const addEducation = () => {
setEduState([...eduState, { ...blankEdu }]);
};
function handleRemove(index) {
console.log(index);
if (eduState.length != 1) {
const updatedEdu = [...eduState];
updatedEdu.splice(index, 1);
setEduState([...updatedEdu]);
}
}
const handleEducationChange = (index, e, c) => {
const updatedEdu = [...eduState];
updatedEdu[index][c] = e.target.value;
updatedEdu[index]["id"] = index;
setEduState(updatedEdu);
};
return (
<div>
<div className="shadow p-3 mb-5 bg-white rounded">
Final Step: Education
</div>
{eduState.map((val, idx) => (
<div
key={idx}
>
<EducationInput
key={`edu-${idx}`}
idx={idx}
handleEducationChange={handleEducationChange}
/>
{eduState.length > 1 ? (
<Button variant="danger" onClick={() => handleRemove(idx)}>
Remove Course
</Button>
) : null}
</div>
))}
<Button variant="outline-info" onClick={addEducation}>
Add New Degree
</Button>
</div>
);
}
export default Education;
EducationInput.jsx
const EducationInput = ({ idx, handleEducationChange }) => {
return (
<div key={`edu-${idx}`} id={`edu-${idx}`}>
<span className="border border-success">
<Form>
<Form.Group as={Row}>
<Form.Label column sm={3}>
{`Course #${idx + 1}`}:
</Form.Label>
<Col sm={5}>
<input
type="text"
onChange={e => handleEducationChange(idx, e, "name")}
/>
</Col>
</Form.Group>
<Form.Group as={Row}>
<Form.Label column sm={3}>
Passing Year:
</Form.Label>
<Col sm={5}>
<input
type="text"
onChange={e => handleEducationChange(idx, e, "year")}
/>
</Col>
</Form.Group>
</Form>
</span>
</div>
);
};
export default EducationInput;
I checked and verified value of updatedEdu by printing on console. It is giving correct output on console but setEduState function does not updating properly on UI, don't know why.
You are depending the index of the item, but indexes are changing when you add or remove elements, so they are not reliable.
You need to generate an automatic unique id when creating a new education.
For example uuid package is popular for this task.
I refactored your code a little bit to make it work:
Education:
import React, { useState } from "react";
import EducationInput from "./EducationInput";
import Button from "react-bootstrap/Button";
import uuidv4 from "uuid/v4";
function Education(props) {
const blankEdu = { id: "", name: "", percentage: "", year: "" };
const [eduState, setEduState] = useState([{ ...blankEdu }]);
const addEducation = () => {
setEduState([...eduState, { ...blankEdu, id: uuidv4() }]);
};
function handleRemove(id) {
console.log(id);
if (eduState.length > 1) {
const updatedEdus = eduState.filter(edu => edu.id !== id);
setEduState(updatedEdus);
}
}
const handleEducationChange = (id, field, value) => {
console.log(field, value);
let updatedEducations = eduState.map(edu => {
if (edu.id === id) return edu;
edu[field] = value;
return edu;
});
setEduState(updatedEducations);
};
return (
<div>
<div className="shadow p-3 mb-5 bg-white rounded">
Final Step: Education
</div>
{eduState.map(val => (
<div key={val.id}>
<EducationInput
key={`edu-${val.id}`}
idx={val.id}
handleEducationChange={handleEducationChange}
/>
{eduState.length > 1 ? (
<Button variant="danger" onClick={() => handleRemove(val.id)}>
Remove Course
</Button>
) : null}
</div>
))}
<Button variant="outline-info" onClick={addEducation}>
Add New Degree
</Button>
<br />
<br />
Educations in json:{JSON.stringify(eduState)}
</div>
);
}
export default Education;
EducationInput
import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Row from "react-bootstrap/Row";
const EducationInput = ({ idx, handleEducationChange }) => {
return (
<div key={`edu-${idx}`} id={`edu-${idx}`}>
<span className="border border-success">
<Form>
<Form.Group as={Row}>
<Form.Label column sm={3}>
{`Course #${idx + 1}`}:
</Form.Label>
<Col sm={5}>
<input
type="text"
name="name"
onChange={e =>
handleEducationChange(idx, e.target.name, e.target.value)
}
/>
</Col>
</Form.Group>
<Form.Group as={Row}>
<Form.Label column sm={3}>
Passing Year:
</Form.Label>
<Col sm={5}>
<input
type="text"
name="year"
onChange={e =>
handleEducationChange(idx, e.target.name, e.target.value)
}
/>
</Col>
</Form.Group>
</Form>
</span>
</div>
);
};
export default EducationInput;
Codesandbox

changing Redux state does not affect the render view

I am making a generic component and using it in a composite component also i pass some props to the generic component to behave in a certain way.I have used Redux to manage my state but when updating the state in Redux the component didn't re-render with the updated state.
Here are the components:
Generic component
export default class LabeledCheckBox extends Component {
constructor(props) {
super(props);
this.state = {
checked: false,
uncheckable: this.props.disableCheckBox
? this.props.disableCheckBox
: false
};
}
handleChange = (event) => {
this.setState({ checked: event.target.checked });
};
render() {
return (
<Form.Group as={Row}>
<Form.Label column="True" sm={9}>
{this.props.controlLabel}
</Form.Label>
<Col sm={3}>
<Form.Check
type="checkbox"
onChange={this.handleChange}
checked={this.state.checked}
onClick={this.props.clicked}
disabled={this.state.uncheckable}
/>
</Col>
</Form.Group>
);
}
}
LabeledCheckBox.propTypes = {
controlLabel: PropTypes.string.isRequired,
disableCheckBox: PropTypes.bool
};
parent component
export default class Endorsments extends Component {
constructor(props, context) {
super(props, context);
this.state = {
open: false
};
}
render() {
const { open } = this.state;
return (
<React.Fragment>
<LabeledCheckBox
clicked={() => this.setState({ open: !open })}
aria-controls="example-collapse-text"
aria-expanded={open}
controlLabel="Apply Endorsment"
disableCheckBox={!this.props.EndorsementSupported}
/>
<Collapse in={this.state.open}>
<div id="example-collapse-text">
<LabeledTextBoxWithCheckBox
controlLabel="BankName"
controlName="setBankName"
style={{ paddingBottom: '10px' }}
/>
<LabeledDateWithCheckBox controlLabel="Cheque Date" />
<Row>
<Col sm={6}>
<LabeledCheckBox controlLabel="User Name" />
</Col>
<Col sm={6}>
<LabeledCheckBox controlLabel="Cheque Sequence" />
</Col>
</Row>
</div>
</Collapse>
</React.Fragment>
);
}
}
Composit Component
class Preferences extends Component {
constructor(props, context) {
super(props, context);
this.props.fetchSupportedVendors();
}
loadScannersBasedOnVendor = () => {
if (
this.props.lastSelected.name === 'scannersVendors' ||
this.props.supportedScannerModule.length !== 0
) {
let select = this.props.supportedScannerModule.filter(
(element) =>
element.vendor ===
this.props.lastSelected.selectedObject.value
);
return select.map((element) => {
return { value: element.Value, label: element.name };
});
}
};
loadScannerMicrFonts = () => {
let supportedMicrs = [];
this.props.scannerCapabilities.supportedMicrFonts.forEach(
(element, key) => {
if (element.supported)
supportedMicrs.push({ value: key, label: element.value });
}
);
return supportedMicrs;
};
loadScannerBitDepth = () => {
let supportedBitDepth = [];
this.props.scannerCapabilities.supportedBitDepth.forEach(
(element, key) => {
if (element.supported)
supportedBitDepth.push({
value: key,
label: element.value
});
}
);
return supportedBitDepth;
};
afterSelectionEnded = () => {
if (
this.props.lastSelected &&
this.props.lastSelected.name === 'scannersModel' &&
!this.props.scannerCapabilities.supportedBitDepth
) {
this.props.loadScannerCapablilitiesToState(
this.props.lastSelected.selectedObject.value
);
}
};
render() {
return (
<Modal
{...this.props}
aria-labelledby="contained-modal-title-vcenter"
centered
dialogClassName="scanningModal"
>
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title-vcenter">
Preferences
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Row>
<h4>Default Scanner</h4>
</Row>
<Row>
<Col sm={12}>
<LabeledDropDown
controlLabel="Vendor"
controlName="scannersVendors"
placeholder="select a vendor please"
dropdownValues={
this.props.supportedScannerVendor
}
/>
</Col>
</Row>
<Row>
<Col sm={12}>
<LabeledDropDown
controlLabel="Model"
controlName="scannersModel"
placeholder="select model"
dropdownValues={[]}
loadDynamicItems={
this.loadScannersBasedOnVendor
}
afterValueChanged={this.afterSelectionEnded}
/>
</Col>
</Row>
<Row>
<Col sm={6}>
<VerticalLabeledDropDown
controlLabel="MICR Font"
controlName="ScannerMicrFont"
placeholder="select MICR Font"
dropdownValues={[]}
loadDynamicItems={this.loadScannerMicrFonts}
/>
</Col>
<Col sm={6}>
<VerticalLabeledDropDown
controlLabel="Bit Depth"
controlName="scannerBitDipth"
placeholder="select BitDepth"
dropdownValues={[]}
loadDynamicItems={this.loadScannerBitDepth}
/>
</Col>
</Row>
<hr />
<Row>
<h4>Scanner Feature</h4>
</Row>
<Endorsments
EndorsementSupported={
this.props.scannerCapabilities.supportedEndorsement
}
/>
<hr />
<Row>
<h4>General</h4>
</Row>
<Row>
<Col sm={6}>
<LabeledCheckBox controlLabel="Auto Save" />
</Col>
<Col sm={6}>
<LabeledCheckBox controlLabel="View While Scanning" />
</Col>
</Row>
<Row>
<Col sm={6}>
<LabeledCheckBox controlLabel="Use OCR for amount & date" />
</Col>
<Col sm={6}>
<LabeledCheckBox controlLabel="With UV scan" />
</Col>
</Row>
<Row>
<Col sm={12}>
<LabeledCheckBox controlLabel="Dont Show Prining language dialog" />
</Col>
</Row>
</Modal.Body>
<Modal.Footer bsPrefix="internal-modal-footer">
<Button onClick={this.props.onHide}>Close</Button>
<Button onClick={this.props.onHide}>Apply</Button>
</Modal.Footer>
</Modal>
);
}
}
const mapStateToProps = (state) => ({
supportedScannerVendor: state.supportedScanners.supportedVendors,
supportedScannerModule: state.supportedScanners.SupportedScanners,
lastSelected: state.dropdownEvents.dropDownSelectionChanged,
scannerCapabilities: state.supportedScanners.ScannerCapabilities
});
export default connect(mapStateToProps, {
fetchSupportedVendors,
loadScannerCapablilitiesToState
})(Preferences);
Finally
when calling loadScannerCapablilitiesToState the state is changed for this.props.scannerCapabilities.supportedEndorsement but it did not re-render the generic component to be disable or enabled
Am i missing something any help please?
if the Redux state is properly working then you need to change the state in LabeledCheckBox component.
componentDidUpdate() is invoked immediately after updating occurs.
componentDidUpdate = (prevProps, prevState) => {
if (prevProps.disableCheckBox !== this.props.disableCheckBox) {
this.setState({ uncheckable: this.props.disableCheckBox });
}
};
Read more about React lifecycle

How to call OnChange function in react using withformik with antd component?

Here I'm calling onChange function on Formik Field but its not calling? How to call custom function on a Formik field?
This is my custom function under React Component:
onStudentScore = (value, form) => {
alert("called");
const maxScore = value.writtenexammaxscore;
console.log(maxScore);
form.getFieldValue("writtenexammaxscore", maxScore);
if (maxScore > form.getFieldValue("writtenexamstudentsscore")) {
alert("MaxScore is less than StudentScore");
}
};
And my Form is created under render and write a onChange function on a StudentScore field. But it's not called? How to call this function?
render() {
const { values, handleSubmit} = this.props
return (
return (
<div>
<h5 align="left">MidTerm Form</h5>
<Card>
<Form onSubmit={handleSubmit}>
<Row>
<Col span={4}>
<b>Written Exam:</b>
</Col>
<Col span={2}>
<Field
name="writtenexammaxscore"
component={AntInput}
type="text"
style={{ width: 40 }}
/>
</Col>
<Col span={2}>outof</Col>
<Col span={3}>
<Field
name="writtenexamstudentsscore"
component={AntInput}
type="text"
style={{ width: 40 }}
onChange={this.onStudentScore}
/>
// I wrote the function on field this way
</Col>
<Col span={2}>
<Divider type="vertical" />
</Col>
</Row>
<Row>
<Col span={10} />
<Col span={8} push={10}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Col>
</Row>
</Form>
</Card>
</div>
);
}
const MidTermForm = withFormik({
mapPropsToValues: () => ({
writtenexammaxscore: '',
writtenexamstudentsscore: '',
oralexammaximumscore: '',
oralexamstudentsscore: '',
}),
handleSubmit(values, { resetForm }) {
resetForm();
console.log(values)
}
})(MidTermFormComponent)
export default MidTermForm
I tried by extending yup validation schema. Instead of calling a function in onChange
check this code sandbox

How to independently delete dynamically-added input fields in ReactJS

I'm trying to independently delete dynamic inputs in a form in React. I have a user attribute group, and then user attribute children. I need to be able to dynamically add new user attribute groups and children, but then delete those fields without deleting ALL of the child attributes.
Right now, when I delete a child attribute, it deletes one from EACH user attribute group.
I have a working fiddle here that shows my code: https://codesandbox.io/embed/23kr654w80
import React, { Component } from "react";
import { Button, Input, Row, Col, Form, FormGroup, Label } from "reactstrap";
class OfferCriteria extends Component {
constructor(props) {
super(props);
this.state = {
single: "",
attributeSingle: [{ single: "" }],
child: "",
attributeChild: [{ child: " " }]
};
}
handleNameChange = event => {
this.setState({
name: event.target.value
});
};
handleAddSingleAttribute = () => {
this.setState({
attributeSingle: this.state.attributeSingle.concat([{ name: "" }])
});
};
handleRemoveSingleAttribute = idx => () => {
this.setState({
attributeSingle: this.state.attributeSingle.filter(
(s, sidx) => idx !== sidx
)
});
};
handleAddChildAttribute = () => {
this.setState({
attributeChild: this.state.attributeChild.concat([{ child: "" }])
});
};
handleRemoveChildAttribute = idz => () => {
this.setState({
attributeChild: this.state.attributeChild.filter(sidz => idz !== sidz)
});
};
render() {
return (
<div>
<Row>
<Col lg="10">
<hr />
</Col>
<Col lg="2" className="float-right">
<Button color="success" onClick={this.handleAddSingleAttribute}>
Add Attribute Group
</Button>
</Col>
</Row>
{this.state.attributeSingle.map(() => (
<div>
<br />
<Row>
<Col lg="2">
<Label>User Attributes</Label>
</Col>
<Col lg="3" className="float-left">
<FormGroup check inline>
<Input
className="form-check-input"
type="radio"
id="includeUserAttributes"
name="inline-radios"
value="includeUserAttributes"
/>
<Label
className="form-check-label"
check
htmlFor="inline-radio1"
>
Include
</Label>
</FormGroup>
<FormGroup check inline>
<Input
className="form-check-input"
type="radio"
id="excludeUserAttributes"
name="inline-radios"
value="excludeUserAttributes"
/>
<Label
className="form-check-label"
check
htmlFor="inline-radio2"
>
Exclude
</Label>
</FormGroup>
</Col>
<Col lg="4">
<Input
type="text"
name="text-input"
placeholder="This is parent attribute"
/>
</Col>
</Row>
<br />
<Row>
<Col lg="3">
{this.state.attributeChild.map(() => (
<div className="shareholder">
<Input
type="text"
name="text-input"
placeholder="This is child attribute"
/>
</div>
))}
</Col>
<Col lg="3" className="float-right">
{this.state.attributeChild.map(() => (
<div className="shareholder">
<Button
color="primary"
onClick={this.handleAddChildAttribute}
>
Add Attribute Child
</Button>
<br />
</div>
))}
</Col>
<Col lg="3" className="float-right">
{this.state.attributeChild.map(idz => (
<div className="shareholder">
<Button
color="danger"
onClick={this.handleRemoveChildAttribute(idz)}
>
Remove Attribute Child
</Button>
<br />
</div>
))}
</Col>
</Row>
<hr />
</div>
))}
</div>
);
}
}
export default OfferCriteria;
I need these child attributes to delete ONLY in their parent attribute group, instead of deleting all of them from all the attribute groups.
There are a couple of things going wrong with your code, but I'll focus on your initial question.
The problem is that you use the same array of child for all your groups. In order to be correct, you should include the attributeChild state into the attributeSingle objects :
{
attributeSingle: [
{
single: "",
attributeChild: [
{
child: " "
}
]
}
]
}
That way, children remain independent between groups.

react+redux with react-bootstrap > FormControl > get value

I am using react+redux with react-bootstrap components.
I would like to pass the value of a FormControl text element (email) to the dispatched redux action but I do not know how to do that.
class LoginForm extends React.Component {
render() {
const email = React.findDOMNode(this.refs.email);
return (
<div>
<Form horizontal>
<FormGroup controlId="formHorizontalEmail">
<Col componentClass={ControlLabel}>Email</Col>
<Col><FormControl type="email" ref="email"/></Col>
</FormGroup>
<FormGroup>
<Col>
<Button type="submit" block>Sign in</Button>
</Col>
</FormGroup>
</Form>
<Button onClick={() => this.props.doLogin(email, 'password')}>Login</Button>
</div>
)
}
}
/**
* Connect staff.
*/
const mapStateToProps = (state) => {
return {
...
};
};
const mapDispatchToProps = (dispatch) => {
return {
doLogin: (email, password) => dispatch(performLogin(email, password))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm)
The only one way how to read text value of a FormControl with React (according to my research) is this:
class LoginForm extends React.Component {
handleOnChange = (event) => {
this.setState({ [event.target.id]: event.target.value }, null);
}
render() {
return (
<div>
<Form horizontal>
<FormGroup controlId="email">
<Col componentClass={ControlLabel}}>Email</Col>
<Col>
<FormControl type="email" placeholder="Email" onChange={this.handleOnChange}
/>
</Col>
</FormGroup>
<FormGroup controlId="password">
<Col componentClass={ControlLabel} sm={2}>Password</Col>
<Col>
<FormControl type="password" placeholder="Password" onChange={this.handleOnChange} />
</Col>
</FormGroup>
<FormGroup>
<Col>
<Button onClick={() => this.props.doLogin(this.state.email, this.state.password)}>Submit</Button>
</Col>
</FormGroup>
</Form>
</div>
)
}
}

Resources