I have a form with a fields array. At the bottom of the form there is a button that deletes all list items (in fact, it does form.restart).
When deleting fields, the validation of each field is triggered, but the value of the field is undefined. This breaks the validation logic.
In theory, this validation should not exist at all.
How to get rid of this validation on form.restart?
Code:
(https://codesandbox.io/s/festive-water-6u3t69?file=/src/App.js:0-1477)
import React from "react";
import { Form, Field } from "react-final-form";
import { FieldArray } from "react-final-form-arrays";
import arrayMutators from "final-form-arrays";
import "./styles.css";
export default function App() {
const [values] = React.useState({ items: [] });
const renderForm = ({ form }) => {
return (
<div>
<FieldArray name={"items"}>
{({ fields }) => (
<div>
{fields.map((name, index) => (
<div key={name}>
<Field
name={`${name}.title`}
validate={(value) => console.log(value)}
key={name}
>
{({ input }) => (
<input
value={input.value}
onChange={(event) => input.onChange(event.target.value)}
/>
)}
</Field>
</div>
))}
<button onClick={() => fields.push({ title: "111" })}>
add item
</button>
<button onClick={() => form.restart({ items: [] })}>reset</button>
</div>
)}
</FieldArray>
</div>
);
};
return (
<div className="App">
<Form
onSubmit={() => {}}
initialValues={values}
render={renderForm}
mutators={{ ...arrayMutators }}
/>
</div>
);
}
Thank you.
Related
I have a form that uses accordion component.
When I print values using watch() then collapse accordion. the values get deleted from inputs when I open it again.
This behaviour is not happening when I don't use watch()
I would like to know why this is happening ? watch() should only listen to data as I know.
CodeSandbox
CreateTest.ts
import { QuestionCreatingForm } from "./QuestionForm";
import {
AccordionHeader,
AccordionItem,
AccordionPanel,
Accordion
} from "#fluentui/react-components";
import { Button, Form } from "#fluentui/react-northstar";
import { useFieldArray, useForm } from "react-hook-form";
export function CreateTest() {
const methods = useForm();
const { control, register, handleSubmit, watch } = methods;
const { fields, append } = useFieldArray({
control,
name: "questions"
});
const addQuestion = (event: any) => {
event.preventDefault();
append({ name: "" });
};
const onSubmit = (data: any) => {
alert(JSON.stringify(data));
};
return (
<div className="w-8/12 m-auto">
{JSON.stringify(watch())}
<Form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<Accordion key={field.id} collapsible>
<AccordionItem value="1">
<AccordionHeader>Accordion Header </AccordionHeader>
<AccordionPanel>
<QuestionCreatingForm
fieldId={field.id}
index={index}
{...{ control, register, watch }}
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
))}
<Button
className="my-10"
content="Add question"
primary
fluid
onClick={addQuestion}
/>
<Button
className="my-10"
fluid
content="submit"
primary
type="submit"
/>
</Form>
{/* </FormProvider> */}
</div>
);
}
QuestionForm.ts
import {
Button,
Divider,
FormCheckbox,
FormInput,
TrashCanIcon
} from "#fluentui/react-northstar";
import { SyntheticEvent } from "react";
import {
Control,
FieldValues,
useFieldArray,
UseFormRegister,
UseFormWatch
} from "react-hook-form";
export function QuestionCreatingForm({
index,
fieldId,
control,
register,
watch
}: {
index: number;
fieldId: string;
control: Control<FieldValues, any>;
register: UseFormRegister<FieldValues>;
watch: UseFormWatch<FieldValues>;
}) {
const { fields, append, remove } = useFieldArray({
control,
name: `questions.${index}.responses`
});
const addResponse = (event: SyntheticEvent<HTMLElement, Event>) => {
event.preventDefault();
append({ name: "" });
};
const deleteResponse = (index: number) => {
remove(index);
};
return (
<>
<FormInput
label="Question"
required
fluid
key={index}
{...register(`questions.${index}.name` as const)}
/>
<div className="w-10/12 m-auto">
{fields.map((field, i) => (
<div className="flex w-full">
<FormCheckbox />
<div className="w-full" key={field.id}>
<FormInput
{...register(`questions.${index}.responses.${i}.name` as const)}
defaultValue=""
label={`reponses ${i + 1}`}
required
fluid
/>
</div>
<Button
text
styles={{ color: "red", placeSelf: "end" }}
icon={<TrashCanIcon />}
onClick={(e) => deleteResponse(i)}
iconOnly
/>
</div>
))}
<Button
content="Ajouter une réponse"
tinted
fluid
onClick={addResponse}
/>
</div>
<Divider />
</>
);
}
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 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
GraphQL Query Defined here
const countryQuery = gql`
query getQuery($filter: _CountryFilter!) {
Country(filter: $filter) {
_id
name
capital
population
nativeName
}
}
`
pages/search.js
export default function Search() {
const [filter, setFilter] = useState({ name: 'Chile' })
const { data, loading, error } = useQuery(countryQuery, {
variables: { filter },})
if (loading){
return <Loader />;
}
if (error) return <p>Error</p>;
return (
<div className="body">
<h1>
Get Information
<br /> about Countries!
</h1>
<div className="wrapper">
<input
className="search"
type="text"
id="search"
placeholder='Enter a Country'
/>
<button
className="submit"
type="submit"
value=" "
onClick={e => setFilter({ name: e.target.value })}
onBlur={e => setFilter({ name: e.target.value })}
> Search
</button>
<div>
{ data?.Country && <CountryInfo country={data?.Country[0]} /> }
</div>
</div>
</div>
);
}
components/queryResults.js This is were I am getting the error. Is {country.name}, {country.capital} etc. the incorrect way to apply the data here?
import React from 'react'
import { Card, ListGroup, ListGroupItem } from 'react-bootstrap'
const CountryInfo = ({country}) => (
<div>
<Card style={{ width: '18rem' }}>
<Card.Body>
<Card.Title>{country.name}</Card.Title>
</Card.Body>
<ListGroup className="list-group-flush">
<ListGroupItem>Capital: {country.capital} </ListGroupItem> {' '}
<ListGroupItem>Population: {country.population}</ListGroupItem>
<ListGroupItem>Native Name: {country.nativeName}</ListGroupItem>
</ListGroup>
</Card>
</div>
)
export default CountryInfo;
When I type a country and click the search the error is happening on this line: <Card.Title>{country.name}</Card.Title> Why is name, capital, population and nativeName undefined? What am I doing wrong?
I'm looking at this line and would like to note that if data.country is an empty array, it will still be truthy. Add a check to see if there is a value in the first element of the array before rendering CountryInfo
{ data?.Country && <CountryInfo country={data?.Country[0]} /> }