I am trying to add validation for a form that has a checkbox selection with a number input next to each checkbox. A user selects a profession checkbox and then enters the number of years of experience they have in the input next to it. The array looks like this (experience has a default of 1):
const fieldsOfEng = [
{
id: "ELECTRICAL",
name: "Electrical",
experience: 1,
},
{
id: "MECHANICAL",
name: "Mechanical",
experience: 1,
}
]
This is how the schema would look if I was just verifying that the user selected one of the options in my professions array
export const userInfoSchema = z.object({
professions: z
.string()
.array()
.refine((val) => val.some(profession =>
fieldsOfEng
.map((field) => field.name)
.includes(profession)))
})
with the input being registered via react-hook-form like:
{fieldsOfEng.map((field) => {
return (
<input
{...register("professions")}
value={field.name}
type="checkbox"
/>
)}
--------------------WHAT I WANT:
I'd like to add an 'experience' field to my schema so I it would look something like (but this isn't correct):
professions: z
.array(
z.object({
name: z.string(),
experience: z.number(),
})
)
.refine(({name}) =>
name.some(({ profession }) =>
fieldsOfEng.map((field) => field.name).includes(profession)
)
)
.refine(({ experience }) => {
experience.some((exp) => exp > 1);
}),
And I think my form would look something like:
{fieldsOfEng.map((field) => {
return (
<input
{...register("professions.name")}
value={field.name}
type="checkbox"
/>
<input
{...register("professions.experience")}
value={field.experience}
type="number"
/>
)}
I can always experiment with the form but my main concern is the schema.
This answer is an update on my progress so far. I closer to getting my schema validation working correctly but for some reason it is not submitting. I could solve this faster if I could get react-hook form's errors to output correctly but since I'm using template literals when I'm registering the inputs, the error is just outputting nothing. I DO know there is an error though because my truthy error outputs the "professions:" string that I added in the p-tag.
Data for checkboxes in form (this is the data I'm validating via zod):
const fieldsOfEng = [
{
id: "ELECTRICAL",
name: "Electrical",
experience: undefined,
},
{
id: "MECHANICAL",
name: "Mechanical",
experience: undefined,
},
Schema for validation: I'm checking that the name they select is included in my fieldsOfEng array and making sure the experience is greater than 1.
const userInfoSchema = object({
professions: z
.object({
name: z
.string()
.refine((name) =>
fieldsOfEng.map((field) => field.name).includes(name)
),
experience: z.number().refine((experience) => experience > 1),
})
.array(),
});
React hook form:
type userInfoType = z.infer<typeof userInfoSchema>;
const {
register,
watch,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<userInfoType>({
resolver: zodResolver(userInfoSchema),
});
Form: Notice I am using template literals for registering each input. It took me forever to figure this out. So far this is the only way I've been able to get the data to output correctly (via react-hook-form's 'watch' method).
<fieldset>
<legend>Practicing fields of engineering</legend>
{fieldsOfEng.map((field, i) => {
return (
<div key={field.id}>
<div>
<input
{...register(`professions.${i}.name`)} <--REGISTERED HERE
value={field.name}
type="checkbox"
/>
<div>
<input
{...register(`professions.${i}.experience`)} <--REGISTERED HERE
type="number"
value={field.experience}
placeholder="Yrs Experience"
/>
</div>
</div>
<div>
{errors.professions && (
<p>
professions:
{errors.professions.message}
</p>
)}
</div>
</div>
);
})}
</fieldset>
Related
Hi I'm new to react and I'm trying to make a workout tracker but I have become hardstuck on trying to capture the input of the exercises the way I'd like, as you can see by the many commented out code blocks I think that I'm missing something fundamental with useState maybe?
this is my useState
const [formData, setFormData] = useState({
title: "",
date: "",
exercises: [{ Exercise: "benchpress", Reps: "5", Sets: "5" }],
});
this works with the title and date but i've tried many approaches and cant get it work with the object inside the exercise array
this is the onChange function on the form inputs
const updateForm = (e) => {
setFormData((currentFormData) => ({
...currentFormData,
[e.target.name]: e.target.value,
}));
All the solutions I've tried has just led to adding whole new objects to the exercises array, or just adding the name + value in the original object next to the title and date.
hey try like this it will update your exercise name and you can add more input fields to update more values
import * as React from 'react'
const Comp = () => {
const [formData, setFormData] = React.useState({
title: '',
date: '',
exercises: [{ Exercise: 'benchpress', Reps: '5', Sets: '5' }]
})
React.useEffect(() => {
console.log('gg', formData)
}, [formData])
const handle = (e, type) => {
console.log('gg', e)
setFormData({
title: formData.title,
date: formData.date,
exercises: [
{
Exercise: type === 'E' ? e : formData.exercises[0].Exercise,
Reps: type === 'R' ? e : formData.exercises[0].Reps,
Sets: type === 'S' ? e : formData.exercises[0].Sets
}
]
})
}
return (
<>
<input onChange={(e) => handle(e.target.value, 'E')} />
<input placeholder="rep" onChange={(e) => handle(e.target.value, 'R')} />
<input onChange={(e) => handle(e.target.value, 'S')} />
</>
)
}
Not the best way but it works
For an array with large number of objects/feilds you should use map instead of hard coding them
setFormData(formData.map((line, i) => {
line.title= "";
line.date="",
line.exercises.map((lineExerc, j) => {
lineExerc.name = e.target.value;
});
return line;
}));
There is a form that contains two inputs, a string and a number both being required.
Here is the schema:
const schema = yup.object().shape({
name: yup.string().required(),
age: yup.number().required()
});
Afterwards, it is react-hook-form:
const { handleSubmit, reset, control, register, watch } = useForm({
defaultValues: emptyObject,
resolver: yupResolver(schema)
});
As you can see, there is defaultValues which has emptyObject.
Here it is:
const emptyObject = {
name: '',
age: 0
}
The name part works fine, I cannot submit the data if there is no input written in that textbox but for age it doesn't require any value. I know it checks for value and it finds it is 0 and it considers there is an input.
But how to change it to make it work? Tried with null but still doesn't work.
I have to mention that it is React with Typescript so there is also an interface which throws error when age is set to null.
MyData: {
name: string;
age: number;
}
Make the age an empty string as well, just for your needs and when its time to submit the form just add some functionality to convert the string to number if its critical for your app.
From the DOCS, you can not set it to any undefined value:
It is encouraged that you set a defaultValue for all inputs to non-undefined such as the empty string or null.
Link to DOCS
I got this from https://react-hook-form.com/get-started#Applyvalidation
import React from "react";
import { useForm, SubmitHandler } from "react-hook-form";
interface IFormInput {
firstName: string;
lastName: string;
age: number;
}
export default function App() {
const { register, handleSubmit } = useForm<IFormInput>();
const onSubmit: SubmitHandler<IFormInput> = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { required: true, maxLength: 20, placeHolder: "John" })} />
<input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
<input type="number" {...register("age", { min: 18, max: 99 })} />
<input type="submit" />
</form>
);
}
can this be an example to go on with?
This is a hypothetical question right now, because I am not sure if my approach is even possible. I have a bunch of input checkbox fields in a form. I am using datoCms. I am thinking that I could use the repeater module to create as many or remove checkbox fields, and give them the same label and name as what I write in a text field in dato as a repeater. Now Dato outputs a graphQL api, that I can query and it would output the repeater fields as an array that I can map over after I query it.
So I will show you my code, and what I think will work, but I just need to be pointed in the right direction, and I can update this question with my attempt, but at this point I don't even know really where to start.
The part that is confusing to me is the React.useState how would I dynamically add values to it from a map method.
Ok here is my state code
const [formState, setFormState] = React.useState({
name: "",
package: `${data.datoCmsPricing.title}`,
email: "",
subject: "",
weightLoss:"",
message: "",
})
const onChange = (e) => {
if (e.target.type === 'checkbox' && !e.target.checked) {
setFormState({...formState, [e.target.name]: ''});
} else {
setFormState({...formState, [e.target.name]: e.target.value });
}
}
and here is my form
<form onSubmit={submitForm}>
<h3>Reasons for wanting to train</h3>
<label>
Weight Loss
<input
type="checkbox"
name="weightLoss"
checked={formState.weightLoss}
onChange={onChange}
/>
</label>
<button type="submit">Submit</button>
</form>
Now this is what I would propose I do with the form to get as many checkboxes, this is basically sudo code at this point because I think it would break at the checked part
{data.datoCmsPricing.details.map(detailEntry => {
return (
<label key={detailEntry.id}>
{detailEntry.reason}
<input
type="checkbox"
name={detailEntry.reason}
checked={formState.{detailEntry.reason}}
onChange={onChange}
/>
</label>
)
})}
after this I don't know what I would do with state?
Thank you ahead of time. Link to Repo https://github.com/wispyco/jlfit
useState perfectly works for dynamic data. And your code is almost correct.
You should add dataEntries property to useState object
const [formState, setFormState] = React.useState({
name: "",
package: `package`,
email: "",
subject: "",
weightLoss:"",
message: "",
dataEntries: {
'1': { reason: 'reason1', checked: false },
'2': { reason: 'reason2', checked: false },
'3': { reason: 'reason3', checked: false },
'4': { reason: 'reason4', checked: false }, },
})
I've prefilled dataEtries with demo data. In real app this data will be fetched from backend.
Modify onChange to correctly handle dataEntries object
const onChange = e => {
let value = undefined;
if (e.target.type === "checkbox") value = e.target.checked;
else value = e.target.value;
setFormState({
...formState,
dataEntries: {
...formState.dataEntries,
[e.target.id]: {
...formState.dataEntries[e.target.id],
[e.target.name]: value
}
}
});
};
And finally set correct name and id properties on form controls, so onChange can properly update data in state.
export const Checkbox = ({ onChange, detailEntry }) => (
<form>
<label key={detailEntry.id}>
{detailEntry.reason}
<input
type="checkbox"
name="checked"
id={detailEntry.id}
checked={detailEntry.checked}
onChange={onChange}
/>
<input
id={detailEntry.id}
name="target weight"
value={detailEntry["target weight"]}
onChange={onChange}
/>
</label>
</form>
);
I've added additional field "target weight" to showcase how any additional control can be used.
Complete demo is here
I am working on the front page of a website, in this context on the signup/login form combo. Those forms use the native forms from mobx-react, composed of a form.js observer, giving each element it's DOM structure, and a corresponding template.js file defining the role of each field. That means I can make nondescript fields in form.js and define them in template.js.
Sadly, one of my fields is being converted into a password input when nothing should make it one.
Here's a snip from form.js
export default observer(({ form }) => {
const isSuccess = state => (!state ? 'is-success' : 'is-danger');
return (
<article className='tiles is-ancestor is-inline-flex drc-column'>
<div className='tile is-vertical is-size-4 has-text-centered has-text-grey'>Inscription</div>
<form className='tile is-parent is-vertical'>
// good field
<div className='tile is-child'>
<label className='has-text-centered is-block' htmlFor={form.$('surname').id}>
{form.$('surname').label}
<input className={`input is-medium has-background-white-ter ${form.$('surname').isEmpty ? '' : isSuccess(form.$('surname').error)}`} id='signup-surname' {...form.$('surname').bind()} />
</label>
<p className='has-text-danger'>{form.$('surname').error}</p>
</div>
// multiple good fields
<button className='tile is-child button is-large has-background-success is-fullwidth has-text-white' type='submit' onClick={form.onSubmit}><span className='is-size-6 is-uppercase'>Je m'inscris</span></button>
<p className={form.error ? 'has-text-danger' : 'has-text-success'}>{form.error}</p>
</form>
</article>
);
});
and here's the other side of the coin, on template.js
setup() {
return {
fields: [{
name: 'name',
label: 'Prénom',
rules: 'required|string'
}, {
name: 'surname',
label: 'Nom',
rules: 'required|string'
}, {
name: 'company',
label: 'Entreprise',
rules: 'required|string'
}, {
name: 'phone',
label: 'Numéro de téléphone',
rules: 'required|numeric'
}, {
name: 'email',
label: 'E-mail',
rules: 'required|email|string'
}, {
name: 'password',
label: 'Mot de passe',
rules: 'required|string',
type: 'password'
}]
};
}
Here are a screenshot of the form itself, and of the actual interpreted HTML.
I want that surname field to be a regular text input, not a password input.
I can't redo the tech stack at this point to exclude using the forms from mobx-react, and it used to work just fine. Nothing is overwriting those input fields, those two snips are the only ones in control, I'm not sure where to look next.
Good day!
Issue was resolved.
When you were redirected (manually or otherwise) from the login form to the signup form, the fields were not reset. Since the second field on the login form was a password, it transitioned into the second field of the signup form, keeping the password type.
Resetting said fields with the componentWillUnmount mobx hook effectively fixes this issue:
componentWillUnmount() {
const { profileStore } = this.props;
profileStore.resetFields();
}
And into profileStore itself:
#observable fields = undefined;
#action
resetFields() {
log.warn('resetFields');
this.fields = undefined;
}
Now forms are effectively recreated from the ground up every time, no more problem.
Just made some slight changes, this should work now dude :)))))
export default observer(({ form }) => {
const isSuccess = state => (!state ? 'is-success' : 'is-danger');
return (
Inscription
// good field
{form.$('surname').label}
{form.$('surname').error}
// multiple good fields
Je m'inscris
<p className={form.error ? 'has-text-danger' : 'has-text-success'}>{form.error}</p>
</form>
</article>
);
});
I am using React-Select, in my form with Formik. I have the following array of Objects, that comes from the API, and which I map through it, in order to transform the data in the format React-select wants.
Here is the mapped through array of data, that I supply to the select.
[ { "label": "picard", "value": "picard" }, { "label": "riker", "value": "riker" } ]
It has a label and value pair. Exactly like react-select want it to. But in my Formik HOC, I have the PropType for the values of the data property(e.g: groups), to be a string, as it should, since the server will give me and receive unique strings.
Here is the PropType:
values: PropTypes.shape({
useraname: PropTypes.string,
email: PropTypes.string,
password: PropTypes.string,
confirmPassword: PropTypes.string,
group: PropTypes.string
}),
For that reason, every time i try to select an item from the dropdown, I get this:
checkPropTypes.js:19 Warning: Failed prop type: Invalid prop `values.group` of type `object` supplied to `AddEditUser`, expected `string`.
Here is the select element, and the mapping function:
mapListGroupToSelect = () => {
const { groups } = this.state;
return groups.map(group => ({
label: group.name,
value: group.name
}));
};
const groups = this.mapListGroupToSelect();
return (
<div className="pt-2">
<label className="font-weight-bold">
Select Group <Asterisk />
</label>
<Select
placeholder="Select a Group (Just One)"
onChangeCallback={handleChange}
type="simpleSelect"
options={groups}
isMulti={false}
id="group"
onChange={options => setFieldValue('group', options)}
/>
{JSON.stringify(groups, null, 2)}
<ErrorMessage name="group" component="div" className="text-danger" />
</div>
)
Do you have any ideas on how to solve this?