I have a child component which receives an array with some string properties and a component
const FormWizardItems = [
{
title: "Organization Data",
number: 1,
component: () => (
<CustomerOrganizationDataTabContentFrm validation={validation} />
),
}
]
return (
<>
<FormWizard
validation={validation}
apiCallResponse={registrationError}
breadcrumbTitle="Clientes"
breadcrumbItemTitle="Register Customer"
cardTitle="Clientes"
cardDescription="Register Customer"
items={FormWizardItems}
/>
</>
)
The FormWizard component internally passes each component to another component (FormWizardContent) which renders all the components from the received array
<CardBody>
<h4 className="card-title mb-4">{cardTitle}</h4>
<div className="wizard clearfix">
<div className="steps clearfix">
<ul>
{items.map(item => {
return (
<FormWizardNavItem
key={item.number}
number={item.number}
title={item.title}
activeTab={activeTab}
passedSteps={passedSteps}
setActiveTab={setActiveTab}
/>
)
})}
</ul>
</div>
<div className="content clearfix">
<TabContent activeTab={activeTab} className="body">
{items.map(item => {
const Component = item.component
return (
<FormWizardContent
key={item.number}
tabId={item.number}
activeTab={activeTab}
component={() => (
<Component exampleProp="Hello" />
)}
/>
)
})}
</TabContent>
</div>
FormWizardContent Component
import React from "react"
import { TabPane } from "reactstrap"
export const FormWizardContent = props => {
const { tabId, component: Component } = props
return (
<TabPane tabId={tabId}>
<Component />
</TabPane>
)
}
The Form it renders (CustomerOrganizationDataTabContentFrm) is a form with a dependency prop (validation) coming from parent Component (CustomerFrm)
const validation = useFormik({
enableReinitialize: true,
initialValues: {
BusinessName: "",
TradingName: "",
EinItin: "",
StateEin: "",
OtherTaxPayerStateEin: "",
CityEin: "",
CompanyCategory: "",
HeadquarterId: null,
IsActive: true,
},
validationSchema: Yup.object({
BusinessName: Yup.string()
.required("Business Name is required")
.min(3, "Business Name should have at minimun of 3 characters")
.max(3, "Business Name should have up to 3 characters")
TradingName: Yup.string()
.required("Trading Name is required")
.min(3, "Trading Name should have at minimun of 3 characters")
.max(3, "Trading Name should have up to 3 characters"),
}),
onSubmit: values => {
dispatch(registerRole(values))
},
})
The problem is that when i type in the input it only allows one character and loses focus, i can't manage it to work properly, any help would be appreciated. Thanks in advance!
I've found the problem, the reason was the anonymous render functions in these two parts of the code
const FormWizardItems = [
{
title: "Organization Data",
number: 1,
component: () => (
<CustomerOrganizationDataTabContentFrm validation={validation} />
),
}
]
<TabContent activeTab={activeTab} className="body">
{items.map((item) => {
const Component = item.component;
return (
<FormWizardContent
key={item.number}
tabId={item.number}
activeTab={activeTab}
component={() => <Component exampleProp="Hello" />}
/>
);
})}
</TabContent>;
According to what I've seen, whenever there was some state changes a new form was generated and rendered instead of reender the existing instance.
Solved it this way
[{
title: "Organization Data",
number: 1,
component: CustomerOrganizationDataTabContentFrm,
}]
<TabContent activeTab={activeTab} className="body">
{items.map(item => {
return (
<FormWizardContent
key={item.number}
tabId={item.number}
activeTab={activeTab}
validation={validation}
component={item.component}
/>
)
})}
Related
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.
I have the following component:
const WorkProviderList: React.FC<StepComponentParams> = ({
metadata,
}): JSX.Element => {
const {
control,
setValue,
getValues,
} = useForm({
resolver: yupResolver(validationSchema),
defaultValues: { [CONFIRM_NAME]: true } as any,
mode: "all",
});
...
const isOtherFieldNameVisible = getValues()[FIELD_NAME] === "other";
console.log("isOtherFieldNameVisible: ", isOtherFieldNameVisible);
return (
<>
<Text element="h1" variant="title2Bold">
Which is your work provider?
</Text>
<Text>
If you work with more than one, select the provider that you work with
the most.
</Text>
<fieldset role="group" aria-labelledby={FIELD_NAME}>
<Controller
name={FIELD_NAME}
control={control}
render={() => (
<RadioGroupFields
fields={radioFields}
name={FIELD_NAME}
onChange={(e) => {
setValue(FIELD_NAME, e.target.value, {
shouldValidate: true,
});
}}
/>
)}
/>
</fieldset>
{isOtherFieldNameVisible && (
<div className="py-3">
<FormField
name={OTHER_FIELD_NAME}
label="What is the name of your work provider?"
>
<Controller
name={OTHER_FIELD_NAME}
control={control}
render={({ field: { onChange } }) => (
<TextField
id="other_input"
isFullWidth
name={OTHER_FIELD_NAME}
statusType="info"
onChange={onChange}
/>
)}
/>
</FormField>
</div>
)}
</>
);
};
and the following test:
const { getByLabelText, getByText } = render(
<WorkProviderList {...props} />
);
const input = getByLabelText("I work with a different work provider");
fireEvent.change(input, { target: { value: "other" } });
fireEvent.click(input);
const text = getByText("What is the name of your work provider?");
expect(text).toBeInTheDocument();
even though the console.log in the component returns true, I would expect to find the label I am looking for but I get the following error:
TestingLibraryElementError: Unable to find a label with the text of:
What is the name of your work provider?
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
I have a form made with formik, in which one of the information entered is the contact, however the user can have more than one contact.
I want to add a button that when clicking, new fields for entering more contacts appear.
I've tried some logic, but to no avail
const { getFieldProps, touched, errors, isValid, handleSubmit } = useFormik({
initialValues: {
phone_name: '',
phone_ddd: '',
phone_number: '',
phone_observation: ''
},
onSubmit: (values, actions) => {
saveOrganization({
variables: {
name: values.phone_name,
ddd: values.phone_ddd,
number: values.phone_number,
observation: values.phone_observation,
}
})
})
return (
<TextField
margin="dense"
id="phone_name"
label={<IntlMessages id="name" />}
fullWidth
{...getFieldProps("phone_name")}
/>
{touched.phone_name && errors.phone_name ? (
<small>{errors.phone_name}</small>
) : null}
<InputMask
mask="99"
disabled={false}
maskChar=" "
{...getFieldProps("phone_ddd")}
>
{() =>
<TextField
label={<IntlMessages id='ddd' />}
fullWidth
{...getFieldProps("phone_ddd")}
/>
}
</InputMask>
{touched.phone_ddd && errors.phone_ddd ? (
<small>{errors.phone_ddd}</small>
) : null}
<InputMask
mask="999999999"
disabled={false}
maskChar=" "
{...getFieldProps("phone_number")}
>
{() =>
<TextField
label={<IntlMessages id='number' />}
fullWidth
{...getFieldProps("phone_number")}
/>
}
</InputMask>
{touched.phone_number && errors.phone_number ? (
<small>{errors.phone_number}</small>
) : null}
I want to add a button and new inputs appear
This is my approach. First I declare initialValues to have field 'contact' as array like this
useFormik({
initialValues: {
contact: [
{
name: "",
age: "",
},
],
},
});
Then I create a function for adding new field
const handleNewField = () => {
formik.setFieldValue("contact", [
...formik.values.contact,
{ name: "", age: "" },
]);
};
And in the render just map it out from array like this
<form onSubmit={formik.handleSubmit}>
{formik.values.contact.map((contact, index) => (
<div key={index}>
<label>Name</label>
<input {...formik.getFieldProps(`contact[${index}].name`)} />
<label>Age</label>
<input {...formik.getFieldProps(`contact[${index}].age`)} />
</div>
))}
<button type="submit">Submit</button>
<button type="button" onClick={handleNewField}>
New Field
</button>
</form>
I have try this and this is work just fine for me. If you have any question feel free to ask me I'll try my best to answer
Final code look like this
import React from "react";
import { useFormik } from "formik";
function App() {
const formik = useFormik({
initialValues: {
contact: [
{
name: "",
age: "",
},
],
},
onSubmit: (values) => {
console.log(values);
},
});
const handleNewField = () => {
formik.setFieldValue("contact", [
...formik.values.contact,
{ name: "", age: "" },
]);
};
return (
<div>
<form onSubmit={formik.handleSubmit}>
{formik.values.contact.map((contact, index) => (
<div key={index}>
<label>Name</label>
<input {...formik.getFieldProps(`contact[${index}].name`)} />
<label>Age</label>
<input {...formik.getFieldProps(`contact[${index}].age`)} />
</div>
))}
<button type="submit">Submit</button>
<button type="button" onClick={handleNewField}>
New Field
</button>
</form>
</div>
);
}
export default App;
I'm trying to render out a list of titles by calling map on state from inside the <MyContext.Consumer> component but the code is not returning the list items. However, if I do a console.log(dataImTryingToMap); it shows exactly what I'm trying to render. The <ul> is rendered but no <li>'s are. There are no errors thrown by create-react-app. What am I missing??? Here is the consumer component:
<MyContext.Consumer>
{context => (
<ul>
{Object.keys(context.state.subjects).map(subject => {
<li>{context.state.subjects[subject].title}</li>;
console.log(context.state.subjects[subject].title);
})}
</ul>
)}
</MyContext.Consumer>
The console log returns exactly the data I'm looking for, but nothing shows on the screen.
And here is the state from the <MyContext.MyProvider> component:
state = {
subjects: {
subject0: {
title: "Math",
description: "",
cards: {
card1: {
note: "",
answer: ""
}
}
},
subject1: {
title: "history",
description: "",
cards: {
card1: {
note: "",
answer: ""
}
}
}
}
};
Thanks for any help!
You are not returning anything from the Array.map() callback:
{Object.keys(context.state.subjects).map(subject => {
<li>{context.state.subjects[subject].title}</li>; <-- this is not returned
console.log(context.state.subjects[subject].title);
})}
const contextValue = { state: {"subjects":{"subject0":{"title":"Math","description":"","cards":{"card1":{"note":"","answer":""}}},"subject1":{"title":"history","description":"","cards":{"card1":{"note":"","answer":""}}}}}};
const MyContext = React.createContext();
const Example = () => (
<MyContext.Consumer>
{context => (
<ul>
{Object.keys(context.state.subjects).map(subject => {
console.log(context.state.subjects[subject].title);
return (
<li key={subject}>{context.state.subjects[subject].title}</li>
);
})}
</ul>
)}
</MyContext.Consumer>
);
const Demo = ({ value }) => (
<MyContext.Provider value={value}>
<Example />
</MyContext.Provider>
);
ReactDOM.render(
<Demo value={contextValue} />,
demo
);
<script crossorigin src="https://unpkg.com/react#16.3/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16.3/umd/react-dom.development.js"></script>
<div id="demo">
</demo>
You haven't returned anything from inside the map function and hence it doesn't render anything. Either return it explicitly like
<MyContext.Consumer>
{context => (
<ul>
{Object.keys(context.state.subjects).map(subject => {
return <li>{context.state.subjects[subject].title}</li>;
})}
</ul>
)}
</MyContext.Consumer>
or implicitly like
<MyContext.Consumer>
{context => (
<ul>
{Object.keys(context.state.subjects).map(subject => (
<li>{context.state.subjects[subject].title}</li>;
))}
</ul>
)}
</MyContext.Consumer>