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
Related
I'm using an object to create the parameters for my form, so I can loop through the data and automatically create the form rather than adding everything manually. My problem comes when trying to pass a variable to the register, everything stops working.
Here's what the object looks like:
const form = [
{
name: "styles",
type: "graphic",
checkboxes: [
{
value: "dresses",
label: "Dresses"
}
]
}
];
export default form;
Checkbox.tsx:
const Checkbox = React.forwardRef(
(
{
label,
name,
value,
onChange,
defaultChecked,
errors,
onBlur,
errorMessage,
...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 && errorMessage}</p>}
</div>
);
}
);
And here's how I use it in the parent component:
const schema = yup.object().shape({
name: yup.array().min(1)
});
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)}>
{form?.map((section: any, index: any) => {
console.log(section, "s");
const name = section.name;
return (
<div key={index}>
<h2 className="font-bold">{name}</h2>
{section.checkboxes.map((checkbox: any, index: any) => {
return (
<>
<Checkbox
key={index}
{...register(name)}
name={name}
value={checkbox.value}
label={checkbox.label}
/>
</>
);
})}
{errors.name && (
<p className="error">
{errors.name.message && `Please select one item`}
</p>
)}
</div>
);
})}
<button type="submit">Submit</button>
</form>
);
}
As you can see from this Codesandbox, everything works if you wrap the name variable in quotes so it's a string, but not as a variable.
Things I've tried:
{...register(`${name}`)}
name={name}
{...register(name.toString())}
name={name}
This works:
{...register("name")}
name={name}
But then I lose the ability to use name as variable in my real code (if that makes sense),
As you can probably tell I'm a bit lost here. There's several different posts on Stackoverflow referring to this issue but none of the answers make any sense to me.
Any ideas what I'm doing wrong?
Don't get this confused with checking each radio button I have on the page. I want to implement a check all button that sets the value of a nested object state equal to a certain value. I am storing each question in a nested state. Ex.
formQuestions({
kitchen: [question,question2,question3],
living: [question,question2,question3]
})
Four radio buttons are being made for each question. Now one radio button can only be selected at once. Each radio button has its' own value. Ex. `"Good", "Fair", "Poor", "N/A".
When a radio button is selected a state is generated dynamically for that section and question. Ex.
formAnswers({
kitchen: {
question: "Good"
question2: "Poor"
}
})
The goal here is the button that I want to create that checks only one value for each question Ex. clicks button question: "Good", question2: "Good" etc..
For me to set the state of a dynamic value I would need the "Section name" lets call it Name and the "Question" we'll call it question. That would give me access to the value like so formAnswers[Name][question]: value
I am trying to set that state from a component called SectionHeader. These contain the buttons.
SectionHeader.js
import { FormAnswersContext, FormQuestionsContext } from "../../Store";
function SectionHeader({ title, name }) {
const [formAnswers, setFormAnswers] = useContext(FormAnswersContext);
const [formQuestions, setFormQuestions] = useContext(FormQuestionsContext);
return (
<div>
<h1 className={styles["Header"]}>{title}</h1>
<div className={styles["MarkAllWrapper"]}>
<button className={styles["MarkAll"]}>
Mark all items as "Good" in this section
</button>
<br />
<button className={styles["MarkAll"]}>
Mark all items as "N/A" in this section
</button>
</div>
</div>
);
}
The parent of Section Header and the rest of the form code excluding the child radio buttons which I have explained, are in another component LivingRoom.js
LivingRoom.js
import { FormQuestionsContext, FormAnswersContext } from "../../Store";
function LivingRoomForm({ Name }) {
const [expanded, setExpanded] = useState(false);
const [formQuestions, setFormQuestions] = useContext(FormQuestionsContext);
const [formAnswers, setFormAnswers] = useContext(FormAnswersContext);
const array = formQuestions.living;
const onChange = (e, name) => {
const { value } = e.target;
setFormAnswers((state) => ({
...state,
[Name]: { ...state[Name], [name]: value },
}));
};
const handleOpen = () => {
setExpanded(!expanded);
};
return (
<div>
<Button
className={styles["CollapseBtn"]}
onClick={handleOpen}
style={{ marginBottom: "1rem", width: "100%" }}
>
<p>LIVING ROOM INSPECTION</p>
<FontAwesome
className="super-crazy-colors"
name="angle-up"
rotate={expanded ? null : 180}
size="lg"
style={{
marginTop: "5px",
textShadow: "0 1px 0 rgba(0, 0, 0, 0.1)",
}}
/>
</Button>
<Collapse className={styles["Collapse"]} isOpen={expanded}>
<Card>
<CardBody>
{array ? (
<div>
<SectionHeader title="Living Room Inspection" name={Name} />
<div
className={styles["LivingRoomFormWrapper"]}
id="living-room-form"
>
{array.map((question, index) => {
const selected =
formAnswers[Name] && formAnswers[Name][question]
? formAnswers[Name][question]
: "";
return (
<div className={styles["CheckboxWrapper"]} key={index}>
<h5>{question}</h5>
<Ratings
section={Name}
question={question}
onChange={onChange}
selected={selected}
/>
</div>
);
})}
</div>
<br />
<ImageUploader name="living" title={"Living Room"} />
</div>
) : (
<div></div>
)}
</CardBody>
</Card>
</Collapse>
</div>
);
}
If there is anything I am missing please let me know, I would be happy to share it. Cheers
Edit: for anyone that needs the radio buttons component.
Ratings.js
import React from "react";
import { FormGroup, CustomInput } from "reactstrap";
function Ratings({ selected, section, question, onChange }) {
return (
<div>
<FormGroup>
<div>
<CustomInput
checked={selected === "Good"}
onChange={(e) => onChange(e, question)}
type="radio"
id={`${section}_${question}_Good`}
value="Good"
label="Good"
/>
<CustomInput
checked={selected === "Fair"}
onChange={(e) => onChange(e, question)}
type="radio"
id={`${section}_${question}_Fair`}
value="Fair"
label="Fair"
/>
<CustomInput
checked={selected === "Poor"}
onChange={(e) => onChange(e, question)}
type="radio"
id={`${section}_${question}_Poor`}
value="Poor"
label="Poor"
/>
<CustomInput
checked={selected === "N/A"}
onChange={(e) => onChange(e, question)}
type="radio"
id={`${section}_${question}_NA`}
value="N/A"
label="N/A"
/>
</div>
</FormGroup>
</div>
);
}
I do not completely understand your question, I am sorry but I think this will help you.
Here is an implementation of radio buttons using react -
class App extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
handleChange = e => {
const { name, value } = e.target;
this.setState({
[name]: value
});
};
render() {
return (
<div className="radio-buttons">
Windows
<input
id="windows"
value="windows"
name="platform"
type="radio"
onChange={this.handleChange}
/>
Mac
<input
id="mac"
value="mac"
name="platform"
type="radio"
onChange={this.handleChange}
/>
Linux
<input
id="linux"
value="linux"
name="platform"
type="radio"
onChange={this.handleChange}
/>
</div>
);
}
}
After a few attempts, I was able to figure out the solution to this issue.
The key here was to figure out a way to get gather each question so that it may be used as a key when setting the state. As my questions were stored in a ContextAPI, I was able to pull them out like so...
this may not be the best solution however it worked for me.
const setStateGood = () => {
formQuestions[name].map((question) => {
setFormAnswers((state) => ({
...state,
[name]: { ...state[name], [question]: "Good" },
}));
});
};
const setStateNA = () => {
formQuestions[name].map((question) => {
setFormAnswers((state) => ({
...state,
[name]: { ...state[name], [question]: "N/A" },
}));
});
};
I was able to map through each question since the name is being passed through props is a key inside the actual object, formQuestions[name]. Because i'm mapping through each one I can set that question as a key and return the new state for each question to whatever I would like.
However, if I was to create an onClick={setState('Good')}, React didn't like that and it created an infinite loop. I will look for more solutions and update this post if I find one.
I have these two select bars:
When I select an option from the first bar, I have it set up to populate this object like so:
I am trying to make it so that the options available in the second select bar populated as the contents of the needed_skills array.
So in this example, the options in the second select bar would be: "Proposal Writing", "The McKinsey 7s Framework" etc
My attempt was this:
const createInputs = () => {
return values.skills_required.map((skill, idx) => {
return (
<div className="input-group">
<select
value={skill} placeholder="Enter a skill"
onChange={e => updateSkill(e, idx)}
className="form-control">
<option>Select an option...</option>
{values.category2.needed_skills && values.category2.needed_skills.map((c, i) => (
<option
key = {i}
value={JSON.stringify(c)}>
{JSON.stringify(c)}
</option>
))}
</select>
<div className="input-group-append">
<button
className="btn btn-outline-danger mb-3"
type="button"
id="button-addon2"
onClick={() => removeSkill(idx)}>x
</button>
</div>
</div>
);
});
};
But nothing, even though when I console.log(values.category2.needed_skills) and get the array, I cant seem to map across it and return it as options in the 'select' dropdown.
Any advice/help is greatly appreciated.
#malfunction is correct, when I console log the index and elements of the loop I get nothing. How can I loop through this array of strings? My data structure is exactly like this:
const [values, setValues] = useState({
name: '',
description: '',
pitch_price: '',
categories: [],
category: '',
quantity: '',
applications: '',
business_name: '',
skills_required: [''],
category2: {
name: "",
needed_skills: [""]
},
photo: '',
created_by: '',
loading: false,
error: '',
createdProject: '',
redirectToProfile: false
});
The category2 part is where I am keeping the data. Also, I know that the data is actually there because when I console log my values.category2 object after selecting a Category - I get this:
Have you tried adding a return. i.e.
return <option key = {i}
value={JSON.stringify(c)}>
{JSON.stringify(c)}
</option>
Also, not sure if JSON.stringify is required there. It's probably not hurting though.
Ok, that wasn't it ^^^
Without knowing the structure of your data I got this working in a new create-react-app without really changing your code:
import logo from './logo.svg';
import './App.css';
import { jsxFragment } from '#babel/types';
const values = {
skills_required: [
"skill_1",
"skill_2"
],
category2: {
needed_skills: [ "Proposal Writing", "The McKinsey Framwork", "etc"],
some_other_stuff: {},
a_date: "2019-09-30"
}
}
const updateSkill = (e, idx) => {
console.log("e: ", e);
console.log("e: ", idx);
}
const removeSkill = (idx) => {
console.log("e: ", idx);
}
const createInputs = () => {
return values.skills_required.map((skill, idx) => {
return (
<div className="input-group">
<select
value={skill} placeholder="Enter a skill"
onChange={e => updateSkill(e, idx)}
className="form-control">
<option>Select an option...</option>
{values.category2.needed_skills && values.category2.needed_skills.map((c, i) => (
<option
key = {i}
value={JSON.stringify(c)}>
{JSON.stringify(c)}
</option>
))}
</select>
<div className="input-group-append">
<button
className="btn btn-outline-danger mb-3"
type="button"
id="button-addon2"
onClick={() => removeSkill(idx)}>x
</button>
</div>
</div>
);
});
};
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
<jsxFragment>
{ createInputs()}
</jsxFragment>
</header>
</div>
);
}
export default App;
Is your data structure similar to that? I'd say that the issue might be with the data structure possibly looping through and getting nothing.
If you're not sure you can always add a console.log in here:
<option>Select an option...</option>
{ values.category2.needed_skills
&& values.category2.needed_skills.map((c, i) => {
console.log("c", c);
console.log("i", i);
return ( <option
key = {i}
value={JSON.stringify(c)}>
{JSON.stringify(c)}
</option>)
}
)
}
The initial return didn't work because the curly braces weren't there.
The only other thing I can think of is making your array it's own variable. i.e. let needed_skills = values.category2.needed_skills;
I hope one of those things helps.
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
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.