I am creating a dynamically generated form which reads a file from a template. I have an array of "questions" which are mapped onto a react-hook-form. The defaultValues I am using is an array of objects. The issue I am having is using react-select inside of a react-hook-form controller because it does not set the form state if it is an array. In my testing it works without issue if default values is not an array. Please see my MRE code example
const questions = [
{
value: '',
}];
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
export default function Form()
{
const methods = useForm({
defaultValues: questions,
});
const onSubmit = (data: any) => console.log(data);
return (
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Controller
name="0.value"
control={methods.control}
render={({ field }) => (
<Select
{...field}
value={options.find((c) => c.value === field.value)}
options={options}
onChange={(e) => field.onChange(e?.value)}
/>
)}
/>
<button type="submit">Submit</button>
</form>
);
}
What would be the best way to get react-hook-form controllers components like react-select to work with an array of defaultValues
The issue you are having is due to your default values being an array instead of an object. You should just wrap your questions in an object with a key of your choice. Please see example.
const questions = [
{
value: '',
}];
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
export default function Form()
{
const methods = useForm({
defaultValues: {questions},
});
const onSubmit = (data: any) => console.log(data);
return (
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Controller
name="questions.0.value"
control={methods.control}
render={({ field }) => (
<Select
{...field}
value={options.find((c) => c.value === field.value)}
options={options}
onChange={(e) => field.onChange(e?.value)}
/>
)}
/>
<button type="submit">Submit</button>
</form>
);
Related
I have a nested array of objects, each object have a nested options array like this.
const [formFields, setFormFields ] = useState({
formTitle: '',
fields: [
{name: 'country', val: '', type: 'radio', options: ['Japan', 'Korea', 'usa'] },
{name: 'state', val: '', type: 'select', options: ['texas', 'florida']},
{name: 'location', val: '', type: 'text', options: []},
]})
Each of the items in the nested options array is supposed to be a value in a textInput which is editable.
I want to be able to add/remove/edit these values inside the textInput with a button click.
Please how will I be able to achieve this?
my code
<Containter>
{formFields.fields.map((field, index) => (
<View key={index}>
<View>
<TextInput
onChangeText={(value ) => {
onChange({name: field.name, value });
}}
value={field.name}
/>
</View>
{(field.type === 'select' || field.type === 'radio') && (
<>
{field.options.map((option) => (
<TextInput value={option}
onChangeText={(value ) => {
onChange({name: field.options, ...field.options, value });
}}
/>
<Text onPress={removeOption}>X</Text>
))}
<Button title="add option" />
</>
)
}
<IconButton
icon="delete"
onPress={handleRemoveField}
/>
</View>
))}
<Button
onPress={handleAddField}
title="Add"
/>
</Containter>
Add & remove implementation:
onAdd (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) {
const options = [...field.options,value]
return {...field, options}
}
return field
})
setFormFields(
{
...formFields,
fields
}
)
}
onRemove (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) {
const options = field.options.filter((item) => item != value)
return {...field, options}
}
return field
})
setFormFields(
{
...formFields,
fields
}
)
}
// in constructor
this.onChange = this.onChange.bind(this)
// in class
onChange (index,value) {
this.setState(state => {
const fields = state.fields.map((field,i) => {
if (i==index) field.val = value
return field
})
return {
...state,
fields
}
})
}
// in component
onChangeText( (e) => onChange(index, e.target.value) )
For value changing:
onChange (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) field.val = value
return field
})
setFormFields({
...formFields,
fields
})
}
...
// somewhere in input element
<TextInput ... onChangeText={(e) => onChange(index,e.target.value)} .. />
I'm trying to create a custom hook that'll can be used in any form but i have a problem which i am not sure of what it is. whenever i start typing into the fields it loses focus, to type into it again you'd have to click into it and it goes on like that. this isn't good and i would appreciate it if anyone can help me solve this error.
useForm.js
import { useState } from "react";
const useForm = () => {
const [values, setValues] = useState({
// contact form
});
const [errors, seterrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value
});
// console.log(name, value);
};
const validate = (data) => {};
const handleSubmit = (e) => {
e.preventDefault();
// console.log(e);
};
// console.log(values);
return {
handleChange,
values,
handleSubmit
};
};
export default useForm;
Form.js
import InputGroup from "./InputGroup";
import useForm from "./useForm";
const Form = () => {
const fields = [
{
label: "First Name",
className: "input-group",
name: "firstName",
placeholder: "John",
type: "text"
},
{
label: "Last Name",
className: "input-group",
name: "lastName",
placeholder: "Doe",
type: "text"
},
{
label: "Email",
className: "input-group",
name: "email",
placeholder: "JohnDoe#example.com",
type: "email"
},
{
label: "Phone Number",
className: "input-group",
name: "Phone",
placeholder: "+234 (0)81 234 5678",
type: "text"
// pattern: "/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-s./0-9]*$/g"
},
{
label: "Comments",
className: "input-group",
name: "comment",
placeholder: "Message",
type: "textarea"
}
];
const { values, handleChange, handleSubmit } = useForm();
//funtion to generate id based on fields lenght
const id = () => {
const head = Date.now.toString(36);
const tail = Math.random().toString(36).substr(2);
return head + tail;
};
console.log(id);
return (
<form onSubmit={handleSubmit}>
{fields.map((field) => (
<InputGroup
key={id}
value={values}
onChange={handleChange}
{...field}
/>
))}
<button>submit</button>
</form>
);
};
export default Form;
Input
const Input = ({ ...props }) => {
return <input {...props} />;
};
export default Input;
You are not using your id function properly, you are only referring to it, not invoking it, change -
<InputGroup
key={id}
to
<InputGroup
key={id()}
or better still, a better name, using a verb would have highlighted the problem easier, as it now looks like a function and not a variable
<InputGroup
key={buildId()}
or whatever, but better still, you should use a static unique value for the key (i.e unique but would not change between renders, so using timestamps is a bad idea), so you could add a unique id to each field and use it -
const fields = [
{
id: <some unique ID>, <!---- here
label: "First Name",
className: "input-group",
name: "firstName",
placeholder: "John",
type: "text"
}
...
]
...
<InputGroup
key={field.id}
Below is the code where I am trying to select value from dropdown.
handleChange is not working, when I select the value from dropdown it's not getting updated with selected value from dropdown. It's getting vanished(blank).
Dropdown values are getting populated when I select it's not catching the new selected value.
Can someone help me on this like what I am missing here?
export const FormikSelectField = ({ label, ...props }) => {
const [field, meta] = useField(props);
const [isFocused, setOnFocus] = useState(false);
const handleChange = (value) => {
// this is going to call setFieldValue and manually update values.topcis
console.log('Value in handlechange..', value ,'and', props.name);
props.onChange(props.name, value.value);
};
const handleFocus = () => {
setOnFocus(true);
};
const handleBlur = (e) => {
// this is going to call setFieldTouched and manually update touched.topcis
setOnFocus(false);
props.onBlur(props.name, true);
field.onBlur(e);
};
return (
<>
<label htmlFor={props.labelName}>{props.labelName} </label>
<Select
id={props.labelName}
options={props.options}
isMulti={false}
onChange={handleChange}
onBlur={(e) => handleBlur(e)}
placeholder='Select an option'
onFocus={handleFocus}
value={props.value}
/>
{meta.touched && meta.error && !isFocused ? (
<div className="error">{meta.error}</div>
) : null}
</>
);
};
formikInitialValues = () => {
return {
Name: [{
title: '',
value: '',
}]
};
};
YupValidationSchema = () => {
return Yup.object({
Name: Yup.array()
.of(
Yup.object().shape({
title: Yup.string().required(),
value: Yup.string().required(),
})
)
.required("Please select an option")
.nullable()
});
};
<FormikSelectField
value={this.state.selectNameOption}
onChange={this.handleNameChange}
onBlur={this.handleNameChangeBlur}
error={formik.errors.Name}
options={this.state.NameOptions}
touched={formik.touched.Name}
name="Name"
labelName="Name"
/>
You should avoid mixing the state when using Formik. Formik will take care of the state for you.
import { Formik, Form, useField, ErrorMessage } from "formik";
import * as Yup from "yup";
import Select from "react-select";
const iceCreamOptions = [
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" }
];
const FormSelect = ({ name, options }) => {
const [field, meta, helpers] = useField(name);
return (
<>
<Select
name={name}
value={field.value}
onChange={(value) => helpers.setValue(value)}
options={options}
onBlur={() => helpers.setTouched(true)}
/>
<ErrorMessage name={name} />
</>
);
};
const initialValues = {
icecream: null
};
const validationSchema = Yup.object().shape({
icecream: Yup.object()
.shape({
value: Yup.string(),
label: Yup.string()
})
.required("Please select a value")
.nullable()
});
export default function App() {
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => console.log(values)}
validationSchema={validationSchema}
>
{(props) => {
return (
<Form>
<pre>{JSON.stringify(props, undefined, 2)}</pre>
<FormSelect name="icecream" options={iceCreamOptions} />
</Form>
);
}}
</Formik>
);
}
Example Working Sandbox
Write this code
onChange={(e:React.ChangeEvent<HTMLInputElement>)=> setFieldValue("title",e.target.value)}
or
onChange={(e)=> setFieldValue("title",e.target.value)}
I have a very simple state that is an array of objects. The second object has an array of objects inside that. I am able to change the basic state using an onChange handler that works well. However when trying to access the nested data, and change it I have problems.
The display first of all is giving me object obJect. I usually would map this to get it working but the value is going inside a value property.
import React, { useState } from "react";
import "./style.css";
const App = () => {
const [data, setData] = useState([
{ firstName: "Elon", lastName: "Musk" },
{
firstName: "Jeff",
lastName: "Bezos",
additionDetails: [{ worth: "Lots" }, { company: "Amazon" }],
},
]);
const handleChange = (e, i) => {
const clonedData = [...data];
clonedData[i][e.target.name] = e.target.value;
setData(clonedData);
};
console.log(Object.values(data[1].additionDetails[1]))
return (
<>
{data.map((x, i) => {
return [
<input
type="text"
name="firstName"
value={x.firstName}
onChange={(e) => handleChange(e, i)}
/>,
<input
type="text"
name="Worth"
value={x.additionDetails}
onChange={(e) => handleChange(e, i)}
/>,
<br></br>,
];
})}
</>
);
};
export default App;
Try this snippet. Since the additionDetails is an array of items, use the map and have the input field for each item.
Update the handleChange accordingly, based on the nested level. (when arg j exist means, it is additionalDetails)
const App = () => {
const [data, setData] = React.useState([
{ firstName: "Elon", lastName: "Musk" },
{
firstName: "Jeff",
lastName: "Bezos",
additionDetails: [{ worth: "Lots" }, { company: "Amazon" }],
},
]);
const handleChange = (e, i, j) => {
const clonedData = [...data];
if (j !== undefined) {
clonedData[i]["additionDetails"][j][e.target.name] = e.target.value;
} else {
clonedData[i][e.target.name] = e.target.value;
}
setData(clonedData);
};
console.log(Object.values(data[1].additionDetails[1]));
return (
<div>
{data.map((x, i) => {
return [
<input
type="text"
name="firstName"
value={x.firstName}
onChange={(e) => handleChange(e, i)}
/>,
x.additionDetails &&
x.additionDetails.map((obj, j) => (
<input
type="text"
name={Object.keys(obj)[0]}
value={Object.values(obj)[0]}
onChange={(e) => handleChange(e, i, j)}
/>
)),
<br></br>,
];
})}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("app"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="app"> Hello </div>
I have a dynamic form as a functional component which is generated via a class based component. I want to make reset button which clears the input field values and sets the state to null array.
Full code is available here:
https://codesandbox.io/s/beautiful-archimedes-o1ygt
I want to make a reset button, clearing all the input values and initializing the Itemvalues array to null.
Even if I set the values to null, it doesn't clear the input field.
However, the problem I'm facing is that since, it is a dynamic form and a functional component it doesn't have a predefined state for each individual form field making it difficult to set value to null.
Can someone please help, I'm stuck on this from a long time
Here's a codesandbox to show you how to reset the items: https://codesandbox.io/s/romantic-heisenberg-93qi7
I also left a note for you on how to get this to work with your API data, see the comment inside onChangeText()
The problem is that the inputs are not controlled by state as you have deduced. We should create an updated object for each item from your API, giving it a value prop.
index.js
import React from "react";
import ReactDOM from "react-dom";
import Cart from "./Cart";
import "./styles.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
Items: [],
itemvalues: [{}]
};
this.onChangeText = this.onChangeText.bind(this);
this.getItems = this.getItems.bind(this);
this.handleReset = this.handleReset.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.findFieldIndex = this.findFieldIndex.bind(this);
this.trimText = this.trimText.bind(this);
}
getItems = () => {
/*if the data is coming from an API, store it in an array then .map() over it.
we can add a value prop to the object like:
so you can do something like:
const newItems = [...apiData].map((item) => {
return {
...item,
value: ""
}
})
this.setState({
Items: newItems
})
*/
this.setState({
Items: [
{
name: "item1",
description: "item1",
group: "groupA",
dtype: "str",
value: ""
},
{
name: "item2",
description: "item2",
group: "groupA",
dtype: "str",
value: ""
},
{
name: "item3",
description: "item3",
group: "groupB",
dtype: "str",
value: ""
},
{
name: "item4",
description: "item4",
group: "groupB",
dtype: "str",
value: ""
}
]
});
};
onChangeText = e => {
const updatedItems = [...this.state.Items].map(item => {
if (item.name === e.target.name) {
return {
...item,
value: e.target.value
};
} else {
return item;
}
});
const updatedItemValues = [...updatedItems].reduce((obj, curr) => {
if (!obj[curr.group]) {
obj[curr.group] = [];
}
obj[curr.group] = [...obj[curr.group], { [curr.name]: curr.value }];
return obj;
}, {});
this.setState({
...this.state,
Items: updatedItems,
itemvalues: updatedItemValues
});
};
findFieldIndex = (array, name) => {
return array.findIndex(item => item[name] !== undefined);
};
trimText(str) {
return str.trim();
}
handleReset = () => {
const resetedItems = [...this.state.Items].map(item => {
return {
...item,
value: ""
};
});
this.setState(
{
...this.state,
Items: resetedItems,
itemvalues: []
},
() => console.log(this.state)
);
};
handleSubmit = () => {
console.log(this.state.itemvalues);
};
render() {
return (
<div>
{
<Cart
Items={this.state.Items}
getItems={this.getItems}
handleSubmit={this.handleSubmit}
handleReset={this.handleReset}
onChangeText={this.onChangeText}
/>
}
</div>
);
}
}
Cart.js
import React, { useEffect } from "react";
import Form from "./Form";
const Cart = props => {
useEffect(() => {
props.getItems(props.Items);
}, []);
return (
<div>
<Form Items={props.Items} onChangeText={props.onChangeText} />
<button onClick={props.handleSubmit}>Submit</button>
<button onClick={props.handleReset}>Reset</button>
</div>
);
};
export default Cart;
The Cart component can remain mostly the same, we do not need to pass in props.items to useEffect() dependency.
Form.js
import React from "react";
const Form = props => {
return (
<div>
{props.Items.map(item => {
return (
<input
name={item.name}
placeholder={item.description}
data-type={item.dtype}
data-group={item.group}
onChange={e => props.onChangeText(e)}
value={item.value}
/>
);
})}
</div>
);
};
export default Form;
Now in Form component, we provide each input a value prop that is connected to the item our upper-most parent component-state.
That's pretty much all you need to reset the values.
See if that works for you:
Working example on CodeSandbox
Since you were already using hooks in part of your code, I've converted your class into a functional component using hooks (my advice: learn hooks and forget about class components).
I've added a value property to your INITIAL_STATE so it will keep the input value for each inputItem.
Full CODE:
index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import FormV2 from "./FormV2";
import "./styles.css";
function App() {
const INITIAL_STATE = [
{
name: "item1",
description: "item1",
group: "groupA",
dtype: "str",
value: "" // ADDED VALUE PROPERTY TO KEEP THE INPUT VALUE
},
{
name: "item2",
description: "item2",
group: "groupA",
dtype: "str",
value: ""
},
{
name: "item3",
description: "item3",
group: "groupB",
dtype: "str",
value: ""
},
{
name: "item4",
description: "item4",
group: "groupB",
dtype: "str",
value: ""
}
];
const [inputItems, setInputItems] = useState(INITIAL_STATE);
function handleChange(event, index) {
const newValue = event.target.value;
setInputItems(prevState => {
const aux = Array.from(prevState);
aux[index].value = newValue;
return aux;
});
}
function handleReset() {
console.log("Reseting Form to INITIAL_STATE ...");
setInputItems(INITIAL_STATE);
}
function handleSubmit() {
inputItems.forEach(item =>
console.log(
"I will submit input: " + item.name + ", which value is: " + item.value
)
);
}
return (
<FormV2
handleSubmit={handleSubmit}
handleReset={handleReset}
handleChange={handleChange}
inputItems={inputItems}
/>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
FormV2.js
import React from "react";
function FormV2(props) {
const formInputItems = props.inputItems.map((item, index) => (
<div key={item.name}>
{item.name + ": "}
<input
type="text"
data-type={item.dtype}
data-group={item.group}
placeholder={item.description}
value={item.value}
onChange={event => props.handleChange(event, index)}
/>
</div>
));
return (
<React.Fragment>
<form>{formInputItems}</form>
<button onClick={props.handleSubmit}>Submit</button>
<button onClick={props.handleReset}>Reset</button>
<div>State: {JSON.stringify(props.inputItems)}</div>
</React.Fragment>
);
}
export default FormV2;
In order to control the values of the child components (Items) which I presume are input fields you need to be passing down their values from their parent component. So each of your items will have an item.value which is stored in the parent component's state.
That means that in the parent component you will be able to define a method which clears all of the item values it is storing in its state.
That will probably look something like
resetInputs = () => {
this.setState({
inputFields: this.state.inputFields.map(inputField => {
...inputField,
value: ''
}
})
}
Also you'll need to write what kind of tag you want for your code to work, like input.
So what you'll end up with for the code of the child component you shared is something like:
const Form = (props) => {
return (
<div>
{props.Items.map(item => (
<input
name={item.name}
value={item.value}
placeholder={item.description}
onChange={e => props.onChangeText(e)}
/>
)
)}
</div>
);
}
export default Form
You want to manage the state of unknown number N of items, one way to achieve it is by managing a single object which contains all states, for example, setValuesManager manages N inputs and clicking the button reset its state:
function TextAreaManager() {
const [valuesManager, setValuesManager] = useState([...items]);
return (
<Flexbox>
{valuesManager.map((value, i) => (
<TextBoxItem
key={i}
value={value}
onChange={e => {
valuesManager[i] = e.target.value;
setValuesManager([...valuesManager]);
}}
/>
))}
<PinkButton
onClick={() =>
setValuesManager([...Array(valuesManager.length).fill('')])
}
>
Reset All
</PinkButton>
</Flexbox>
);
}
Demo: