I want to render a Form with nested fields inside a <FieldArray /> component. But when I create the Form Fields based on the index, I get extra fields that I don't want. As shown below:
As you can see Julia and 28 should be on the same row. But instead, I get four fields in two rows. Empty fields are also writing the age and name values when typed. I don't get why this is happening. But I don't want them. Below you can see the code for the component. I also created a sandbox to work on it in here codesandbox.
Note: I want these nested Fields so the structure of my array friends: [{ name: "Julia" }, { age: "28" }] is important to the question.
import React from "react";
import { Formik, Form, Field, FieldArray } from "formik";
// Here is an example of a form with an editable list.
// Next to each input are buttons for insert and remove.
// If the list is empty, there is a button to add an item.
const FriendList = () => (
<div>
<h1>Friend List</h1>
<Formik
initialValues={{ friends: [{ name: "Julia" }, { age: "28" }] }}
onSubmit={values =>
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
}, 500)
}
render={({ values }) => (
<Form>
<FieldArray
name="friends"
render={arrayHelpers => (
<div>
{values.friends.map((friend, index) => (
<div key={index}>
<Field name={`friends[${index}].name`} />
<Field name={`friends.${index}.age`} />
<button
type="button"
onClick={() => arrayHelpers.remove(index)}
>
-
</button>
</div>
))}
<button
type="button"
onClick={() => arrayHelpers.push({ name: "", age: "" })}
>
+
</button>
</div>
)}
/>
<pre>{JSON.stringify(values, null, 2)}</pre>
</Form>
)}
/>
</div>
);
export default FriendList;
You have wrong initial values, should be like:
[{ name: "Julia", age: "27" }]
Instead you passed 2 array items
Related
I want to add 4 empty inputText fields once button Add Person is clicked, which then needs to be filled and update values property. Those empty inputs supposed to be added to values.persons property which is an array of objects once addPerson()is called. I have some logic inside, but unfortunately nothing happens with this code
const data = [
{
luxmedType: {
package: "Indywidualny",
type: "Opieka standardowa",
cost: 0
},
companyCost: 91.6,
comment: null,
persons: [
{
name: "Marian",
lastName: "Kowalski",
type: "współpracownik",
cooperationForm: "B2B"
}
]
},
{
luxmedType: {
package: "Rodzinny",
type: "Opieka premium",
cost: 559.1
},
companyCost: 0,
comment: null,
persons: [
{
name: "Ewa",
lastName: "Kowalska",
type: "partner",
cooperationForm: null
},
{
name: "Maria",
lastName: "Kowalska",
type: "dziecko",
cooperationForm: null
}
]
},
{
luxmedType: {
package: "osobisty",
type: "Opieka premium",
cost: 1000
},
companyCost: 0,
comment: null,
persons: [
{
name: "Anna",
lastName: "Michalska",
type: "partner",
cooperationForm: null
},
{
name: "Maria",
lastName: "Michalska",
type: "dziecko",
cooperationForm: null
},
{
name: "Aleksander",
lastName: "Michalski",
type: "dziecko",
cooperationForm: null
}
]
}
];
const initialValues = data;
export default function App() {
const { values, handleChange, onSubmit, setValues } = useFormik({
initialValues,
onSubmit: (values) => console.log(values)
});
const addPerson = () => {
const persons = [...values.persons];
persons.push({
name: "",
lastName: "",
type: "",
cooperationForm: null,
id: Math.random()
});
setValues({ ...values,persons });
};
return (
<div>
<h1>Luxmed</h1>
<form onSubmit={onSubmit}>
{initialValues.map((object) => {
let luxmedType = object.luxmedType,
companyCost = object.companyCost,
comment = object.comment,
persons = object.persons;
return (
<div style={{ marginBottom: "20px" }}>
<InputText
name="package"
value={luxmedType.package}
onChange={handleChange}
/>
<InputText
name="type"
value={luxmedType.type}
onChange={handleChange}
/>
<InputText
name="cost"
value={luxmedType.cost}
onChange={handleChange}
/>
<InputText
name="companyCost"
value={companyCost}
onChange={handleChange}
/>
<InputText
name="companyCost"
value={comment || ""}
onChange={handleChange}
/>
{persons.map((person) => {
return (
<div>
<InputText
name="package"
value={person.name}
onChange={handleChange}
/>
<InputText
name="package"
value={person.lastName}
onChange={handleChange}
/>
<InputText
name="package"
value={person.type}
onChange={handleChange}
/>
<InputText
name="package"
value={person.cooperationForm}
onChange={handleChange}
/>
<button>Remove</button>
</div>
);
})}
</div>
);
})}
<button onClick={() => addPerson()}>Add Person</button>
<button type="submit">submit</button>
</form>
</div>
);
}
Here is also codesandbox for You if You wish
https://codesandbox.io/s/serene-bose-x6qk9y?file=/src/App.js:1552-1553&fbclid=IwAR3Q6bnkVyCtox5hTCHaFibtKJ92huwkSDVHKPOSiTdoA743lURBq76Abq0
Thanks
You should iterate (map) values instead of initialValues
Every mapped item should have unique key (every object in value array as well as every person in persons array). Consider adding unique keys to objects itself.
Also you can pass reference to addPerson function instead of anonymous function onClick={addPerson} and in addPerson you will receive event as an argument and you should add event.preventDefault() in your function body to prevent re-loading of page when you click you button
Instead of using setValue and passing updated values as an object, use setValue with callback function setValue(oldValues => {/*...Do you update here and return updated values */}) when you need previous values in state update. This will guarantee that most up to date state is used.
Your values are not an object with person property of type array, but array of object each having property persons as an array. So I believe you want to add new object to an array of objects. Do not mutate current array, instead copy existing one. Then push new object with all properties luxmedType, companyCost, comment, persons. Push new person to newly added objects persons array.Then setValue with updated array of objects (use callback update as in 4th step).
Please try to correct code yourself and ask questions if something unclear so community can give you more hints and suggestions if you stuck.
<Form
layout="vertical"
size="medium"
className="test-form"
requiredMark={false}
onFinish={onFinish}
>
<Form.Item
name="companyId"
label="Company/Customer"
rules={[{ required: true, message: "Please select Company!"}]}
>
<Select
onChange={this.handleSelectCompanyOnchange}
style={{ width: "50%" }}
name="companyId"
>
{users.map((user, index) => {
return (
<Option key={index} value={user.companyID}>
{user.companyName}
</Option>
);
})}
</Select>
</Form.Item>
<Form.Item
label="Products"
name="products"
rules={[{ required: true, message: "Please select Products!"}]}
>
<Select mode="multiple" allowClear style={{ width: "70%" }}>
{products.map((product, index) => {
if (this.state.companyId == product.companyId) {
return (
<Option key={index} value={product.id}>
{product.productName}
</Option>
);
}
})}
</Select>
</Form.Item>
</Form>
I am trying to achieve Options in Products Select element changes according to the Company Select onChange selection.
I have specified onChange in Select and calling this.handleSelectCompanyOnchange. In which I get selected companyId.
In this.state.companyId I had set companyId manually which I will remove.
I am really new to ant design and not able to figure out how to update the Products list once Company is selected.
Here, users and products are json as below.
users:
[{
companyID: 2
companyName: "TEST1"
},{
companyID: 7
companyName: "TEST2"
}]
products:
[{
companyId: 2
id: 1
productName: "TESTProduct1"
},{
companyId: 7
productName: "TESTProduct2"
id: 2
},{
companyId: 7
id: 3
productName: "TESTProduct3"
},{
companyId: 7
id: 4
productName: "TESTProduct4"
}]
However, I have tried getValueFromEvent but not able to achieve this. I am using Ant design Form and Select for this. Also I did referred to https://github.com/ant-design/ant-design/issues/4862 and how to get field value on change for FormItem in antd
Here is what you need to achieve it.
Use onValuesChange prop of the Form. This is the best place to perform setState when it comes to antd Form field changes, not on Select or Input onChange.
<Form onValuesChange={handleFormValuesChange}>
...
</Form>
A form instance (hook). This is optional in your case, but this is useful on setting and getting form values. See more here about it.
const [form] = Form.useForm();
<Form form={form} onValuesChange={handleFormValuesChange}>
...
</Form>
This is the product options render looks like, a combination of map and filter where selectedCompanyId comes from state. Take note that don't use index as key if the fixed length of the list is unknown, the react will confuse on this and you will get some logical error. Use some unique id.
<Form.Item label="Products" name="product">
<Select>
{products
.filter((product) => product.companyId === selectedCompanyId)
.map((product) => (
<Option key={product.id} value={product.id}>
{product.productName}
</Option>
))}
</Select>
</Form.Item>
And here is the handleFormValuesChange
const handleFormValuesChange = (changedValues) => {
const formFieldName = Object.keys(changedValues)[0];
if (formFieldName === "company") {
setSelectedCompanyId(changedValues[formFieldName]); // perform setState here
form.setFieldsValue({product: undefined}) //reset product selection
}
};
Here is the complete working code in react hooks:
the idea here is that you only need to watch the value change in the state.
For example, your select should "watch" a value of state and then you can easily update the state via its own setState method of React. Inside of an onChange event, you can simply just do something with our state.
<Select
value={state.productId}
onChange={e => {// Do something}}
>
{...}
<Select/>
Below is my example code how did I update the day every time I reselect week selection. Hopefully that this can help you.
import { Divider, Select } from 'antd';
import React, { useState } from 'react';
export function Example(): JSX.Element {
const [state, setState] = useState<{week: number, day: number}>({
day: 1,
week: 1
});
const weeks = [1,2,3,4];
const days = [1,2,3,4,5];
return <>
<Select
value={state.week}
onChange={(value) => setState({ week: value, day: 1})}
>
{
weeks.map(week => {
return <Select.Option
key={week}
value={week}
>
{week}
</Select.Option>;
})
}
</Select>
<Divider/>
<Select
value={state.day}
onChange={(value) => setState({...state, day: value})}
>
{
days.map(day => {
return <Select.Option
key={day}
value={day}
>
{day}
</Select.Option>;
})
}
</Select>
</>;
}
Lets say you want to update your option list based on data you get from backend;
note:categories is an array object. From this array you take out label and value for each option, see below:
const newOptions = categories.map((item, index) => {
return {
label: item.name,
value: item._id,
};
});
Then use newOptions inside your form like that:
<Form.Item label="anyName" name="anyName">
<Select style={{ width: 220 }} onChange={ handleChange } options={ newOptions } />
</Form.Item>
So I'm using a custom-built dialog component in react-admin which has a SimpleForm inside it. When I try to use input-level validate attribute on a LongTextInput, it just doesn't work. There is no error in the console or anything like that, but the input just accepts the text as is without validation. I don't seem to understand where I'm going wrong.
In the code that you see below, the inputs in the form that I have to render are conditional based on the props I'm passing from the parent. Hence, creating the form in a variable and then rendering it in the return() method.
What I've tried is rendering the inputs directly in the return() method without using the variable, but that does not seem to fix the validation problem as well.
Here's the array that defines the validation parameters I want to use -
const validateReason = [required(), minLength(5), maxLength(100)];
And I'm using this in the below render() method -
render() {
const props = this.props;
let renderForm = null;
if(props.formType === 'reject') {
renderForm =
(<SimpleForm
form="form"
toolbar={null}>
<RadioButtonGroupInput onChange={this.handleChoiceChange} source="reason" choices={[
{ id: '1', name: 'Inappropriate' },
{ id: '2', name: 'Abusive' },
{ id: '3', name: 'Not Product Related' },
{ id: '4', name: 'Others' },
]} />
<FormDataConsumer label="Select L2 Category" alwaysOn>
{({ formData }) => formData.reason === '4' &&
<LongTextInput
defaultValue={this.state.reasontext}
onChange={this.handleReasonChange}
source="reason_text"
label={props.title}
validate={validateReason}
/>
}
</FormDataConsumer>
</SimpleForm>);
}
else if(props.formType === 'skip') {
renderForm =
(<SimpleForm
form="form"
toolbar={null}>
<LongTextInput
defaultValue={this.state.reasontext}
onChange={this.handleReasonChange}
source="reason_text"
label={props.title}
validate={validateReason}
/>
</SimpleForm>);
}
return (
<Fragment>
<Dialog
fullWidth
open={props.showDialog}
aria-label={props.title}
>
<DialogTitle>{props.title}</DialogTitle>
<DialogContent>
{renderForm}
</DialogContent>
<DialogActions>
<SaveButton
onClick={() => {
props.handleClose();
this.handleSaveClick(props.formType);
}}
label={props.positiveButtonText}
/>
<Button onClick={() => {
this.clearReason();
props.handleClose();
}}>
Cancel
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}
I expect that the validate attribute validates my input correctly according to my validation parameters.
Was wondering if Formik has a native solution for identifying the addition and deletion (and update) of FieldArray in the form ?
I have the code on sandbox here https://codesandbox.io/s/jn7x2m75o9 ( based on the original Formik Array example # https://github.com/jaredpalmer/formik/blob/master/examples/Arrays.js )
but also the relevant part here :
With an Initial state of 3 friend defined, how can I know in my onSubmithandler which one were modified,deleted,updated.
import React from "react";
import { Formik, Field, Form, ErrorMessage, FieldArray } from "formik";
const initialValues = {
friends: [
{
name: "Friend_A",
email: "email_A#somewhere.com"
},
{
name: "Friend_B",
email: "email_B#somewhere.com"
},
{
name: "Friend_C",
email: "email_C#somewhere.com"
}
]
};
const mySubmit = values => console.log();
const SignIn = () => (
<div>
<h1>Invite friends</h1>
<Formik
initialValues={initialValues}
onSubmit={values => {
var itemRemoved = values.GetItemRemoveFromArray; // This is what I'm looking for
console.log(itemRemoved);
// Would print Friend_A
var itemAdded = values.GetItemAddedFromArray; // This is what I'm looking for
console.log(itemAdded);
// Would print New_Friend
var itemUpdated = values.GetItemUpdatedInArray; // This is what I'm looking for
console.log(itemUpdated);
// Would print Friend_C
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
}, 500);
}}
render={({ values }) => (
<Form>
<FieldArray
name="friends"
render={({ insert, remove, push }) => (
<div>
{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={() => push({ name: "", email: "" })}
>
Add Friend
</button>
</div>
)}
/>
<button type="submit">Invite</button>
</Form>
)}
/>
</div>
);
export default SignIn;
So if with the above a user where to :
Click on the X below Friend_A
Modify Friend_C email to email_C#nothere.com
Click "Add Friend"
Enter value Name: New_Friend_X and email: XX#YY.com
Click "Add Friend"
Enter value Name: New_Friend_Z and email: Friend_Z#coolplace.com
Click "X" button below newly entered "New_Friend_X"
Click "Invite"
in my mySubmit I'm looking for a way to easily get :
Friend_A was Removed
Friend_C was Modified
New_Friend_Z was added (was not in the original initialValues to formik)
(I Don't care about New_Friend_X. No need to know it was added/removed )
Point of this is to minimize rest call to the back end to create/update entity/link and also I really dont want to write my own "secondary state" in the onClick handler of the remove button before calling the remove(index) handler provided by Formik to track what need to be deleted from the DB.
Its not built into Formik, but it is not hard to do in javascript.
First, understand that Formik clones the object you give to initialValues. So in onSubmit, you will compare the final value to your original object.
The incoming data:
const initialFriends = [
{
name: "Friend_A",
email: "email_A#somewhere.com"
},
{
name: "Friend_B",
email: "email_B#somewhere.com"
},
{
name: "Friend_C",
email: "email_C#somewhere.com"
}
];
const initialValues = { friends: initialFriends };
Modified Formik declaration:
<Formik initialValues={initialValues}
...
onSubmit={values => {
const { added, deleted, changed } = addDeleteChange(
initialFriends,
values.friends
);
setTimeout(() => {
alert(
"Added: " + JSON.stringify(Object.fromEntries(added.entries()))
);
alert(
"Deleted: " + JSON.stringify(Object.fromEntries(deleted.entries()))
);
alert(
"Changed:" + JSON.stringify(Object.fromEntries(changed.entries()))
);
alert(JSON.stringify(values, null, 2));
}, 500);
}}
...
Helper functions:
function partition(array, filter) {
let pass = [],
fail = [];
array.forEach(e => (filter(e) ? pass : fail).push(e));
return [pass, fail];
}
const addDeleteChange = (in1, out1) => {
let inMap = new Map(in1.map(f => [f.name, f]));
let outMap = new Map(out1.map(f => [f.name, f]));
let inNames = new Set(inMap.keys());
let outNames = new Set(outMap.keys());
let [kept, added] = partition(out1, f => inNames.has(f.name));
let deleted = in1.filter(f => !outNames.has(f.name));
//alert(JSON.stringify(Object.fromEntries(deleted.entries())));
let changed = kept.filter(f => f.email !== inMap.get(f.name).email);
//alert(JSON.stringify(Object.fromEntries(changed.entries())));
return { added: added, deleted: deleted, changed: changed };
};
Code in codesandbox
NOTE: If you change the name of a friend, that will appear as a delete of original friend and an add of a new friend.
A more robust solution would be to add a (hidden) "id" field to each friend. Then instead of comparing name, would compare id.
That requires generating a new id as add each friend.
I have two problems that are a result of each other. I populate two fields with initialValue data, I can then push another field to the array. The issue came about when I tried to amend the initialValue structure from:
initialValues: {
rockSingers: [ "Axl Rose", "Brian Johnson"]
}
to:
initialValues: {
rockSingers: [{ singer: "Axl Rose" }, { singer: "Brian Johnson" }]
}
The first problem is that the field now returns [object Object]. Upon submitting the form the correct json format is displayed until I come on to my 2nd issue... when adding a new value that does not format the same as the initialValue data - e.g.
{
"rockSingers": [
{
"singer": "Axl Rose"
},
{
"singer": "Brian Johnson"
},
"Tom Rudge"
]
}
Here is the codesandbox - https://codesandbox.io/s/8kzw0pw408
Modify renderRockSingers so that you are grabbing the object, not a string.
const renderRockSingers = ({ fields }) => (
<div>
<h3>Rock Singers:</h3>
{fields.map((rockSinger) => (
<div>
<Field name={`${rockSinger}.singer`} key="index" component="input" />
</div>
))}
<button type="button" onClick={() => fields.push()}>
Add more
</button>
</div>
);
More on the FieldArray component here: fieldarrays
Try this:
const renderRockSingers = ({ fields }) => (
<div>
<h3>Rock Singers:</h3>
{fields.map((rockSinger, index) => (
<div>
<Field
name={rockSinger}
format={value => value.singer}
parse={value => ({ singer: value })}
key={index}
component="input"
/>
</div>
))}
<button type="button" onClick={() => fields.push({ singer: '' })}>
Add more
</button>
</div>
);
<Field
name={rockSinger}
key={index}
component="input"
format={(value, name) => (value !== undefined ? value.singer : "")}
normalize={value => ({ singer: value })}
/>
Code Sandbox: https://codesandbox.io/s/7m1p9600y0