Serialize dynamic form into array of objects - javascript

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.

Related

Visit each child in props.children and trigger a function

I want to be able to visit the children <Textfield> of my form <Form> upon submit.
In each child hook object, I also want to trigger a certain function (eg., validate_field). Not sure if this possible in hooks? I do not want to use ref/useRef and forwardRef is a blurred concept to me yet (if that's of any help).
My scenario is the form has been submitted while the user did not touch/update any of the textfields so no errors were collected yet. Upon form submit, I want each child to validate itself based on certain constraints.
I tried looking at useImperativeHandle too but looks like this will not work on props.children?
Updated working code in:
https://stackblitz.com/edit/react-ts-jfbetn
submit_form(evt){
props.children.map(child=>{
// hypothetical method i would like to trigger.
// this is what i want to achieve
child.validate_field() // this will return "is not a function" error
})
}
<Form onSubmit={(e)=>submit_form(e)}
<Textfield validations={['email']}>
<Textfield />
<Textfield />
</Form>
Form.js
function submit_form(event){
event.preventDefault();
if(props.onSubmit){
props.onSubmit()
}
}
export default function Form(props){
return (
<form onSubmit={(e)=>submit_form(e)}>
{props.children}
</form>
)
}
So the Textfield would look like this
…
const [value, setValue] = useState(null);
const [errors, setErrors) = useState([]);
function validate_field(){
let errors = []; // reset the error list
props.validations.map(validation => {
if(validation === 'email'){
if(!some_email_format_validator(value)){
errors.push('Invalid email format')
}
}
// other validations (eg., length, allowed characters, etc)
})
setErrors(errors)
}
export default function Textfield(props){
render (
<input onChange={(evt)=>setValue(evt.target.value)} />
{
errors.length > 0
? errors.map(error => {
return (
<span style={{color:'red'}}>{error}</span>
)
})
: null
}
)
}
I would recommend moving your validation logic up to the Form component and making your inputs controlled. This way you can manage the form state in the parent of the input fields and passing in their values and onChange function by mapping over your children with React.cloneElement.
I don't believe what you're trying to do will work because you are trying to map over the children prop which is not the same as mapping over say an array of instantiated child elements. That is to say they don't have state, so calling any method on them wouldn't be able to give you what you wanted.
You could use a complicated system of refs to keep the state in your child input elements, but I really don't recommend doing that as it would get hairy very fast and you can just solve the issue by moving state up to the parent.
simplified code with parent state:
const Form = ({ children }) => {
const [formState, setFormState] = useState(children.reduce((prev, curr) => ({ ...prev, [curr.inputId]: '' }), {}));
const validate = (inputValue, validator) => {}
const onSubmit = () => {
Object.entries(formState).forEach(([inputId, inputValue]) => {
validate(
inputValue,
children.filter(c => c.inputId === inputId)[0].validator
)
})
}
const setFieldValue = (value, inputId) => {
setFormState({ ...formState, [inputId]: value });
};
const childrenWithValues = children.map((child) =>
React.cloneElement(child, {
value: formState[child.inputId],
onChange: (e) => {
setFieldValue(e.target.value, child.inputId);
},
}),
);
return (
<form onSubmit={onSubmit}>
{...childrenWithValues}
</form>
)
};
const App = () =>
<Form>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="password" inputId="foo"/>
</Form>
I still don't love passing in the validator as a prop to the child, as pulling that out of filtered children is kinda jank. Might want to consider some sort of state management or pre-determined input list.

Managing multiple calls to the same Apollo mutation

So taking a look at the Apollo useMutation example in the docs https://www.apollographql.com/docs/react/data/mutations/#tracking-loading-and-error-states
function Todos() {
...
const [
updateTodo,
{ loading: mutationLoading, error: mutationError },
] = useMutation(UPDATE_TODO);
...
return data.todos.map(({ id, type }) => {
let input;
return (
<div key={id}>
<p>{type}</p>
<form
onSubmit={e => {
e.preventDefault();
updateTodo({ variables: { id, type: input.value } });
input.value = '';
}}
>
<input
ref={node => {
input = node;
}}
/>
<button type="submit">Update Todo</button>
</form>
{mutationLoading && <p>Loading...</p>}
{mutationError && <p>Error :( Please try again</p>}
</div>
);
});
}
This seems to have a major flaw (imo), updating any of the todos will show the loading state for every single todo, not just the one that has the pending mutation.
And this seems to stem from a larger problem: there's no way to track the state of multiple calls to the same mutation. So even if I did want to only show the loading state for the todos that were actually loading, there's no way to do that since we only have the concept of "is loading" not "is loading for todo X".
Besides manually tracking loading state outside of Apollo, the only decent solution I can see is splitting out a separate component, use that to render each Todo instead of having that code directly in the Todos component, and having those components each initialize their own mutation. I'm not sure if I think that's a good or bad design, but in either case it doesn't feel like I should have to change the structure of my components to accomplish this.
And this also extends to error handling. What if I update one todo, and then update another while the first update is in progress. If the first call errors, will that be visible at all in the data returned from useMutation? What about the second call?
Is there a native Apollo way to fix this? And if not, are there options for handling this that may be better than the ones I've mentioned?
Code Sandbox: https://codesandbox.io/s/v3mn68xxvy
Admittedly, the example in the docs should be rewritten to be much clearer. There's a number of other issues with it too.
The useQuery and useMutation hooks are only designed for tracking the loading, error and result state of a single operation at a time. The operation's variables might change, it might be refetched or appended onto using fetchMore, but ultimately, you're still just dealing with that one operation. You can't use a single hook to keep track of separate states of multiple operations. To do that, you need multiple hooks.
In the case of a form like this, if the input fields are known ahead of time, then you can just split the hook out into multiple ones within the same component:
const [updateA, { loading: loadingA, error: errorA }] = useMutation(YOUR_MUTATION)
const [updateB, { loading: loadingB, error: errorB }] = useMutation(YOUR_MUTATION)
const [updateC, { loading: loadingC, error: errorC }] = useMutation(YOUR_MUTATION)
If you're dealing with a variable number of fields, then we have to break out this logic into a separate because we can't declare hooks inside a loop. This is less of a limitation of the Apollo API and simply a side-effect of the magic behind hooks themselves.
const ToDo = ({ id, type }) => {
const [value, setValue] = useState('')
const options = { variables = { id, type: value } }
const const [updateTodo, { loading, error }] = useMutation(UPDATE_TODO, options)
const handleChange = event => setValue(event.target.value)
return (
<div>
<p>{type}</p>
<form onSubmit={updateTodo}>
<input
value={value}
onChange={handleChange}
/>
<button type="submit">Update Todo</button>
</form>
</div>
)
}
// back in our original component...
return data.todos.map(({ id, type }) => (
<Todo key={id} id={id} type={type] />
))

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})?

How to validate radio button inside reducer using the event object passed to the reducer?

I have the following reducer in my React app:
const initialState = {
genderRadio : false,
ageRadio : false
}
const reducer = ( state = initialState , action ) => {
switch(action.type) {
case "VALI_RADIO_INP":
console.log(action.payload.target);
return state
}
return state;
}
export default reducer;
action.payload is basically the event object that is passed to the reducer, like so from my component:
validateRadioInput : (e) => dispatch({ type: 'VALI_RADIO_INP' , payload : e })
What I would like to do in my reducer is check if the input element has been checked or not and update the state. How do I using the event object check if a element is checked or not checked?
NOTE::-
Before integrating redux I was checking if the checkbox is checked calling a method that resided right inside my component like so:
Array.from(document.getElementsByName('customer_gender')).some( (elem , idx) => { return elem.checked })
But of course I can't use this anymore; any suggestions on how I can validate the checkbox in my reducer using the event object?
First set attribute name to your checkbox element like so:
<input type="checkbox" name="genderRadio"/>
or:
<input type="checkbox" name="ageRadio"/>
And modify your code that set correct piece of state depending on the attribute name of checkbox element.
Example:
const reducer = ( state = initialState , action ) => {
switch(action.type) {
case "VALI_RADIO_INP":
console.log(action.payload.target);
return { ...state, [payload.target.name]: payload.target.checked };
}
return state;
}
export default reducer;
How do i using the event object check if a element is checked or not checked ?
You shouldn't do that. Your Redux reducers shouldn't be coupled to the DOM if you can help it. Though, it is possible that you can traverse the DOM from the event's target, if you're using React you shouldn't be depending on the DOM at all.
One way to do it is to get your component to have a data representation of the view. This could be your React component's state. Or, you could grab it from the DOM if you're not using React with something like this:
validateRadioInput: (e) => {
const checkedArr = Array.from(document.getElementsByName('customer_gender'))
.map(elem => elem.checked);
return dispatch({
type: 'VALI_RADIO_INP',
payload: checkedArr,
});
}
// reducer
const reducer = ( state = initialState , action ) => {
switch(action.type) {
case "VALI_RADIO_INP":
const valid = action.payload.some(checked => checked);
return { ...state, valid };
}
return state;
}
Ultimately, though, I don't agree with the concept of doing form validation sort of logic in Redux -- just do it in the component and then dispatch some action to Redux if it's valid. Redux shouldn't have to deal with every nitty-gritty state in your application; just state that affects multiple components in potentially complex ways.
Also, note that you may be trying to fix an HTML problem in JS since you can only check one radio button at a time, anyway, and you could just make the HTML have a required field. See HTML5: How to use the "required" attribute with a "radio" input field

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