Create input component and control multiple form states in React + Redux - javascript

My project has 2 separated forms, 'user' and 'movies'. I have created a single input component which I want to use in all 3 forms. This isn't the complex thing to do but I am wondering what the right approach is to save the data on every keystroke or blur action to the right redux store state.
So I have 2 reducers and 2 forms, 'user' and 'movies'. The user form contains just some user and user meta data. The movies form will be a 'collection' of movies the user has seen.
I have created this text field component
import React from 'react';
class TextField extends React.Component {
constructor(props) {
super(props);
this.state = {
name: this.props.name,
value: this.props.value
}
}
onChange = (e) => {
const {
value
} = e.target
this.setState({
value: value
});
}
onBlur = (e) => {
const {
value
} = e.target
this.setState({
value: value
});
}
render() {
return(
<input
type="text"
name={this.state.name}
value={this.state.value}
onBlur={(e) => this.onBlur(e)}
onChange={(e) => this.onChange(e)}
/>
)
}
}
export default TextField;
And in the form component
<TextField
name={`${this.state.movies}[${index}].name`}
value={item.name}
/>
No, I need the right approach to write the values by onChange or onBlur directly to the right 'user' or 'movies' sate. e.g. movies[1].name should be append to the movies state via reducer:
movies : [{
name: 'movie #1'
},
name: 'movie #2'
}]
Should I pass down on every input field a function for onChange and onBlur via props to let the TextField component know how to handle and where to write the value to the store? e.g.
<TextField
name={`${this.state.movies}[${index}].name`}
value={item.name}
onBlur={(event) => console.log('onBlur action')}
onChange={(event) => console.log('onChange action')}
/>
I have worked with modules such as 'redux-form', but it feels heavy, not that stable. I would like to control all by myself as the 'movies' form is just a collection without user side validations.

Yup! Event propagation is the way to go. You definitely don't want to couple your TextField component with any redux or any other state management logic as it defeats the whole purpose of reusability. I would create handling functions in the Form component that would do that exact thing(e.g. handleOnBlurMovieInput, handleOnChangeUserInput) and pass those into the TextField as props respectively.

Related

rule validation not triggered on input field when value is changed from React state

I have an input text field that's required on a form. When I edit the field manually and set it to the empty string the validation rule triggers and the message does indeed show up. When I empty the field via React state (e.g. using a button), the validation rule is not triggered.
Sandbox is here. For completeness purposes, code is also given below:
import React from 'react';
import { Form, Input, Button } from 'antd';
class FormExample extends React.Component {
constructor(props) {
super(props);
this.state = {fields: [{name: 'field-a', value: 42}]}
}
emptyField = () => {
this.setState({fields:[{name: 'field-a', value: null}]});
}
render() {
const rules = [{ required: true, message: 'the field is required!' }];
return (
<>
<Button onClick={this.emptyField}>emptyField</Button>
<Form
name="global_state"
layout="inline"
fields={this.state.fields}
>
<Form.Item
label={'The answer is: '}
name={'field-a'}
rules={rules}>
<Input disabled={false}/>
</Form.Item>
</Form>
</>
);
}
}
export default FormExample;
Short answer:
You need to add a validateFields to your antd FormInstance
E.g.: this.formRef.current.validateFields()
Long answer:
As described in the antd FormItem docs, you should not use setState instead use setFieldsValue.
You shouldn't call setState manually, please use form.setFieldsValue
to change value programmatically.
An example how to use Form method with Class Component can be found in this CodeSandbox.
E.g.:
emptyField = () => {
this.formRef.current.setFieldsValue({"field-a": null})
this.formRef.current.validateFields()
};
Additional you can use resetFields instead of setting the value to null.
E.g.:
emptyField = () => {
this.formRef.current.resetFields()
this.formRef.current.validateFields()
};
Here is the complete example:
import React from "react";
import { Form, Input, Button } from "antd";
class FormExample extends React.Component {
constructor(props) {
super(props);
this.state = { fields: [{ name: "field-a", value: 42 }] };
}
formRef = React.createRef();
emptyField = () => {
// this.formRef.current.setFieldsValue({"field-a": null})
this.formRef.current.resetFields()
this.formRef.current.validateFields()
};
render() {
const rules = [{ required: true, message: "the field is required!" }];
return (
<>
<Button onClick={this.emptyField}>emptyField</Button>
<Form ref={this.formRef} name="global_state" layout="inline" fields={this.state.fields}>
<Form.Item label={"The answer is: "} name={"field-a"} rules={rules}>
<Input disabled={false} />
</Form.Item>
</Form>
</>
);
}
}
export default FormExample;
This answer by zerocewl presents an imperative approach which I didn't want to follow as I am using Redux to store the state of my application (even though for this short, self-contained example, to keep things simple, I am using local component state). However, I used the validateFields() API presented in his answer, initially as follows:
componentDidUpdate = (prevProps: any, prevState: any) => {
this.formRef.current.validateFields();
}
For some reason, the above code wasn't working. The below code however does work and that's all that was necessary. I.e. field values are propagated directly from the Redux store - there are no resetFields or setFieldsValue calls.
componentDidUpdate = (prevProps: any, prevState: any) => {
setTimeout(()=>{this.formRef.current.validateFields()}, 0);
}
I cannot explain why the above code works while the fully synchronous version does not.

Updating local state through React Bootstrap DropdownButton and dropdown items rendered from an array

I am building an application that gets a list of job descriptions from backend and takes user input to select one of them along with several parameters in order to make a second backend request. Since there will be a few more components involved, I have decided to use Redux for managing global state, and that appears to cause complications in my code.
My DropdownButton component uses handleJobSelect to modify local state and thereby show current selectedJobValue as the value of a FormControl component, and all is apparently well and good if my items are rendered directly (e.g. "Good item" in the code below). If I use a fetched list to map my items, however, selecting one of them will somehow cause the entire website to reload, losing all state changes and resetting both the selectedJobValue and a FormControl value property.
class Settings extends Component {
constructor() {
super();
this.state = {
descriptions: [],
selectedJobValue: 'None'
};
this.handleJobSelect = this.handleJobSelect.bind(this);
}
componentDidMount() {
this.props.fetchData('http://127.0.0.1:5000/api/1.0/descriptions');
}
handleJobSelect = evt => {
const someVal = evt;
console.log(someVal);
console.log(this.state);
this.setState({ selectedJobValue: someVal });
console.log(this.state);
};
render() {
const { selectedJobValue } = this.state;
return (
<InputGroup classname="mb-3">
<DropdownButton
as={InputGroup.Prepend}
variant="outline-primary"
title="Description"
id="input-group-dropdown-1"
onSelect={this.handleJobSelect}
>
{this.props.items.map(item => (
<Dropdown.Item href={item} key={item} eventkey={item}>
{item.toString()}
</Dropdown.Item>
))}
<Dropdown.Item href="#">Good item</Dropdown.Item>
</DropdownButton>
<FormControl
placeholder="Placeholder job description"
aria-label="Job description"
aria-describedby="basic-addon1"
value={selectedJobValue}
readOnly="True"
/>
<InputGroup.Append>
<Button variant="outline-primary">Submit</Button>
</InputGroup.Append>
</InputGroup>
const mapStateToProps = state => {
return {
items: state.items,
};
};
const mapDispatchToProps = dispatch => {
return {
fetchData: url => dispatch(itemsFetchData(url))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Settings);
I suspect that my attempt to use both local and global state is complicit in this issue, but this seems impossible to avoid given my plans for the application. Is there a way to resolve the problem without abandoning Redux?
After going through my code a few more times, I think that the problem is in my use of href and handleJobSelect() function. Since my description items represent relative file paths with slashes, they are read as valid hrefs that can call the page by themselves, thereby ruining everything. Is it possible to extract through my handleJobSelect() function different properties of the target dropdown item, for example, its text (that is, {item})?

Pass value from class method to a react component

Situation: I have a class with its own methods. This class is instantiated in a React component.
What I'm needing: During one of the methods in the class, it changes the value of an input (this.$el) using .val(), but I'm listening to changes to this input in the React.component via onChange. I need to pass the value I'm using to set the value of the input (via this.$el.val(value)) to the React component to change its state.
What I've tried: I've tried chaining .change() and trigger('change') to the val(value), but it doesn't have any affect.
So, I need to be able to access the value I'm using in .val(value) in my React component WHEN it is set in the class method. I thought about using a method and calling that method on componentWillUpdate, but the component doesn't update since setting the input value via val() doesn't trigger a change.
Any ideas?
Component code:
// Create a ref to manage blur/focus state on the search input
this.inputRef = React.createRef()
// Setup initial state
this.state = {
supersearchResults: [],
value: this.props.initialValue || ''
}
this.onInputChange = this.onInputChange.bind(this)
tag('input', {
autoComplete: 'off',
className: blockElement('search-input'),
onChange: this.onInputChange,
placeholder: 'Find people, collections and pages',
ref: this.inputRef,
type: 'text',
value: this.state.value
})
Class code:
this = class
this.$el = input
// What is happening:
// A user types in an input, suggestions display in a list, when you
// select a suggestion, it calls the below to change the input value
this.$el.val(complete)
this.$el.blur()
this.hide()
If I understand correctly, you want to be able to access the value of a html field. Then please take into consideration the following considerations. Use controlled inputs such that the
class ReactComponent extends...
constuctor (props) {
super();
this.state = { fieldValue: props.fieldValue || '' };
}
onFieldChange = (event) => {
this.setState('fieldValue': event.currentTarget.value)
}
render () {
return (
<div>
<input type="text"
value={this.state.fieldValue}
onChange={this.onFieldChange}
>
<div>
)
}
}
Now having this code, in case you need to use some external class to call some code, simply put it correctly in the lifecycle. But in order to reference the value use the components state. And in case you want to programmatically want to change the value, do the same update the value in the state. If I missed something let me know in the comments.
You need to keep the state in your class component. consider the following
class TextExample extends Component{
constructor(){
super(props);
this.state ={
username: null
}
this._handleChange = this._handleChange.bind(this);
}
_handleChange(e){
const { name, value } = e.target;
this.setState({ username: value}) // for single TextField
// if you want to reuse this _handleChange function for all the TextFields then you need to use the below commented code which updates state of current TextField
//this.setState({ [name]: value }) // it is similar like the JSON bracket notation
}
render(){
return(
<div>
<TextField
id="username"
label="Username"
name="username"
value={this.state.username}
onChange={this._handleChange} // it will call the _handleChange function on every keypress inside the TextField.
/>
</div>
)
}
}

Serialize dynamic form into array of objects

Here is my initial state in redux
grains: [{grainId: "", amount: "0.0"}],
Im trying to get my form to serialize to something similar but I cant find a way to have grains be the outer object. Im wondering if its even possible or if I'll have to recreate the object manually. Or just get the state from the store.
My form looks like this:
<select
name='grainId'
value={this.props.value}
>
...options
</select>
<input
type="text"
name='amount'
value={this.props.value}
/>
Then theres an add grain button which will add another object to the array in the state which will then render another section of the form on the page. Is there anyway to wrap each "Grain" section in a form element so that is serializes nicely?
If not can I just post the state from the store on form submit or is that bad practice?
Since you are already loading your initial form data from the redux state, I think it would make sense to have the form update the Redux state as it changes. With this approach every input chnage would trigger an update of the Redux state, which would then be passed down to the component (through mapStateToProps), and cause a re-render showing the new value. That way you can make sure, in your reducer, that the state always has the shape you prefer ( {grains: [{grainId: "", amount: "0.0"}, etc... ] ).
Like you hinted, that would mean that when you finally submit, you are basically submitting the Redux form state (or at least the props passed down from it).
That could look something like this (runnable JSFiddle demo here):
class App extends React.Component {
renderGrains() {
const {grains, changeGrain} = this.props;
return grains.map((grain, index) => (
<div key={ index }>
<input
type="text"
value={grain.grainId}
onChange={({target:{value}}) => changeGrain(index, 'grainId', value)}
/>
<input key={ grain.grainId + 'amount' }
type="number"
value={grain.amount}
onChange={({target:{value}}) => changeGrain(index, 'amount', value)}
/>
<br />
</div>
));
}
render() {
const {addEmptyGrain, grains} = this.props;
const onSubmit = (e) => {
e.preventDefault();
alert('submitting: \n' + JSON.stringify({grains}, null, 2));
};
return (
<form>
{ this.renderGrains() }
<button onClick={ (e) => { e.preventDefault(); addEmptyGrain();} }>Add grain</button><br />
<button onClick={onSubmit}>Submit</button>
</form>
);
}
}
where the reducer would look something like this:
const r1_initialState = { grains: [{grainId: "", amount: 0.0}] };
const reducer1 = (state = r1_initialState, action) => {
switch(action.type) {
case CHANGE_GRAIN:
const {index, key, newValue} = action;
const grainsCopy = [...state.grains];
grainsCopy[index][key] = newValue;
return {...state, grains: grainsCopy};
case ADD_EMPTY_GRAIN: {
return {...state, grains: [...state.grains, {grainId: '', amount: 0.0}]}
}
default:
return state;
}
};
If this seems like too much work, but you still want to keep the form data in the Redux state, there are libraries like redux-form, which handles the repetitive handling of onChange etc. for your forms.
Another option would be to load the initial state from Redux, but just use it to set the internal state of the component. Then handle all the form changes as a change to the component state. The same logic as the one in the example above, could be used for rendering the fields from an array.

How to properly manage forms in React/Redux app

I am following some tutorials about React/Redux.
The part that is not clear to me is input forms.
I mean how to handle changes and where to manage state
This is a very simple form with just one field.
export default class UserAdd extends React.Component {
static propTypes = {
onUserSubmit: React.PropTypes.func.isRequired
}
constructor (props, context) {
super(props, context);
this.state = {
name: this.props.name
};
}
render () {
return (
<form
onSubmit={e => {
e.preventDefault()
this.handleSubmit()
}}
>
<input
placeholder="Name"
onChange={this.handleChange.bind(this)}
/>
<input type="submit" value="Add" />
</form>
);
}
handleChange (e) {
this.setState({ name: e.target.value });
}
handleSubmit () {
this.props.onUserSubmit(this.state.name);
this.setState({ name: '' });
}
}
I feel this like breaking Redux philosophy, because a presentation component is updating the state, am I right?
This is the connected component to be coupled with the presentation component.
const mapDispatchToProps = (dispatch) => {
return {
onUserSubmit: (name) => {
dispatch(addUser(name))
}
}
}
const UserAddContainer = connect(
undefined,
mapDispatchToProps
)(UserAdd)
Is this the correct way to follow, or am i mixing things up?
Is correct to call setState in UserAdd component and updating state on every key pressed (handleChange) ?
Thank you
There is a nice library Redux Form for handling forms by updating global store in a Redux way. With it's help you shouldn't have to set up actions for each input, just the whole form and its state. Check it out.
The main principle of this library, consists in updating inputs value by dispatching redux actions, not using setState stuff. For every form in the app, there is a separate property in the global state. Every blur, onChange, submit events dispatches an action that mutates the state. Action creators are common for all the forms, no need to declare them for every form apart, just pass form id or name in payload to the reducer, so it could know which form`s property should be updated.
For example. There should be set a property form as a plain object in the app state. Each new form in the application, should store it's state in it. Let's give your form a name attribute, so it should serve us as the identificator.
render () {
return (
<form
name="AForm"
onSubmit={e => {
e.preventDefault()
this.handleSubmit()
}}
>
<input
name="name"
placeholder="Name"
onChange={this.handleChange.bind(this)}
/>
<input type="submit" value="Add" />
</form>
);
}
Since it has just one property Name, form state should now have a structure like:
form: {
AForm: {
Name: {
value: '',
error: ''
}
}
}
Also, there should be an action creator:
export function onFormFieldChange(field) {
return {
type: "redux-form/CHANGE"
field: field.name
value: field.value,
form: field.form
}
}
All needed data should be passed as the pay load so, the reducer will know now what form and what field to update.
Now, when the form component is being connected, this action creator should be set as a property:
import { onFormFieldChange } from `path-to-file-wit-actions`
const mapStateToProps = (state, ownProps) => {
const { AForm } = state.form
return {
name: AForm.name
}
}
const mapDispatchToProps = (dispatch) => {
return {
onChange: (e) => {
dispatch(onFormFieldChange({
field: 'name',
value: e.target.value,
form: 'AForm'
}))
},
onUserSubmit: (name) => {
dispatch(addUser(name))
}
}
}
const UserAddContainer = connect(
undefined,
mapDispatchToProps
)(UserAdd)
In the component, field value and onChange event handler should now be taken from props:
<input placeholder="Name" name="this.props.name.value" onChange={this.props.handleChange.bind(this)} />
So, form is being handled in a "Redux" way. On every key press, global state will be updated and input will be rerendered with it's new value. Similar thing should be done with other events, like onBLur, onFocus, onSubmit etc. Since it's a lot work to do, it's much more comfrotable to use Redux Form.
It's a very rough example. Nearly each line of code could be enhanced, hope you'll understand what was meant.
I usually store my form state inside a form component using this.setState() and only fire an action with the complete form object, which gets passed to some sort of POST ajax call.

Categories

Resources