I'm trying to create a form with Formik, which is essentially a series of check-boxes. Here is a simplified version of what I am doing,
I have some props looking like this:
"settings" : {
"email_addresses":
[
"a#b.com",
"c#b.com",
],
"alerts":
{
"SYSTEM": {
"UP": {
"enabled": true,
"states": ["UP"]
},
"DOWN": {
"enabled": false,
"states": ["DOWN"]
}
}
}
}
Here is the form
...
return (
<Formik
initialValues={this.props.settings}
onSubmit={(values) => {
console.log('values', values)
}}
>
{props => {
return (
<Form>
{
Object.keys(props.values.alerts).map((alert) => {
Object.keys(props.values.alerts[alert]).map((state) =>
<Field name={props.values.alerts[alert][state].enabled}>
{({ field, form, meta }) =>
<input
type="checkbox"
id={props.values.alerts[alert][state]}
checked={props.values.alerts[alert][state].enabled}
/>
}</Field>
)
}
)
}
</Form >
)
}}
</Formik >
)
...
When the page is rendered, check-boxes are correctly rendered and ticked/not ticked depending on the values of "enabled".
What is not working is the behaviour when clicking on the checkbox. Tick is not added/removed and values are not changed.
I worked on the issue following two hypotheses:
Formik checkbox does not need an onChange/onClick/etc. function to
handle this (as it proved to be the case for a couple of "text" fields in
the same form).
Formik checkbox needs a function. Which I tried to add both within the tag and in the render() function. But without success.
Any idea where the problem might be?
It turns out that the syntax I used, which I found in a number of examples, is unnecessarily complicated for what I needed to do.
Another issue was the way I was defining the 'path' to the values for 'name'.
It seems that as I have assigned props.settings to initialValues then I just need to indicate the relative path.
Using the following simple syntax, it worked immediately.
<Field name={`alerts[${alert}][${state}].enabled`} type="checkbox" />
Related
I'm really trying to wrap my head around why this is a design choice and how it's not more of an issue.
I have a form that has a number of inputs on it, and when I start typing in any of the fields the whole form is validated as per my validate function, causing every validation error message to be rendered onto my form.
The form isn't even doing anything magical either, I'm literally just taking in basic text input and validating if the values are empty or not. Yet when I do that, Formik thinks it's a great idea to validate EVERY field, including my pristine ones that are yet to be touched.
Can someone please explain to my how I can change this behaviour so that only the form currently firing onChange events is validated?
I've tried using the form's touched object to check which are yet to be touched and only validate the touched ones, but that produces more problems, especially when trying to submit the form.
validateOnChange and validateOnBlur don't help either, they just turn off the validation
Again, why is this the default behaviour. Who would ever want to validate their entire form at once based on onChange events?
I've even tried to do field-level validation using instead, and even that produces the same behaviour? I'm genuinely at a loss as to how anyone can produce working forms with this. Obviously people do, so if someone can explain how to do this on a field by field basis I'd very much appreciate it.
const formik = useFormik({
initialValues: {
positionTitle: "",
experienceRequired: ""
},
onSubmit: (values) => {
console.log("Form output: ", JSON.stringify(values));
console.log("Errors: ", formik.errors);
},
isInitialValid: false,
validate: validate
validateOnMount: false,
validateOnChange: false,
validateOnBlur: false
});
<FormikProvider value={formik}>
<form onSubmit={formik.handleSubmit} className="flex flex-col mt-4">
<Field
as="input"
validate={validateBasicRequired}
name="positionTitle"
onChange={formik.handleChange}
value={formik.values.positionTitle}
></Field>
{formik.errors.positionTitle &&
formik.errors.positionTitle === "Required" && (
<BasicValidationError message="Required field."></BasicValidationError>
)}
<Select
name="experienceRequired"
onChange={handleSelectOnChange}
options={getMultiSelectOptions(experienceValues)}
></Select>
{formik.errors.experienceRequired &&
formik.errors.experienceRequired === "Required" && (
<BasicValidationError message="Required field."></BasicValidationError>
)}
</form>
</FormikProvider>
This is the validation function I was using before field-level
const validate = (values: PostJobForm) => {
const errors: any = {
compensation: {},
};
if (!values.positionTitle) {
errors.positionTitle = "Required";
}
if (!values.experienceRequired) {
errors.experienceRequired = "Required";
}
return errors;
};
Formik has two options controlling when validation occurs: validateOnChange and validateOnBlur.
Problem with using validateOnChange - user will get errors as they start typing, say, email - because it is not valid when you just started to type first letters.
In case of validateOnBlur - let's say user typed in invalid email, left the field, returned and fixed to correct email - the error will be shown anyway until they leave the field, so this also does not work for me.
What I would like to achieve - first time validate a field on blur and starting that point - on change.
Every field should get this treatment individually.
I tried to find native ways to do it but couldn't find any, so I came up with this solution that I do not really like because it is not flawless:
TL;DR: I override standard onBlur and onChange coming from Field's props and a) onBlur - mark field as changed once (probably I could use touched here) b) onChange I check if field was touched once and if so - call validation.
0) Disable validation
<Formik
validateOnChange={false}
validateOnBlur={false}
...
1) Added changedFields to my state
public state: IState = {
changedFields: {}
};
2) Use special method that overrides default onBlur and onChange
<Field name="userName" render={(props: FieldProps<MyFormInterface>) => (
<input type="text" label={'Name'} {...formikCustomValidation(props, this)} />
)}/>
3) The method. Passing Field's props and the parent component where state with changedFields is located.
function formikCustomValidation({ field, form }: FieldProps, ownerCmp: Component<any, any>) {
return {
...field,
onBlur: e => {
if (field.value !== form.initialValues[field.name]) {
ownerCmp.setState({
changedFields: {
...ownerCmp.state.changedFields,
[field.name]: true
}
});
}
field.onBlur(e);
setTimeout(form.validateForm);
},
onChange: e => {
field.onChange(e);
if (ownerCmp.state.changedFields[field.name]) {
setTimeout(form.validateForm);
}
}
};
}
The question is - is there a way to do it easier?
The problem with this solution - if I Submit and untouched form and some errors come up - focus and fix value does not mark field as valid - because it is the first 'touch' and field is still in stat waiting for first blur state that supposed to make listen for 'changes'.
Anyway, maintaining this could be a pain, so looking for better solution.
Thanks!
I have a scenario where there will be two fields of each item. One field is a checkbox and another is dropdown but the point is to get a pair of data from this and I am mapping this based on the item(they have category too). And the dropdown depends on checkbox(when unchecked the dropdown is disabled and value is reset to none or zero)
I tried
<Field name={ `${item.label}` } component={MyCheckBoxComponent}>
<Field name={ `${item.value}` } component={MyDropDownComponent}>
what happens is each of them have unique name and I cant do anything when I need to update the values depending on the checkbox selections.
I have tried putting the two inputs in one Field but I can't get it to work. Help would be much appreciated
You need to use Redux Fields (https://redux-form.com/6.0.4/docs/api/fields.md/) not Redux Field.
You can create a separate component which wraps your check-box component and your drop-down component.
This is how you use it
<Fields
names={[
checkboxFieldName,
dropDownFieldName
]}
component={MyMultiFieldComponent}
anotherCustomProp="Some other information"
/>
And the props that you get in your MyMultiFieldComponent
{
checkboxFieldName: { input: { ... }, meta: { ... } },
dropDownFieldName: { input: { ... }, meta: { ... } },
anotherCustomProp: 'Some other information'
}
The input property has a onChange property (which is a method), you can call it to update the respective field value.
For example in onChange method of check-box
onChangeCheckbox (event) {
const checked = event.target.checked;
if (checked) {
// write your code when checkbox is selected
} else {
const { checkboxFieldName, dropDownFieldName } = props;
checkboxFieldName.input.onChange('Set it to something');
dropDownFieldName.input.onChange('set to zero or none');
}
}
This way you can update multiple field values at the same time.
Hope this helps.
Probably, this is what are you looking for - formValues decorator.
Just wrap your dropdown with this decorator and pass the name into it of your checkbox so you will have access inside of the MyDropDownComponent.
For example:
import { formValues } from 'redux-form';
<form ...>
<Field name="myCheckBox" component={MyCheckBoxComponent}>
<Field name="myDropdown" component={formValues('myCheckBox')(MyDropDownComponent)} />
</form>
So then myCheckBox value will be passed as a prop.
Performance note:
This decorator causes the component to render() every time one of the selected values changes.
Use this sparingly.
See more here - https://redux-form.com/7.3.0/docs/api/formvalues.md/
I have an array of checkbox rendered as:
<form>
{items.map(item => {
<Field component="input"
type="checkbox"
name=`items.{item.id}`
})}
</form>
This gives me values as an Array:
{
//index: 0, 1, ,…, 120, 121, … ,231, …
item: [undefinded,undefined,…,true,undefined,…,true,undefined,…]
}
Instead I would prefer an Object, (mainly to avoid a large array being created):
{
120: true,
131: true,
165: false
}
Is there a way to force redux-form output object when itemId is integer.
Note: If itemId is passed as string then redux-form does return an Array.
Similar post but solution does not work with redux-form 7.
I came across this while looking for a solution to the same problem.
What eventually worked for me is to initialize the object when passing the initial values to the form
const ItemsForm = ({ items }) => {
return <form>
{items.map(item => {
<Field component="input"
type="checkbox"
name={`items.${item.id}`} />
})}
</form>
}
export default reduxForm({
form: 'itemsForm'
})(ItemsForm)
Initialize the form like this:
<ItemsForm initialValues={{items: {}}} />
You could just use the filter function when you're mapping the form field from the state:
checkboxArray.filter(checkbox =>{
return checkbox; // if its undefined, it'll get dropped from the array
});
Associative arrays and objects are actually identical in Javascript, so no more work is needed, so having filtered out all of the undefined items in the array gets you the object you're looking for.
issue is that the field name is a number. try prefixing it.
instead of '102' >> 'myField_102'
I am having a very annoying issue with React and checkboxes. The application I am working with requires a list of checkboxes that represent settings that are persisted in the back-end. There is an option to restore the settings to their original state.
At first, I created a component that has an object like a map of settings. Each setting has a key and a boolean value. Hence:
{
bubbles: true,
gregory: false
}
Is to be represented as:
<input type="checkbox" value="bubbles" checked="checked" />
<input type="checkbox" value="gregory" />
Now, it seems React is ignorant about how a checkbox is supposed to work. I don't want to set the checkboxes' values, I want the "checked" property.
Yet, if I try something like this:
<input
type="checkbox"
value={setting}
checked={this.settings[setting]}
onChange={this.onChangeAction.bind(this)}
/>
I get this warning:
Warning: AwesomeComponent is changing an uncontrolled input of type checkbox to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: [some useless docs page I read several times to no avail]
So I decided to create another component to wrap each individual checkbox and I got this:
<input
type="checkbox"
checked={this.state.checked}
onChange={this.onChangeAction.bind(this)}
/>
Now the checked is a property present directly in my state.
This yields the same warning, so I tried using defaultChecked:
<input
type="checkbox"
defaultChecked={this.state.checked}
onChange={this.onChangeAction.bind(this)}
/>
Which makes the warning disappear, but now it is unable to reset the checked value to the default one. So I tried playing with the method componentWillReceiveProps, this way I am quite sure my state is correct, this.state.checked is correct and the component renders again.
And it does. But the checkbox remains as it was originally.
For now I left that ugly warning and I am using checked.
How do I fix this thing so the warning goes away?
I was thinking that perhaps there is a way to force-re-render the component, so it captures the new defaultChecked value and uses it. But I don't know how to do that. Perhaps suppress the warning only for this component? Is that possible? Perhaps there is something else that can be done?
The problem arises if you set the checked property of your checkbox to null or undefined.
Those are "falsy" values in JS. However, React treats a value of null as if the property was not set at all. Since the default state of a checkbox is unchecked, everything will work fine though. If you then set checked to true, React thinks the property suddenly comes into existence! This is when React figures you switched from uncontrolled to controlled, since now the prop checked exists.
In your example, you can get rid of this warning by changing checked={this.settings[setting]} to checked={!!this.settings[setting]}. Notice the double bang (!!). They will convert null or undefined to false (and leave true alone), so React will register your checked property with a value of false and start off with a controlled form component.
I had this problem too and I, too, read the docs about controlled-components several times to no avail, but I finally figured it out, so I thought I'd share. Also, since version 15.2.0, normal inputs are triggered to be controlled by setting value, while checkboxes are initialized as controlled by setting checked, regardless of their value property, which added a bit to the confusion.
Amoebe's answer is correct, but I think there's a cleaner solution than the double bank (!!). Simply add a defaultProps property with value false for checked prop of your Checkbox component:
import React from 'react';
const Checkbox = ({checked}) => (
<div>
<input type="checkbox" checked={checked} />
</div>
);
Checkbox.defaultProps = {
checked: false
};
export default Checkbox;
Basically, the defaultChecked means you don't want to control the input – it just renders with this value and then there is no way to control it. Also, value shouldn't be used, but checked instead, so your second code should be correct. And you shouldn't use them both simultaneously.
<input
type="checkbox"
checked={this.state.checked}
onChange={this.onChangeAction.bind(this)}
/>
Can you create a small fiddle with this behaviour?
Here is an answer using hooks should you choose to convert the class component to a functional one...
export default Checklist = () => {
const [listOfItems, setListOfItems] = useState([
{name: 'bubbles', isChecked: true},
{name: 'gregory', isChecked: false}
]);
const updateListOfItems = (itemIndex, isChecked) => {
const updatedListOfItems = [...listOfItems];
updatedListOfItems[itemIndex].isChecked = isChecked;
setListOfItems(updatedListOfItems);
}
return (
{listOfitems.map((item, index) =>
<index key={index} type="checkbox" checked={item.isChecked} onChange={() => updateListOfItems(index, !item.isChecked)} />
)}
)
}
You can assign your data to the state and then make use of the checked property associated with the individual checkbox to set the state as
{ this.state.data.map(function(item, index) {
return (
<input type="checkbox" checked={item.value} onChange={this.handleChange.bind(this, index)}/>
);
}.bind(this))
}
Sample Data in state is as
data: [{name:'bubbles', value: true}, {name:'gregory', value: false}]
JSFIDDLE
What finally worked, combining other answers:
const [categories, setCategories] = useState(
[
{
field: 'Label 1',
checked: true
},
{
field: 'Label 2',
checked: false
},
{
field: 'Label 3',
checked: false
}
]
)
const updateList = (item, isChecked) => {
const updatedList = [...categories];
updatedList[item].checked = isChecked;
setCategories(updatedList);
}
... in your markup:
{
categories.map((item, index) =>
<div key = {item.field}>
<label>
<span>{item.field}</span>
<input key={index}
type="checkbox"
checked={item.checked}
onChange={() => updateList(index, !item.checked)}
/>
</label>
</div>
)
}