Nested form state and one onChange function in React - forms

I'm working on a form in React. I have nested state in MatchForm component and "bind" values from state with different inputs. I would like to have on onChange function which manages all cinput changes and pass it to state. Current onChange function works when I have only lineup inputs. But when I added other inputs I have no idea how to handle with it. How should I name inputs and how onChange function should look like to work with all inputs (without many ifs)?
Thank you in advance
class MatchForm extends React.Component {
constructor(props) {
super(props);
this.state = {
lineup: {
setter: '',
receiver1: '',
receiver2: '',
attacker: '',
blocker1: '',
blocker2: '',
libero: ''
},
distribution: {
receiver1: 20,
receiver2: 20,
attacker: 20,
blocker1: 20,
blocker2: 20
},
risk: 'normal',
default: false
};
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.setState({lineup: { ...this.state.lineup, [e.target.name]:e.target.value } })
}
render() {
return (
<div>
{
Object.keys(this.state.lineup).map((el,i) =>
<select
key={i}
name={el}
onChange={this.onChange}
value={this.state.lineup[el]}
>
<option value="" disabled>{el}</option>
<option value="Marian Noga">Marian Noga</option>
<option value="Janek Kowalski">Janek Kowalski</option>
</select>
)
}
{
Object.keys(this.state.distribution).map((el,i) =>
<Field
key={i}
field={el}
label={el}
type='number'
value={this.state.distribution[el]}
onChange={this.onChange} />
)
}
<select
name="risk"
onChange={this.onChange}
value={this.state.risk}
>
<option value="" disabled>risk</option>
<option value="safe">safe</option>
<option value="normal">normal</option>
<option value="risk">risk</option>
</select>
<label><input name="default" type="checkbox" value={this.state.default} onChange={this.onChange} /> Set as default lineup</label>
</div>
)
}
}
export default MatchForm

You can bind the onChange function with different values for each of your fields:
onChange={this.onChange.bind(this, 'lineup', el)}
Then if you write your onChange like this:
onChange(field, name, e) {
this.setState({[field]: { ...this.state[field], [name]:e.target.value } });
}
You should be able now to repeat that for each of your fields.

If it is just a controlled input, and no other UI logic, I do something as simple as this:
<input type="text"
name="email"
value={this.state.email}
onChange={e => this.setState({ email: e.target.value })}
/>

You could also try _.set for more complex use cases. Good thing is, the set function alone can be downloaded as an npm package
<input type="text" name="lineup.receiver1" onChange={handleChange} value={this.state.lineup.receiver1} />
import update from "lodash.set";
const handleChange = (ev) => { update(this.state, ev.target.name, ev.target.value ) }
_.set has more complex options for updating a nested object and can be used in our case with HTML name property of a field.

Related

How to enable required validation in vuelidate based on onChange event of select field

I have to enable required validation for the input field based on the onChange event of select field. I'm using vuelidate package for form validation in my project. Kindly provide solution to accomplish it.
My Template Code Below:
<template>
<section class="page_blk">
<form #submit="submitForm($event)" class="cryp_form">
<div class="input_ctrl_wrp">
<label for="username">Template</label>
<div class="input_select">
<select #change="getTemplate" v-model="$v.adForm.tnc.$model" name="" id="">
<option value="">Select</option>
<option value="New">New</option>
<option :value="term.idTnCTemplate"
v-for="term in termsList"
:key="term.idTnCTemplate">{{term.title}}</option>
</select>
<i class="fal fa-angle-down"></i>
</div>
</div>
<div class="input_ctrl_wrp">
<label for="username">Title</label>
<div class="input_text">
<input v-model="$v.adForm.title.$model" placeholder="" type="text">
</div>
</div>
<div class="input_ctrl_wrp">
<label for="username">Terms Of Trade</label>
<div class="input_textarea">
<textarea v-model="$v.adForm.content.$model" name="" rows="10"></textarea>
</div>
</div>
</form>
</section>
</template>
My Script Below:
<script>
import { required,requiredIf, decimal, numeric } from "vuelidate/lib/validators";
export default {
data() {
return {
adForm: {
tnc: '',
title: '',
content: '',
}
}
},
validations: {
adForm: {
tnc: {
required
},
title: {
required
},
content: {
required: requiredIf( (abc) => {
console.log('abc',abc)
return true;
})
},
schedule: {
required
}
}
},
methods: {
submitForm(e) {
},
getTemplate(e) {
}
},
mounted() {
}
}
</script>
I want to toggle the validation to required for the title and content field, if the user select new and other option from dropdown. Please provide solution to accomplish it. Thanks in advance.

Are these bugs in lit-html?

In lit-html 1.0.0-rc.2, I have the following template, which does not work correctly. So, I suppose I'm doing something wrong. Or is this just a bug in lit-html?
import { html } from './node_modules/lit-html/lit-html.js';
import css from './css.js';
export default function template(c) {
console.log('props', c.text, c.selected, c.checkbox, c.radioButton);
const changeText = c.changeText.bind(c);
return html`
${css()}
<form>
<div>input cannot be updated programatically</div>
<input type="text" value="${c.text}" #input="${changeText}"/>
<div>select cannot be set/changed programatically</div>
<select #change="${ev => {c.selected = ev.currentTarget.value; console.log('value set to', ev.currentTarget.value)}}">
<option value="" ?selected="${c.selcted === ''}">Select</option>
<option value="1" ?selected="${c.selcted === '1'}">1</option>
<option value="2" ?selected="${c.selcted === '2'}">2</option>
<option value="3" ?selected="${c.selcted === '3'}">3</option>
<option value="4" ?selected="${c.selcted === '4'}">4</option>
</select>
<div>checkbox cannot be updated programatically</div>
<input
type="checkbox"
#change="${(ev) => {c.checkbox = ev.currentTarget.value; console.log('checkbox value:', ev.currentTarget.value)}}"
?checked="${c.checkbox === 'on'}"
/>
<div>radio buttons cannot be updated programatically</div>
<input
name="radio"
type="radio"
value="1"
#change="${ev => {c.radioButton = '1'; console.log('radio button value: ', ev.currentTarget.value)}}"
?checked="${c.radioButton === '1'}"/>
<label>1</label>
<input
name="radio"
type="radio"
value="2"
#change="${ev => {c.radioButton = '2'; console.log('radio button value: ', ev.currentTarget.value)}}"
?checked="${c.radioButton === '2'}"/>
<label>2</label>
<input
name="radio"
type="radio"
value="3"
#change="${ev => {c.radioButton = '3'; console.log('radio button value: ', ev.currentTarget.value)}}"
?checked="${c.radioButton === '3'}"/>
<label>3</label>
</form>
`;
}
It is populated/controlled by the following web component:
import { render } from './node_modules/lit-html/lit-html.js';
import template from './template.js';
class MyForm extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.text = 'foo';
this.selected = '2';
this.checkbox = 'on';
this.radioButton = '1';
}
attributeChangedCallback(name, oVal, nVal) {
}
connectedCallback() {
this.render();
setInterval(() => {
this.text = 'foobar';
this.selected = '3';
this.checkbox = 'off';
this.radioButton = '2';
this.render();
}, 2000);
}
disconnectedCallback() {
}
changeText(ev) {
const { value } = ev.currentTarget;
this.text = value;
this.render();
}
render() {
render(template(this), this.shadowRoot);
}
static get observedAttributes() {
return ['']
}
}
customElements.get('my-form') || customElements.define('my-form', MyForm);
export { MyForm }
Note the web component attempts to set the value of various inputs on first render. Thereafter, it attempts to set them again using setInterval. setInterval is used solely to show how the web component is attempting to update the template.
In the case of the select, an option cannot be set programatically. And in the case of each of the other input elements, once selected in the UI cannot be updated programatically.
I don't think it's a bug in Lit, though you're using it in quite a unique way.
In the case of your <select> the problem is that you're setting c.selected but then checking c.selcted in each <option>.

Can I autofocus on the first field in a redux-form created using a loop?

I have created a React component using redux-form and am looking to autoFocus on the first field. The fields are created by looping through an object and creating a field for each item in that object. When I use autoFocus in the JSX is autoFocuses on the last field in the form (which makes sense).
Does anyone know how I can autoFocus on the first field in the form?
Here is my component:
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
class BalanceForm extends Component {
constructor(props) {
super(props);
this.submitForm = this.submitForm.bind(this);
this.cancel = this.cancel.bind(this);
}
cancel() {
//not relevant
}
submitForm(e, values) {
//not relevant
}
render() {
return (
<div>
{this.props.balanceFormVisible &&
<div className="modal-background">
<div className="modal">
<form onSubmit={this.submitForm}>
{Object.keys(this.props.accounts).map((key) => {
return (
this.props.accounts[key].visible &&
<div key={this.props.accounts[key].name}>
<label className="form-label" htmlFor={this.props.accounts[key].name}>
{this.props.accounts[key].display}
</label>
<Field
name={this.props.accounts[key].name}
component="input"
type="number"
placeholder=""
autoFocus
/>
</div>
)
})}
<button type="submit">Submit</button>
<button onClick={ this.cancel } className="cancelbtn" >Cancel</button>
</form>
</div>
</div>
}
</div>
);
}
}
BalanceForm = reduxForm({form: 'balance'})(BalanceForm)
export default BalanceForm;
Thanks in advance :)
Solution to this was to conditionally render the form field. Thanks to Alexander Borodin for the inspiration...
{Object.keys(this.props.accounts).map((key, i) => {
console.log(key, i)
return (
this.props.accounts[key].visible &&
<div key={this.props.accounts[key].name}>
<label className="form-label" htmlFor={this.props.accounts[key].name}>
{this.props.accounts[key].display}
</label>
{(i === 0) ? (
<Field
name={this.props.accounts[key].name}
component="input"
type="number"
placeholder=""
autoFocus
/>
) : (
<Field
name={this.props.accounts[key].name}
component="input"
type="number"
placeholder=""
/>
)}
</div>
)
})}
Some of these either didn't compile or are a bit verbose. This worked for me:
{Object.keys(item)
.map((key, i) =>
<div className={`form-row ${key}`} key={key}>
<label>{key}</label>
<input value={item[key]}
type='text'
onChange={e => {
this._updateValue(key, e.target.value);
}}
autoFocus={i === 0}
/>
</div>
)}
Alternatively, if you hook in to ComponentDidMount, you can ask the DOM to focus the first existing field within the form.
Add a ref to the form
<form onSubmit={this.submitForm} ref='form'>
Use the ref to focus the element after mounting
componentDidMount() {
const firstInput = this.refs.form.querySelector('input')[0];
firstInput && firstInput.focus();
}
Try this:
{Object.keys(this.props.accounts).map((key, i) => {
return (
this.props.accounts[key].visible &&
<div key={this.props.accounts[key].name}>
<label className="form-label" htmlFor={this.props.accounts[key].name}>
{this.props.accounts[key].display}
</label>
<Field
name={this.props.accounts[key].name}
component="input"
type="number"
placeholder=""
{ (i === 0) ? 'autoFocus': ''}
/>
</div>
)
})}
I ran into an issue where autoFocus wasn't properly passing down from Field to the underlying component. Had to explicitly pass autoFocus in the props prop for Field.

React updating state in two input fields from form submission

I am trying to make a simple contact form using React. Eventually I will send the data collected from the state to a database, but for right now I am trying to just get it to console log the correct values.
Right now, the email field overrides the name field and when I console log both states, name shows up and email is undefined. Here is my React Component
import React, { Component, PropTypes } from 'react';
import ContactData from '../data/ContactData.js';
class FormContact extends Component {
constructor(props) {
super(props)
this.state = {
name: '',
email: '',
textArea: ''
}
}
handleChange(event) {
event.preventDefault();
this.setState({
name: event.target.value,
email: event.target.email
})
}
handleSubmit(event) {
event.preventDefault();
console.log(this.state.name + ' ' + this.state.email);
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<label> Name:
<input type="text" placeholder="Name" value={this.state.name} onChange={this.handleChange.bind(this)} />
</label><br />
<label> Email:
<input type="text" placeholder="Email" value={this.state.email} onChange={this.handleChange.bind(this)}/>
</label><br />
<input className="btn btn-primary" type="submit" value="Submit" />
</form>
)
}
}
FormContact.PropTypes = {
subName: PropTypes.string,
subEmail: PropTypes.string
}
FormContact.defaultProps = {
subName: 'Sam',
subEmail: ''
}
class Contact extends Component {
render() {
return (
<div>
<h1>CONTACT PAGE</h1>
<FormContact />
</div>
)
}
}
export default Contact;
If I understand what you want, you could do it as follows :
Add an empty object in your state for the form values
formValues: {}
Add the name attribute to your fields
<input name="name" .... />
<input name="email" .... />
then depending on that name update your state in handleChange function
let formValues = this.state.formValues;
let name = event.target.name; // Field name
let value = event.target.value; // Field value
formValues[name] = value;
this.setState({formValues})
And if the values go one level deeper, you could use
value={this.state.formValues["name"]} instead of value={this.state.name} - where name is the value of the name attribute of your input field
Thus, everything together should be as follows :
class Test extends React.Component {
constructor(props) {
super(props)
this.state = {
formValues: {}
}
}
handleChange(event) {
event.preventDefault();
let formValues = this.state.formValues;
let name = event.target.name;
let value = event.target.value;
formValues[name] = value;
this.setState({formValues})
}
handleSubmit(event) {
event.preventDefault();
console.log(this.state.formValues);
}
render(){
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<label> Name:
<input type="text" name="name" placeholder="Name" value={this.state.formValues["name"]} onChange={this.handleChange.bind(this)} />
</label><br />
<label> Email:
<input type="text" name="email" placeholder="Email" value={this.state.formValues["email"]} onChange={this.handleChange.bind(this)}/>
</label><br />
<input className="btn btn-primary" type="submit" value="Submit" />
</form>
)
}
}
React.render(<Test />, document.getElementById('container'));
Here is fiddle.
Hope this helps.
The reference to event.target.email does not exist on the event element. The value of a text input from an inline-event handler would be event.target.value for both email and name. The quick solution is to create a separate handler for each input:
handleChangeName(event) {
event.preventDefault();
this.setState({ name: event.target.value }); //<-- both use the same reference
} // to get the contextual value
handleChangeEmail(event) { // from the inputs v
event.preventDefault(); // |
this.setState({ email: event.target.value }); //<--------------------
}

Passing id from this.props.data.id to React ReduxForm

My ListItem component was passed data from another component using:
renderData() {
return (
<ListItem
key={data.id}
data={data} />
)
Now in my ListItem component I have access to the data using this.props.data. I want to pass the id, this.props.data.id to reduxForm or my handleFormSubmit function.
I tried adding a hidden input field to pass the id which didn't work?
handleFormSubmit({ id, date }) {
this.props.post( { id, date })
}
And in my render method.....
render() {
const { handleSubmit, fields: { id, date }} = this.props;
return(
<div>
<form className="form-inline" onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
<input {...id} type="hidden" name="id" value={this.props.data.id} />
<select className="form-control" {...date} >
<option value="Monday">Monday</option>
<option value="Tuesday">Tuesday</option>
<option value="Wednesday">Wednesday</option>
<option value="Thursday">Thursday</option>
<option value="Friday">Friday</option>
<option value="Saturday">Saturday</option>
<option value="Sunday">Sunday</option>
</select>
</form>
</div>
)
}
export default reduxForm({
form: 'day',
fields: ['id', 'date']
})
There must be another way to add the data into my handleFormSubmit method that I'm not seeing?
Figured it out..simply had to send the id in my handleFormSubmit helper function and remove id from the form.
handleFormSubmit({ date }) {
const id = this.props.data.id;
this.props.post( { id, date })
}