Formik check if checkbox is checked - reactjs

I am having the hardest time figuring out how to determine if the checkbox is checked using Formik and React. So far i am able to return the value of the checked box but can't seem to return true or false.
I am trying to add a class to the checkbox if it has been checked, which I can do if it has a value but when unchecked, it doesn't remove the class. I think i may be approaching this incorrectly...
Any help is much appreciated. Thank you
Initial Values:
const initialValues = {
// step two
processorOptions: [],
};
processorOptions:
const processorOptions = [
{ type: 'cc', value: 'test1', name: 'Test 1' },
{ type: 'cc', value: 'test2', name: 'Test 2' },
{ type: 'cc', value: 'test3', name: 'Test 3' },
];
Custom Field Component:
const Processors = () => {
const { values } = useFormikContext();
return (
<>
<FieldArray
name='processorOptions'
render={(arrayHelpers) => (
<>
{processorOptions.map((processorOption, index) => (
<Col lg={4} key={index}>
<div className='form-group'>
<div className='form-check'>
<label className={`onboard__processor-label ${processorOption.checked ? 'is-checked' : ''}`}>
<input
name='processorOptions'
type='checkbox'
value={processorOption.id}
checked={values.processorOptions.includes(processorOption.id)}
onChange={(e) => {
if (e.target.checked) {
arrayHelpers.push(processorOption.id);
} else {
const idx = values.processorOptions.indexOf(processorOption.id);
arrayHelpers.remove(idx);
}
}}
/>
{processorOption.name}
</label>
</div>
</div>
</Col>
))}
</>
)}
/>
<pre>{JSON.stringify(values.processorOptions, null, 2)}</pre>
</>
);
};

Related

How do I create dynamic form for users to create and populate html select elements?

Scenario: building an eCommerce type application in react. A feature is allowing merchants to create product listings. Now most products will have options (sizes, colors, etc.) and merchants should be able to add them to the listing. I am having trouble adding this part to the form as it needs to be dynamic because we don't know many options a merchant wants to add to a product listing, nor do we know how many values the option will take.
A filled array of options should look something like this before being sent to server:
const options = [
option1:
{
name: 'Size',
values: ['small', 'medium', 'large']
},
option2:
{
name: 'Color',
values: ['blue', 'black', 'white', 'tan']
},
]
I just don't know how to go about constructing the UI and the logic to able this feature.
Hope this code helps you.
CodeSandBox
const [options, setOptions] = useState([
{
name: "Size",
values: ["small", "medium", "large"]
},
{
name: "Color",
values: ["blue", "black", "white", "tan"]
}])
const [optionName, setOptionName] = useState("")
const [optionVal, setOptionVal] = useState([])
// console.log("name : ", { name: optionName });
// console.log("val : ", { values: optionVal });
const handleSubmit = () => {
const newOpt = {
name: optionName,
values: optionVal
};
// console.log(newOpt);
setOptions([...options, newOpt]);};
console.log("option is ", options);
return (
<>
<div>
<input
id="optionName"
type="text"
onBlur={(e) => setOptionName(e.target.value)}
/>
<br />
<input
id="optionVal"
type="text"
placeholder="Use , to splite values"
onBlur={(e) => {
const data = e.target.value;
setOptionVal(data.split(","));
}}
/>
<br />
<button type="submit" onClick={handleSubmit}>
Submit form
</button>
</div>
<div>
{options.map((item, i) => (
<>
<div>{item.name}</div>
<div>
{item.values.map((val) => (
<div>{val}</div>
))}
</div>
<br />
</>
))}
</div>
</>);

react hook form validation is not working after paste value

I created a model for a form which maps throught it to create form using react-hook-form
when i submit form for the first time, everything is Okay. but for the other times, when i paste a value into inputs, validation not work correctly(it shows required error but the input is not empty)
this is model:
export const formData = [
{
id: 1,
type: "text",
labelRequired: true,
label: "name",
shape: "inline",
placeholder: "name",
name: "nameOne",
validation: {
required: true,
minLength: 8,
},
size: 6,
},
{
id: 2,
type: "text",
labelRequired: true,
label: "family",
shape: "inline",
placeholder: "family",
name: "nameTwo",
validation: {
required: true,
minLength: 8,
},
size: 6,
},
{
id: 7,
type: "checkbox",
label: "Sample",
shape: "checkbox",
placeholder: "placeholder",
name: "checkbox_btn",
data: [
{
id: 41,
inputId: "inline-form-1",
label: "1",
},
],
size: 12,
},
];
the map method to create form based on model:
const Sample = () => {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm({
mode: "onBlur",
});
const submitHandler = (data, e) => {
e.target.reset();
console.log(data);
reset();
};
const mapForm = formData.map((item) => {
if (item.shape === "inline") {
return (
<InputContainer
key={item.id}
register={{
...register(item.name, {
required: item.validation.required,
minLength: item.validation.minLength,
}),
}}
validation={item.validation}
placeholder={item.placeholder}
label={item.label}
size={item.size}
error={errors[item.name]}
/>
);
}
if (item.shape === "checkbox") {
return (
<CheckBoxContainer
key={item.id}
label={item.label}
data={item.data}
register={{ ...register("medium", { required: true }) }}
error={errors.medium}
/>
);
}
});
return (
<Fragment>
<Breadcrumb parent="Dashboard" title="Default" />
<Container fluid={true}>
<Row>
<Col sm="12">
<Card>
<CardHeader>
<h5>Sample Card</h5>
</CardHeader>
<CardBody>
<Form onSubmit={handleSubmit(submitHandler)}>
<Row>{mapForm}</Row>
<Button type="submit" className="m-t-40">
Submit
</Button>
</Form>
</CardBody>
</Card>
</Col>
</Row>
</Container>
</Fragment>
);
};
export default Sample;
Also there are some container components for wrapping inputs like:
const InputContainer = ({
id,
register,
error,
label,
labelRequired,
size,
type,
placeholder,
validation,
}) => {
return (
<Col lg={size} style={{ marginTop: "-10px" }}>
<FormGroup>
<Label className={`col-form-label`}>
{label} {labelRequired && <span></span>}
</Label>
<Input
className="form-control"
type={type}
placeholder={placeholder}
{...register}
/>
{error && error.type === "required" && (
<p className="p-16 text-danger">this filed is required</p>
)}
</FormGroup>
</Col>
);
};
export default InputContainer;
I have not looked that deep into it to find out if this happens from a simple misuse of the useForm() hook, but here's what I've figured out.
According to the source code, register() returns the following interface (unrelated stuff omitted):
type UseFormRegisterReturn<...> = {
onChange: ChangeHandler;
onBlur: ChangeHandler;
ref: RefCallBack;
...
}
As you can see, this means only the onChange() and onBlur() events are being registered. onChange() is the one we're interested in.
After some quick testing, I've realized that for some unknown reason (maybe a browser bug? no idea), in certain conditions onChange() doesn't trigger when pasting text with CTRL+V.
Luckily, the regular onInput() event still triggers, so we can simply define this event using the register.onChange() handler:
<Input
className="form-control"
type={type}
placeholder={placeholder}
{...register}
onInput={register && register.onChange}
// TypeScript
// onInput={register?.onChange}
/>
If onInput() is already being used:
<Input
className="form-control"
type={type}
placeholder={placeholder}
{...register}
onInput={e => {
doOtherStuff(e);
if (register && register.onChange) {
register.onChange(e);
}
// TypeScript
// register?.onChange(e);
}
/>

React Formik Material-UI autocomplete: how to submit multiple selected values on submit

I'm trying to figure out how to use Material-UI Autocomplete with Formik.
Here I've created a form which displays 3 question fields per page, which is working fine.
Since I'm very new to this and don't really understand Formik very well I'm having trouble with this type of input.
So what I'm doing here is that first there is a condition, you select (yes or no) depending on if you want to answer the question. After that there is the Autocomplete dropdown, from which you can select multiple options. The part where I'm having trouble is I don't know how to submit multiple selected options. The options you select from the autocomplete dropdown will go into the remedies array.
In the end it should return something like this:
question11: {
agree: 'Yes',
remedies: ['option you selected', 'another selected option']
}
So I want to be able to select multiple values from Autocomplete and populate them in the remedies: [] array.
The FormikControl is another thing I made which is just a bunch of switch statements which render a certain type of Input depending on the condition that is passed. In this case its returning the Input Component I posted below.
The form:
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Formik, Form } from 'formik';
import * as yup from 'yup';
import FormikControl from './FormikControl';
import SurveyFormLayout from '../surveyForm/SurveyFormLayout';
import PrimaryBtn from '../../../components/buttons/PrimaryBtn';
const data = [
{
type: 'radioWithDropdown',
question: 'Do you take any natural remedies? (Vitamins, minerals, amino acids,
herbs etc.)',
name: 'question11',
conditions: [ { key: 'yes', value: 'Yes' }, { key: 'no', value: 'No' } ],
options: [
{ key: 'Option 11', value: 'word' },
{ key: 'Option 12', value: 'another word' },
{ key: 'Option 13', value: 'some other' },
{ key: 'Option 14', value: 'random' }
]
}
]
const InputAssesment = () => {
const initialValues = {
question11: {
agree: '',
remedies: []
}
};
const validationSchema = yup.object({
question11: yup.object().shape({
agree: yup.string(),
remedies: yup.array()
})
});
return (
<SurveyFormLayout>
<div className="survey-form__container">
<FormikStepper
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={() => {}}
>
<FormikStep>
<FormikControl
control="conditionWithSelectMultiple"
label={data[9].question}
name="question11.remedies"
options={data[9].options}
conditionName="question11.agree"
conditionOptions={data[9].conditions}
/>
</FormikStep>
</FormikStepper>
</div>
</SurveyFormLayout>
)
}
export function FormikStep({ children }) {
return <div className="formik-step">{children}</div>;
}
export function FormikStepper({ children, ...props }) {
const childrenArray = React.Children.toArray(children);
const [ step, setStep ] = useState(0);
const currentChild = childrenArray[step];
function isLastStep() {
return step === childrenArray.length - 1;
}
return (
<Formik
{...props}
onSubmit={async (values, helpers) => {
if (isLastStep()) {
await props.onSubmit(values, helpers);
} else {
setStep((s) => s + 1);
}
console.log(values);
}}
>
<Form autoComplete="off" className="formik-form">
{currentChild}
<div className="survey-btns-container">
{step > 0 ? (
<button
className="btn-secondary survey-back-btn"
type="button"
onClick={() => setStep((s) => s - 1)}
>
Back
</button>
) : null}
<PrimaryBtn text={isLastStep() ? 'Submit' : 'Next'} type="submit" />
</div>
</Form>
</Formik>
);
}
The input component:
import React from 'react';
import { Field } from 'formik';
import Checkbox from '#material-ui/core/Checkbox';
import TextField from '#material-ui/core/TextField';
import { Autocomplete } from 'formik-material-ui-lab';
import CheckBoxOutlineBlankIcon from '#material-ui/icons/CheckBoxOutlineBlank';
import CheckBoxIcon from '#material-ui/icons/CheckBox';
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
const ConditionWithSelectMultiple = (props) => {
const { label, name, options, conditionName, conditionOptions, ...rest } = props;
return (
<div className="question-field">
<label className="survey-question-h">{label}</label>
<div className="radio-options-container">
<Field name={conditionName} {...rest}>
{({ field }) => {
return conditionOptions.map((option) => {
return (
<div key={option.key} className="radio-button-option">
<input
className="radio-button"
type="radio"
id={option.value}
{...field}
value={option.value}
checked={field.value === option.value}
/>
<div className="radio-button-gap" />
<label htmlFor={option.value} className="radio-button-option-label">
{option.value}
</label>
</div>
);
});
}}
</Field>
</div>
<Field
name="name"
component={Autocomplete}
multiple
options={options}
disableCloseOnSelect
getOptionLabel={(option) => option.key}
renderOption={(option, { selected }) => (
<React.Fragment>
<Checkbox icon={icon} checkedIcon={checkedIcon} style={{ marginRight: 8 }} checked={selected} />
{option.key}
</React.Fragment>
)}
style={{ width: 500 }}
renderInput={(params) => <TextField {...params} variant="outlined" placeholder="Select" />}
/>
</div>
);
};
export default ConditionWithSelectMultiple;
Any help is more than welcome

unable to setFieldValue in FieldArray using Formik

thanks for reading this.
I am trying to build a form using Formik. And it includes a FieldArray inside a FieldArray.
For some reason, setFieldValue works as I can console.log the correct e.target.name and e.target.value
The problem is upon submitting the form, all the values from the inputs are not where they supposed to be. The expected behavior is the values should be inside of exclusionGroups, not outside.
Would anyone be able to give me some insight ? I've been stuck on this the whole day and feels like my head is going to explode.
Expected:
exclusionGroups: [
{
exclusion: [
{
param: 'delayMin',
operator: '<',
value: 'test1',
},
{
param: 'airlineCode',
operator: '>',
value: 'test2',
},
],
},
{
exclusion: [
{
param: 'flightNumber',
operator: '=',
value: 'test3',
},
],
},
],
Reality:
exclusionGroups: (2) [{…}, {…}]
group-1-operator-1: "<"
group-1-operator-2: ">"
group-1-param-1: "delayMin"
group-1-param-2: "airlineCode"
group-1-value-1: "test1"
group-1-value-2: "test2"
group-2-operator-1: "="
group-2-param-1: "flightNumber"
group-2-value-1: "test3"
My Code:
Index.tsx
type ExclusionRuleValues = {
param: string;
operator: string;
value: string;
};
interface ExclusionGroupProps {
exclusionRules?: ExclusionRuleValues[];
}
const Exclusion = ({ data, type }: any) => {
const onSubmit = (values: any) => {
console.log(values);
};
const initialValues = {
exclusionGroups: [
{
exclusion: [
{
param: 'group1',
operator: 'group1',
value: 'group1',
},
],
},
{
exclusion: [
{
param: 'group2',
operator: 'group2',
value: 'group2',
},
],
},
],
};
const emptyGroup = {
exclusion: [
{
param: '',
operator: '',
value: '',
},
],
};
return (
<React.Fragment>
<Formik
enableReinitialize
onSubmit={onSubmit}
initialValues={initialValues}
render={(formProps) => {
const { values, handleSubmit, submitForm } = formProps;
const { exclusionGroups } = values;
const setFieldValue = (e: ChangeEvent<HTMLInputElement>) => {
return formProps.setFieldValue(e.target.name, e.target.value);
};
return (
<React.Fragment>
<Header>
Model will return excluded message code if the following condition is true.
<Button onClick={submitForm} color="primary">
Save Changes
</Button>
</Header>
<Form onSubmit={handleSubmit}>
<FieldArray
name="exclusionGroups"
render={(arrayHelper) => (
<React.Fragment>
{exclusionGroups.map((exclusionRulesGroup: any, index: number) => {
return (
<TargetFields
type={type}
group={index + 1}
key={`field-${index}`}
name={`${arrayHelper.name}.${index}`}
setFieldValue={setFieldValue}
/>
);
})}
<AddNewGroupButton type="button" onClick={() => arrayHelper.push(emptyGroup)}>
+ New Group
</AddNewGroupButton>
</React.Fragment>
)}
/>
</Form>
</React.Fragment>
);
}}
/>{' '}
</React.Fragment>
);
};
export default Exclusion;
TargetFields.tsx
interface TargetFieldsProps {
group: number;
name: string;
type: string;
data?: ExclusionRuleValues[];
setFieldValue: (e: ChangeEvent<HTMLInputElement>) => void;
}
const TargetFields = ({ group, data, name, type, setFieldValue }: TargetFieldsProps) => {
const emptyTarget = {
param: '',
operator: '',
value: '',
};
return (
<React.Fragment>
<Field name={name}>
{(fieldProps: any) => (
<React.Fragment>
<ExclusionGroupHeader>
<b>Group {group}</b>
</ExclusionGroupHeader>
<Wrapper>
<FieldArray
name={`${fieldProps.field.name}.exclusion`}
key={`exclusion-${group}`}
render={(targetHelper) => (
<React.Fragment>
{fieldProps.field.value.exclusion.map(
(target: ExclusionRuleValues, key: number) => {
const { param, operator, value } = target;
return (
<ExclusionRuleGroup key={`group-${key}`}>
<ExclusionRuleHeader>
<b>Target {key + 1}</b>
<DeleteButton type="button" onClick={() => targetHelper.remove(key)}>
remove
</DeleteButton>
</ExclusionRuleHeader>
<StyledRow>
<CCol sm="4">
<Select
onChange={setFieldValue}
// value={param}
label="Params"
name={`group-${group}-param-${key + 1}`}
options={
type === 'input' ? InputExclusionParams : OutputExclusionParams
}
placeholder="Operator"
/>
</CCol>
<CCol sm="4">
<Select
onChange={setFieldValue}
// value={operator}
label="Operator"
name={`group-${group}-operator-${key + 1}`}
options={SelectOptions}
placeholder="Operator"
/>
</CCol>
<CCol sm="4">
<Input
onChange={setFieldValue}
// value={value}
label="Value"
name={`group-${group}-value-${key + 1}`}
type="text"
placeholder="Value"
/>
</CCol>
</StyledRow>
</ExclusionRuleGroup>
);
}
)}
<AddNewRuleButton type="button" onClick={() => targetHelper.push(emptyTarget)}>
+ New Rule
</AddNewRuleButton>
</React.Fragment>
)}
/>
</Wrapper>
</React.Fragment>
)}
</Field>
</React.Fragment>
);
};
export default TargetFields;
The problem is when you pass the name property to your form's inputs.
In the component TargetFields you are passing the name like name={`group-${group}-operator-${key + 1}`} so formik think you want to store that value in the property that will result from that string, e.g. group-1-operator-1 and that is why it's going of the object you want, but going in a property with that name.
If you want to use nested objects / arrays, you need to concatenate the name with object with want using . or [${index}], just like you did in here
<TargetFields
type={type}
group={index + 1}
key={`field-${index}`}
name={`${arrayHelper.name}.${index}`} // You use the . with the name and the index.
setFieldValue={setFieldValue} // It could also be using [] instead of . like this => `${arrayHelper.name}[${index}]`
/>
and like here
<FieldArray
name={`${fieldProps.field.name}.exclusion`} // You use the . with the name and the property of the object you want
key={`exclusion-${group}`}
render={(targetHelper) => ( ... )
/>
So to solve you problem, you need to change the following.
<StyledRow>
<CCol sm="4">
<Select
onChange={setFieldValue}
// value={param}
label="Params"
name={`${targetHelper}[${key}].param`}
options={
type === 'input' ? InputExclusionParams : OutputExclusionParams
}
placeholder="Operator"
/>
</CCol>
<CCol sm="4">
<Select
onChange={setFieldValue}
// value={operator}
label="Operator"
name={`${targetHelper}[${key}].operator`}
options={SelectOptions}
placeholder="Operator"
/>
</CCol>
<CCol sm="4">
<Input
onChange={setFieldValue}
// value={value}
label="Value"
name={`${targetHelper}[${key}].value`}
type="text"
placeholder="Value"
/>
</CCol>
</StyledRow>
And just a guess, you didn't code all that by your self right? Because in one place you are doing exactly what you need to do to solve your problem. If you don't know how that name thing works with FieldArray, I recommend you reading this part of the formik docs. Know how this works is very important for using nested objects / arrays.

How can I set the values of a dynamic set of range inputs in React?

I have an array that I want to loop through and create range inputs out of.
const ratings = [
{
title: 'Category One'
},
{
title: 'Category Two'
},
{
title: 'Category Three'
}
];
const renderRatings = ratings.map((rating, key) => {
return (
<div className="rating" key={key}>
<h3>{rating.title}</h3>
<div className="rating__num">0</div>
<input className="rating__range" type="range" min="0" max="10" />
</div>
);
});
How would I update each rating__num to reflect the value of it's rating__range sibling?
I figured it out. I just needed to create a new component out of the rating, so it has it's own state.
function Rating(props) {
const [ratingValue, setRatingValue] = useState(0);
return (
<div className="rating" key={key}>
<h3>{rating.title}</h3>
<div className="rating__num">{ratingValue}</div>
<input
className="rating__range"
type="range"
min="0"
max="10"
onChange={(e) => setRatingValue(e.currentTarget.value)}
/>
</div>
):
}
function MyComponent(props) {
const renderRatings = ratings.map((rating, key) => {
return (
<Rating key={key} rating={rating} />
);
});
return (
{renderRatings()}
);
}

Resources