Using object as a value in `react-hook-form` - reactjs

Description
I have a component that uses an object as a value.
I would like to use this component with react-hook-form
The problem is that react-hook-form thinks that my object is a nested form control
Setup
This is just an example.
Range date picker is a common use case for such a behaviour
Example codepen
The value that the component accepts:
type ComponentValue = {
a: string;
b: string;
}
The component:
const Component = ({ value, onChange }: { value: ComponentValue, onChange: (value: ComponentValue) => void }) => {
return (
<input value={value.a} onChange={e => onChange({ a: e.target.value, b: (Math.random() * 100 | 0).toString() }) } />
)
}
Form value
type FormValues = {
someField: ComponentValue;
// other fields
};
Default values:
const defaultValues = {
someField: {
a: 'Default a',
b: 'Default b'
},
// other fields
};
useForm usage:
const {
register,
handleSubmit,
formState: { errors },
control,
} = useForm<FormValues>({
defaultValues,
});
The problem
Hovering over (or trying to use) errors reveals the type:
const errors: {
someField?: {
a?: FieldError | undefined;
b?: FieldError | undefined;
} | undefined;
// other fields
}
But I would it to be:
const errors: {
someField?: FieldError | undefined;
// other fields
}
Summary
Can I somehow force react-hook-form to treat my object as a value instead of a nested form field?

If you want to combine 2 inputs in a same component, you can try the approach with a controller.
export default function App() {
const { control, handleSubmit, watch } = useForm({
defaultValues: {
travel: {
from: "paris",
to: "london"
}
}
});
const onSubmit = (data) => {
console.log("dans onSubmit", JSON.stringify(data, null, 2));
};
return (
<>
<h1>Travel</h1>
<form
onSubmit={handleSubmit(onSubmit)}
style={{ display: "flex", flexDirection: "column", maxWidth: 600 }}
noValidate
>
<TravelPicker control={control} />
<input type="submit" />
<pre>{JSON.stringify(watch(), null, 2)}</pre>
</form>
</>
);
The component have a control props, to be able to register the 2 inputs :
const TravelPicker = ({ control }) => {
return (
<>
<label>
From
<Controller
name="travel.from"
control={control}
render={({ field: { onChange, value, name } }) => (
<input name={name} value={value} onChange={(e) => onChange(e)} />
)}
/>
</label>
<label>
To
<Controller
name="travel.to"
control={control}
render={({ field: { onChange, value, name } }) => (
<input name={name} value={value} onChange={(e) => onChange(e)} />
)}
/>
</label>
</>
);
};
The errors object has then the same structure as the form inputs.
See this sandbox for a functional exemple.
The createor of RHF seems to indicate here that you can also have only one component for 2 inputs.

You can use setValue and setError like below:
const {setValue, setError} = useForm();
// ... some where in your form
<input
type = "text"
name = "from"
onChange = {
(event) => {
const value = event.target.value;
if(value === ""){
setError("from", "This field is required");
}else{
setValue(`from[0]`, {
"otherProperty" : "otherValue",
"from_value": value, // this is your input value
});
}
}
}
/>
Probably you would be iterating through an array. change static 0 with your index in setValue If not you may use object syntax like below:
//....
setValue(`from.your_property_name`, {
"otherProperty" : "otherValue",
"from_value": value, // this is your input value
});
// ....
This won't cause re-render of component.
More info in doc
NOTE: Please be respectful when commenting.

Related

How to get date from IonDateTime with Ionic React

I have recently started using react due to the performance it provides, so I'm not used to this new framework. I have searched on this exact topic but cannot find an answer.
Although the problem is very simple, (Just want to return the selected date).
Here's what I currently am trying to do:
let dateValue = format(new Date(), 'yyyy-MM-dd')+ 'T09:00:00.000Z';
const dateChanged = (value: any) => {
console.log("value: ", value);
dateValue = value;
};
const DateModal: React.FunctionComponent<any> = ({ isOpen, onClose }) => {
return (
<IonModal className="datemodal" isOpen={isOpen}>
<IonContent className="dateModalOpen">
<IonDatetime
locale="en-GB"
value={dateValue}
id="datetime"
onChange={() => dateChanged(datetime)}
showDefaultButtons={true}
min="1920"
max="2022"
className="calendar"
presentation="date"
>
<span slot="title">Date of Birth</span>
</IonDatetime>
</IonContent>
</IonModal>
);
};
I recieve an error on the "onChange" (Cannot find name 'datetime'.), this is what I used to do in Angular. I tried to use a template reference by doing "id=datetime", which in Angular was "#datetime". And in so doing would work inside the onChange event.
How do I make this work?
Thank you in advance!
Solved. On react you can use "Controller" to get the data from IonDateTime as follows:
const {
handleSubmit,
control,
setValue,
register,
getValues,
formState: { errors },
} = useForm({
defaultValues: {
fullname: "",
date: "",
gender: "MALE",
email: "",
},
});
console.log(getValues());
/**
*
* #param data
*/
const onSubmit = (data: any) => {
alert(JSON.stringify(data, null, 2));
};
const [ionDate, setIonDate] = useState("");
const dateChanged = (value: any) => {
let formattedDate = format(parseISO(value), "dd-MM-yyyy").replace(
/-/g,
"/"
);
setIonDate(formattedDate);
setshowDate({ isOpen: false });
};
const DateModal: React.FunctionComponent<any> = ({ isOpen }) => {
console.log(isOpen);
return (
<IonModal className="datemodal" isOpen={isOpen}>
<IonContent className="dateModalOpen">
<Controller
render={({ field }) => (
<IonDatetime
value={field.value}
onIonChange={(e) => dateChanged(e.detail.value)}
locale="en-GB"
onChange={dateChanged}
showDefaultButtons={true}
onIonCancel={() => setshowDate({ isOpen: false })}
min="1920"
max="2022"
className="calendar"
presentation="date"
>
<span slot="title">Date of Birth</span>
</IonDatetime>
)}
control={control}
name="date"
rules={{ required: "This is a required field" }}
/>
<IonButton type="submit">submit</IonButton>
</IonContent>
</IonModal>
);
};
};
You just need to use the ionOnChange handler:
<IonDatetime presentation="date"
id="datetime"
onIonChange={(e) => console.log(e)}>
</IonDatetime>

React-hook-form doesn't change the form validity when React-Select is changed

I have a react-hook-form with a react-select:
When I change the value of react-select, the form becomes both dirty and valid.
The submit button is now enabled and the form can be sent.
Once the form is sent, useForm defaultValue's are reset to the new values
The submit button is disabled again, waiting for a change in the form.
When I change again react-select value, the form is not dirty. The button remains disabled. Yet, the react-select value has changed.
I have tried almost everything: replace onChange by setValue("name", value, {shouldDirty: true}, etc.
Here is the code:
import { useForm, Controller } from "react-hook-form";
import Select from "react-select";
const options = [
{ value: "joe", label: "Joe" },
{ value: "jack", label: "Jack" },
{ value: "averell", label: "Averell" }
];
type Data = { name: string; surname: string };
export default function Form({
defaultValues,
onSend
}: {
defaultValues: Data;
onSend: (data: Data) => void;
}) {
const {
register,
handleSubmit,
reset,
getValues,
control,
formState: { isValid, isDirty }
} = useForm({
mode: "onChange",
defaultValues
});
const onSubmit = (data: Data) => {
onSend(data);
reset(getValues());
console.log("data sent!", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="name"
render={({ field }) => (
<Select
{...field}
value={field.value}
options={options}
onChange={(v) => field.onChange(v)}
/>
)}
/>
<input placeholder="surname" {...register("surname")}/>
<button type="submit" disabled={!isValid || !isDirty}>
submit
</button>
</form>
);
}
How to fix this?
ps: here is also a sandbox: https://codesandbox.io/s/react-hook-form-test-8tdq3?file=/src/FormController.tsx
So the answer is that I needed to add a useEffect to reset the form values after the submission:
useEffect(() => {
if (formState.isSubmitSuccessful) {
reset(getValues());
}
}, [formState, getValues, reset]);

Cannot set defaultValue in React-Select

I am using React-Select library in my react project, i am stuck on a point where i want to set default value on the first select option rendered in a loop.
Here is the code below for your understanding
export default function FormSection() {
const options = [
{ value: "Titular", label: "Titular", isDisabled: true },
{ value: "Conjuge", label: "Conjuge" },
{ value: "Filho(a)", label: "Filho(a)" },
];
const formik = useFormik({
initialValues: {
relation: null,
},
onSubmit: (values) => {
console.log(values);
},
});
const calcFormikValuesSelect = (index) => {
if (formik.values.relation == null) {
return null;
} else if (formik.values.relation) {
let allrelation = formik.values.relation;
return allrelation[index];
}
};
const handleChangeRelation = (selectedOption, index) => {
console.log(selectedOption, index);
formik.setFieldValue(`relation[${index}]`, selectedOption);
// formik.setFieldValue('firstSmoker', selectedOption)
};
return (
<div className="secondSection">
<form onSubmit={formik.handleSubmit}>
{Array.apply(null, { length: 3 }).map((item, index) => (
<React.Fragment>
<div className="wrapper-person-2">
<p className="tab-text">Profissão</p>
<Select
value={calcFormikValuesSelect(index)}
name={`relation[${index}]`}
onChange={(selectedOption) =>
handleChangeRelation(selectedOption, index)
}
options={options}
className="select-smoker"
/>
</div>
</React.Fragment>
))}
<button type="submit" className="button-enabled">
CONTINUE
</button>
</form>
</div>
);
}
So for index=0, i want to set default value
{value:'Titular,label:'Titular}
and disabled that Select so that dropdown does not show on click and then for index above 0 I want to show the options as options array has
I tried passing prop to React-Select like
defaultValue:{label:'Titular',value:'Titular}
but that doesn't work
"react-select": "^4.3.1",
Hope someone helps me out, thanks !
To set the default value in formik, you need to provide it in initialValues like this:
const formik = useFormik({
initialValues: {
relation: {0: options[0] } OR [options[0]], // depends on how you put it
},
onSubmit: (values) => {
console.log(values);
},
});

React-Hook-Form unexpected behavior with Multi Level Nested Object

Lately i discovered the react-hook-from plugin which on the first sight seem to be perfect to start using and replace other plugins due to its great performance.
After using the plugin for some really simple forms i came across a complicated form that i wish to handle with the plugin. My form is based on a nested object which has the following structure. (Typescript definition)
type model = {
tag: string;
visible: boolean;
columns?: (modelColumn | model)[];
}
type modelColumn = {
property: string;
}
So in order to handle to handle a n-level nested form i created the following components.
const initialData = {
...
datasource: {
tag: "tag1",
visible: true,
columns: [
{
property: "property",
},
{
property: "property1",
},
{
tag: "tag2",
visible: true,
columns: [
{
property: "property",
},
{
tag: "tag3",
visible: false,
columns: [
{
property: "property",
}
]
}
]
},
{
entity: "tag4",
visible: false,
}
],
},
...
}
export const EditorContent: React.FunctionComponent<EditorContentProps> = (props: any) => {
const form = useForm({
mode: 'all',
defaultValues: initialData,
});
return (
<FormProvider {...form}>
<form>
<Handler
path="datasource"
/>
</form>
</FormProvider>
);
}
In the above component the main form is created loaded with initial data. (I provided an example value). The Handler component has the login of recursion with the path property to call nested logic of the form data type. Here is the sample implementation.
...
import { get, isNil } from "lodash";
import { useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form";
...
export type HandlerProps = {
path: string;
index?: number;
...
}
export const Handler: React.FunctionComponent<HandlerProps> = (props) => {
const { path, index, onDelete, ...rest } = props;
const { control } = useFormContext();
const name = isNil(index)? `${path}` :`${path}[${index}]`;
const { fields, append, insert, remove } = useFieldArray({
control: control,
name: `${name}.columns`
});
...
const value = useWatch({
control,
name: `${name}`,
});
...
const addHandler = () => {
append({ property: null });
};
console.log(`Render path ${name}`);
return (
<React.Fragment>
<Row>
<Col>
<FormField name={`${name}.tag`} defaultValue={value.tag}>
<Input
label={`Tag`}
/>
</FormField>
</Col>
<Col>
<FormField defaultValue={value.visible} name={`${name}.visible`} >
<Switch />
</FormField>
</Col>
<Col>
<button onClick={addHandler}>Add Column Property</button>
</Col>
</Row>
{
fields && (
fields.map((field: any, _index: number) => {
if (field.property !== undefined) {
return (
<Column
path={`${name}.columns`}
index={_index}
control={control}
fields={fields}
onDelete={() => remove(_index) }
/>
)
} else {
return (
<Handler
path={`${name}.columns`}
index={_index}
/>
)
}
})
)
}
</React.Fragment>
);
}
Essentially the Handler Component uses the form's context and call itself if it should render a nested object of the form like datasource.columns[x] which the register to useFieldArray to get it's columns. Everything so far works fine. I render the complete tree (if i can say) like object form correctly.
For reference here is the code of the Column Component as also for the formField helper component FormField.
export const Column: React.FunctionComponent<ColumnProps> = (props) => {
const { fields, control, path, index, onDelete } = props;
const value = useWatch({
control,
name: `${path}[${index}]`,
defaultValue: !isNil(fields[index])? fields[index]: {
property: null,
}
});
console.log(`Render of Column ${path} ${value.property}`);
return (
<Row>
<Col>
<button onClick={onDelete}>Remove Property</button>
</Col>
<Col>
<FormField name={`${path}[${index}].property`} defaultValue={value.property}>
<Input
label={`Column Property name`}
/>
</FormField>
</Col>
</Row>
);
}
export type FormFieldProps = {
name: string;
disabled?: boolean;
validation?: any;
defaultValue?: any;
children?: any;
};
export const FormField: React.FunctionComponent<FormFieldProps> = (props) => {
const {
children,
name,
validation,
defaultValue,
disabled,
} = props;
const { errors, control, setValue } = useFormContext();
return (
<Controller
name={name}
control={control}
defaultValue={defaultValue? defaultValue: null}
rules={{ required: true }}
render={props => {
return (
React.cloneElement(children, {
...children.props,
...{
disabled: disabled || children.props.disabled,
value: props.value,
onChange: (v:any) => {
props.onChange(v);
setValue(name, v, { shouldValidate: true });
},
}
})
);
}}
/>
);
}
The problem is when i remove a field from the array. The Handler component re-renders having correct values on the fields data. But the values of useWatch have the initial data which leads to a wrong render with wrong fields being displayed an the forms starts to mesh up.
Is there any advice on how to render such a nested form the correct way. I guess this is not an issue from the react-hook-form but the implementation has an error which seems to cause the problem.

How to iterate children in React using render props

I have this pseudo code for my form. Where I would like to display just fields with canAccess=true.
const initialValues = {
firstName: { canAccess: true, value: 'Mary' },
surName: { canAccess: false, value: 'Casablanca' }
}
<Form initialValues={initialValues}>
{props =>
<>
<div className="nestedItem">
<Field name="firstName" />
</div>
<Field name="surName" />
</>
}
</Form>
With this code I would like to see rendered just field with firstName.
I know that I can iterate through React.Children.map() but I don't know how to iterate children when using render props.
Also there can be nested elements, so I would like to find specific type of component by name.
Thanks for help.
const initialValues = {
firstName: { canAccess: true, value: 'Mary' },
surName: { canAccess: false, value: 'Casablanca' }
}
<Form initialValues={initialValues}>
{props =>
<>
{
Object.keys(props.initialValues).map(k => (
k.canAccess && <Field name={k} />
));
}
</>
}
</Form>
Edit: Your form can perform some logic and pass back filtered items to your component.
getFilteredItems = items => Object.keys(items).reduce((acc, key) => {
const item = items[key];
const { canAccess } = item;
if(!canAccess) return acc;
return {
...acc,
[key]: item
}
}, {}));
render() {
const { initialValues, children } = this.props;
const filteredItems = this.getFilteredItems(initialValues);
return children(filteredItems);
}
<Form initialValues={initialValues}>
{ props =>
<>
{
Object.keys(props).map(k => <Field name={k} />)
}
</>
</Form>
This is what I was looking for.
const Form = ({initialValues, children}) =>
props =>
<Authorized initialValues={initialValues}>
{typeof children === 'function' ? children(props) : children}
</Authorized>
const Authorized = ({initialValues, children}) => {
// Do check
React.Children.map(chidlren, x => {
if(x.type === Field ) // && initialValues contains x.props.name
return x
return null
... })
}

Resources