I'm doing application with friends. I'm using react-hook-form. The api gets only null values. It's a very crucial element of our application. Please help, here is my code:
const NewItemBar = ({ isVisible, handleClose }) => {
const { register, handleSubmit, watch, errors } = useForm();
const onSubmit = ({ name, data, description }) => {
api
.post(endpoints.createTask, {
name,
data,
description,
})
.then((response) => {
console.log(response);
})
.catch((err) => console.log(err));
};
return (
<StyledWrapper handleClose={handleClose} isVisible={isVisible}>
<StyledHeading big>Dodaj zadanie do wykonania</StyledHeading>
<StyledForm onSubmit={handleSubmit(onSubmit)}>
<StyledInput
placeholder="nazwa zadania"
type="text"
name="name"
{...register('name', { required: 'Required' })}
/>
<StyledInput
placeholder="data wykonania"
type="text"
name="data"
{...register('data', { required: 'Required' })}
/>
<StyledTextArea
type="text"
placeholder="opis"
name="description"
as="textarea"
{...register('description', { required: 'Required' })}
/>
<StyledButton type="submit">Zapisz</StyledButton>
</StyledForm>
</StyledWrapper>
);
};
In case of custom components you should create wrapper using Controller or pass a function with forwardRef.
https://react-hook-form.com/get-started#IntegratingwithUIlibraries
forwardRef might be useful when you want your component to manage inner state on it's own.
For example:
const PhoneInput = forwardRef(
(
{
id,
name,
label,
placeholder,
errorMsg,
onSubmit,
onChange,
onBlur,
disabled,
},
ref
) => {
const [value, setValue] = useState("");
const _onChange = (value) => {
setValue(value);
onChange(value);
};
const classes = (className) =>
[
className,
disabled ? "disabled" : "",
errorMsg ? "is-invalid" : "",
].join(" ");
console.log(value);
return (
<div className={classes("f-form-group phone-input")}>
<div className="phone-input-wrap">
<div className="inputs">
<label htmlFor={name}>{label}</label>
<NumberFormat
className={classes("f-form-control")}
name={name}
id={id}
type="tel"
format="+7 (###) ### ##-##"
mask="_"
placeholder={placeholder}
value={value}
onChange={(e) => _onChange(e.target.value)}
onBlur={onBlur}
getInputRef={ref}
/>
</div>
<button
type="button"
className="btn btn-primary"
onClick={() => onSubmit(value)}
>
Подтвердить
</button>
</div>
<div className="invalid-feedback">{errorMsg}</div>
</div>
);
}
);
Related
I have a Checkbox component which works fine, until I had a Yup schema:
const Checkbox = React.forwardRef(
(
{
label,
name,
value,
onChange,
defaultChecked,
errors,
onBlur,
...rest
}: any,
forwardedRef: any
) => {
return (
<div className="grow w-full">
<label htmlFor={name}>{label}</label>
<input
type="checkbox"
value={value}
name={name}
onChange={onChange}
onBlur={onBlur}
ref={forwardedRef}
{...rest}
/>
{errors && <p className="error">{errors.message}</p>}
</div>
);
}
);
Which I use like this:
export default function App() {
const {
handleSubmit,
register,
formState: { errors }
} = useForm({
resolver: yupResolver(schema)
});
const onSubmit = (data: any) => {
console.log("clicked");
console.log(errors, "errors");
alert(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Checkbox
{...register("name", { required: "This is required." })}
name={"name"}
value={"test"}
label={"Test"}
errors={errors.name}
/>
<Checkbox
{...register("name", { required: "This is required." })}
name={"name"}
value={"test"}
label={"Test"}
errors={errors.name}
/>
<button type="submit">Submit</button>
</form>
);
}
When I go to submit my form, I get the following error:
name must be a `array` type, but the final value was: `null` (cast from the value `false`). If "null" is intended as an empty value be sure to mark the schema as `.nullable()`
Here's my sandbox
Here's the code I followed to get me to this stage - to me it looks the same? Can't see where I've gone wrong here
I have the following code which you can find here:
https://stackblitz.com/edit/react-d2fadr?file=src%2FApp.js
import { ErrorMessage, Field, Form, Formik } from 'formik';
import React from 'react';
import { Button } from 'react-bootstrap';
import * as Yup from 'yup';
let fieldName = 'hexColor';
const TextInput = ({ field, value, placeholder, handleChange }) => {
value = (field && field.value) || value || '';
placeholder = placeholder || '';
return (
<input
type="text"
placeholder={placeholder}
onChange={(e) => handleChange(e.target.value)}
value={value}
/>
);
};
export default () => {
const onSubmit = (values, { setSubmitting }) => {
console.log(values);
setSubmitting(false);
};
return (
<Formik
initialValues={{ [fieldName]: 'ff0000' }}
validationSchema={Yup.object({
hexColor: Yup.string().test(
fieldName,
'The Hex Color is Wrong.',
(value) => {
return /^[0-9a-f]{6}$/.test(value);
}
),
})}
onSubmit={onSubmit}
enableReinitialize
>
{(formik) => {
const handleChange = (value) => {
value = value.replace(/[^0-9a-f]/g, '');
formik.setFieldValue(fieldName, value);
};
return (
<Form>
<div>
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
handleChange={handleChange}
/>
<ErrorMessage name={fieldName} />
</div>
<Button
type="submit"
disabled={!formik.isValid || formik.isSubmitting}
>
Submit
</Button>
</Form>
);
}}
</Formik>
);
};
I want to know if is it any way to render the ErrorMessage element automatically?
The error message should be shown somewhere around the input text.
If you know how, you can fork the StackBlitz above with your suggestion.
Thanks!
Don't really know why ErroMessage is not rendering before you submit your form once but you can replace the line <ErrorMessage name={fieldName} /> by {formik.errors[fieldName]} to make it works
import { ErrorMessage, Field, Form, Formik } from 'formik';
import React from 'react';
import { Button } from 'react-bootstrap';
import * as Yup from 'yup';
let fieldName = 'hexColor';
const TextInput = ({ field, value, placeholder, handleChange }) => {
value = (field && field.value) || value || '';
placeholder = placeholder || '';
return (
<input
type="text"
placeholder={placeholder}
onChange={(e) => handleChange(e.target.value)}
value={value}
/>
);
};
export default () => {
const onSubmit = (values, { setSubmitting }) => {
console.log(values);
setSubmitting(false);
};
return (
<Formik
initialValues={{ [fieldName]: 'ff0000' }}
validationSchema={Yup.object({
hexColor: Yup.string().test(
fieldName,
'The Hex Color is Wrong.',
(value) => {
return /^[0-9a-f]{6}$/.test(value);
}
),
})}
onSubmit={onSubmit}
enableReinitialize
>
{(formik) => {
const handleChange = (value) => {
value = value.replace(/[^0-9a-f]/g, '');
formik.setFieldValue(fieldName, value);
};
return (
<Form>
<div>
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
handleChange={handleChange}
/>
{formik.errors[fieldName]}
</div>
<Button
type="submit"
disabled={!formik.isValid || formik.isSubmitting}
>
Submit
</Button>
</Form>
);
}}
</Formik>
);
};
the issue is validation schema. When I changed 6
return /^[0-9a-f]{6}$/.test(value);
to 3
return /^[0-9a-f]{3}$/.test(value);
and submitted with the initial value, ErrorMessage component is rendered
To reach your goal, I changed your code as below:
Since Formik's default component is Input, I deleted your TextInput component as there was nothing special in your component and handleChange function.
<Field name="hexColor" placeholder="Hex Color" onChange={(e) => handleChange(e.target.value)}/>
<ErrorMessage name="hexColor" />
As in mentioned in this answer, I changed your submit button condition to determine whether the button is disabled or not:
<Button type="submit" disabled={Object.keys(errors).length}>
Submit
</Button>
You can view my entire solution here.
Edit
If you want to keep your component, you should pass props as you might be missing something important, e.g. onChange,onBlur etc.
const TextInput = ({ field, ...props }) => {
return (
<input
{...field} {...props}
// ... your custom things
/>
);
};
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
onChange={(e) => handleChange(e.target.value)}
/>
Solution 2
I am working on a react where I am dealing with dynamic number of input fields as I am rendering them using arr.map function. But How can I handle the input onChange method with so many input fields?
Here's my component:
this.props.setsList.map((codeset, index) => (
<Table.Row key={codeset.code_system_id}>
<Table.Cell>{index + 1}</Table.Cell>
<Table.Cell>{codeset.name}</Table.Cell>
<Table.Cell>
<Input
type='text'
className='form-control'
value={codeset.code}
placeholder={translateText('Code')}
onChange={() => this.handleCodeChange(event, index)}
style={{ height: '70%' }}
/>
</Table.Cell>
<Table.Cell>
<Input
type='text'
className='form-control'
value={codeset.description}
placeholder={translateText('Code Description')}
onChange={() => this.handleCodeDescriptionChange(event, index)}
style={{ height: '70%' }}
/>
</Table.Cell>
</Table.Row>
All the input fields may have existing fields or may be empty and one can edit it them semd the edit fields to the API. How can I handle such a case with just 1 handleFunction. Is there a way? Any leads will be appreciated.
In the above code, 2 input boxes are there with initial values from the api.
Data passed into your component should go directly into state. Then each field sends the array index, field name and new value to the handleChange callback.
import { useState } from "react";
// passed in as a prop from the parent component
const data = [
{
name: "foo",
code: "gdfgsd"
},
{
name: "bar",
code: "gfdsgsdfgfd"
}
];
const App = ({ setsList = data }) => {
const [state, setState] = useState(setsList);
const handleChange = (e, i) => {
const { value, name } = e.target;
const newState = [...state];
newState[i] = {
...newState[i],
[name]: value
};
console.log(newState);
setState(newState);
};
return (
<div className="App">
{state.map(({ name, code }, index) => {
return (
<div key={index}>
<label>
name
{": "}
<input
name="name"
value={name}
onChange={(e) => handleChange(e, index)}
/>
</label>
<label>
code
{": "}
<input
name="code"
value={code}
onChange={(e) => handleChange(e, index)}
/>
</label>
</div>
);
})}
</div>
);
};
export default App;
There's also a way that you can render each field dynamically by mapping over the array, and then inner mapping over that array item's keys.
import { useState } from "react";
import { data } from "./data";
const App = ({ setsList = data }) => {
const [state, setState] = useState(setsList);
const handleChange = (e, i) => {
const { value, name } = e.target;
const newState = [...state];
newState[i] = {
...newState[i],
[name]: value
};
console.log(newState);
setState(newState);
};
return (
<table className="App">
<tbody>
{state.map((item, index) => (
<tr key={index}>
{Object.keys(item).map((key) => (
<td key={`${index}-${key}`}>
<label>
{key}
{": "}
<input
name={key}
value={item[key]}
onChange={(e) => handleChange(e, index)}
/>
</label>
</td>
))}
</tr>
))}
</tbody>
</table>
);
};
export default App;
I'm trying to add an extra input field if user clicks add input button and store those values as an object:
const {useState } = React;
const InviteUser = () => {
const [userDetails, setUserDetails] = useState([
{
email: "",
role: ""
}
]);
const handleCancel = () => {
setUserDetails([
{
email: "",
role: ""
}
]);
};
const handleChange = (e, index) => {
const { name, value } = e.target;
let newUserDetails = [...userDetails];
newUserDetails[index] = { ...userDetails[index], [name]: value };
setUserDetails({ newUserDetails });
};
const addInput = () => {
setUserDetails([...userDetails, { email: "", role: "" }]);
};
return (
userDetails.length >= 0 && (
<div>
{[...userDetails].map((el, index) => (
<form key={index + el} name={el}>
<div>
<input
type="email"
name="email"
required
onChange={(event) => handleChange(event, index)}
value={el.email}
style={{ marginTop: 17 }}
placeholder="Enter Email Address"
/>
</div>
<div>
<input
type="text"
name="roles"
required
value={el.role}
style={{ marginTop: 17 }}
placeholder="Role"
onChange={(event) => handleChange(event, index)}
/>
</div>
</form>
))}
<div>
<button label="Invite Another?" onClick={addInput}>
Add input
</button>
</div>
<div>
<button>Invite</button>
<button onClick={() => handleCancel()}>Cancel</button>
</div>
</div>
)
);
};
ReactDOM.render(
<InviteUser/>,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
A state is an array of objects that holds email and role:
const [userDetails, setUserDetails] = useState([
{
email: "",
role: ""
}
]);
handleChange function which updates the state by pushing an object (of users inputs values from each individual row) into an array:
const handleChange = (e, index) => {
const { name, value } = e.target;
let newUserDetails = [...userDetails];
newUserDetails[index] = { ...userDetails[index], [name]: value };
setUserDetails({ newUserDetails });
};
and addInput function adds new input fields row:
const addInput = () => {
setUserDetails([...userDetails, { email: "", role: "" }]);
};
And return JSX:
return (
userDetails.length >= 0 && (
<InputsContainer>
{[...userDetails].map((el, index) => (
<form key={index + el} name={el} onSubmit={handleSubmit}>
<EmailContainer>
<input
type="email"
name="email"
required
onChange={(event) => handleChange(event, index)}
value={el.email}
style={{ marginTop: 17 }}
placeholder="Enter Email Address"
/>
</EmailContainer>
<RolesContainer>
<input
type="text"
name="roles"
required
value={el.role}
style={{ marginTop: 17 }}
placeholder="Role"
onChange={(event) => handleChange(event, index)}
/>
</RolesContainer>
</form>
))}
<InviteButtonContainer>
<button label="Invite Another?" onClick={addInput}>
Add input
</button>
</InviteButtonContainer>
<SubmitButtonsContainer>
<button onClick={(event) => handleSubmit(event)}>Invite</button>
<button onClick={() => handleCancel()}>Cancel</button>
</SubmitButtonsContainer>
</InputsContainer>
)
);
What am I doing wrong here?
You've got two problems:
In setUserDetails you return the array wrapped in an object, instead of just returning the array.
In the roles field you take the value from el.role insted of el.roles.
In addition, since you update the current state, based on a previous one, it's better to use functional updates.
const {useState } = React;
const createNewEntry = () => ({ email: "", role: "" });
const InviteUser = () => {
const [userDetails, setUserDetails] = useState(() => [createNewEntry()]);
const handleCancel = () => {
setUserDetails([createNewEntry()]);
};
const handleChange = (e, index) => {
const { name, value } = e.target;
setUserDetails(userDetails => {
const newUserDetails = [...userDetails];
newUserDetails[index] = { ...userDetails[index], [name]: value };
return newUserDetails;
});
};
const addInput = () => {
setUserDetails([...userDetails, createNewEntry()]);
};
return (
userDetails.length >= 0 && (
<div>
{[...userDetails].map((el, index) => (
<form key={index + el} name={el}>
<div>
<input
type="email"
name="email"
required
onChange={(event) => handleChange(event, index)}
value={el.email}
style={{ marginTop: 17 }}
placeholder="Enter Email Address"
/>
</div>
<div>
<input
type="text"
name="roles"
required
value={el.roles}
style={{ marginTop: 17 }}
placeholder="Role"
onChange={(event) => handleChange(event, index)}
/>
</div>
</form>
))}
<div>
<button label="Invite Another?" onClick={addInput}>
Add input
</button>
</div>
<div>
<button>Invite</button>
<button onClick={() => handleCancel()}>Cancel</button>
</div>
</div>
)
);
};
ReactDOM.render(
<InviteUser/>,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I have a dynamic array of form fields, whose values are fetched via REST API. On the page, there is also a dropdown, that, when changed, shows a different array of fields. I fetch all of these fields/values during the componentDidMount life cycle hook and filter the list to show the relevant data.
The Formik docs mention FieldArrays as a means to handle an array of fields. However, their example shows a static list of objects as its initialValues -- but I don't see how dynamically generated lists. In fact, since I'm fetching initialValues via AJAX, it's initially an empty array -- so nothing is rendered even after getting the data.
This is simplified version of my code:
const MyComponent = class extends Component {
componentDidMount() {
// data structure: [{Name: '', Id: '', Foo: '', Bar: ''}, ...]
axios
.get('/user')
.then((res) => {
this.setState({
userData: res.data
});
});
}
render() {
return (
<div>
<Formik
initialValues={{
users: this.state.userData
}}
render={({values}) => (
<Form>
<FieldArray
name="users"
render={arrayHelpers => (
<ul>
{
values.users.map((user, index) => {
return (
<li key={user.Id}>
<div>{user.Name}</div>
<Field name={`user[${index}].Foo`} type="text" defaultValue={user.Foo} />
<Field name={`user[${index}].Bar`} type="text" defaultValue={user.Bar} />
</li>);
}
}
</ul>
)}
/>
</Form>
)}
/>
</div>
);
}
}
You can do this via setting enableReinitialize true. According to doc it will do this:
Default is false. Control whether Formik should reset the form if initialValues changes (using deep equality).
I created complete codesanbox where your incoming data is async and when you push the data its also async. check this:
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import { Formik, Field, Form, ErrorMessage, FieldArray } from "formik";
const InviteFriends = () => {
const [initialValues, setInitialValues] = React.useState({
friends: []
});
useEffect(() => {
const initialValues = {
friends: [
{
name: "",
email: ""
}
]
};
const timer = setTimeout(() => {
setInitialValues(initialValues);
}, 1000);
return () => {
timer && clearTimeout(timer);
};
}, []);
return (
<div>
<h1>Invite friends</h1>
<Formik
initialValues={initialValues}
enableReinitialize={true}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
alert(JSON.stringify(values, null, 2));
}}
>
{({ values }) => (
<Form>
<FieldArray name="friends">
{({ insert, remove, push }) => (
<div>
{console.log("Values", values, initialValues)}
{values.friends.length > 0 &&
values.friends.map((friend, index) => (
<div className="row" key={index}>
<div className="col">
<label htmlFor={`friends.${index}.name`}>Name</label>
<Field
name={`friends.${index}.name`}
placeholder="Jane Doe"
type="text"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<label htmlFor={`friends.${index}.email`}>
Email
</label>
<Field
name={`friends.${index}.email`}
placeholder="jane#acme.com"
type="email"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<button
type="button"
className="secondary"
onClick={() => remove(index)}
>
X
</button>
</div>
</div>
))}
<button
type="button"
className="secondary"
onClick={async () => {
await new Promise((r) =>
setTimeout(() => {
push({ name: "", email: "" });
r();
}, 500)
);
}}
>
Add Friend
</button>
</div>
)}
</FieldArray>
<button type="submit">Invite</button>
</Form>
)}
</Formik>
</div>
);
};
ReactDOM.render(<InviteFriends />, document.getElementById("root"));
Here is the demo: https://codesandbox.io/s/react-formik-async-l2cc5?file=/index.js