React Formik JSON to YAML, then back to JSON on Submit - reactjs

My goal is to have a JSON object stringified to YAML for a field value then, on form submit, take the YAML and convert it to JSON.
I was able to take the JSON, and use YAML.stringify(options, null, 2); to get a YAML value in the field to be edited with YAML node package. However, upon submit I'm not exactly sure how to parse the YAML as JSON.
Near the bottom of my component I tried to push JSON.stringify('', null, 0) to get the value but I keep getting YAML.
How can I get the edited YAML field to parse back to JSON for the data with Formik's onSubmit?
Is taking edited YAML and converting it as JSON on submit something that is possible on the frontend or would I have to add backend logic? I found converters but not much in this instance.
My JSON data (focus is on "options" key):
{
"id": "1",
"photo_name": "Testing",
"options": {
"test": true,
"command": [
"test photo"
],
"max_size": 50,
"is_prio": false
},
YAML output:
test: true
command:
- test photo
max_size: 50
is_prio: false
My React component:
import YAML from 'yaml';
const NewPhoto = () => {
const fetchContext = useContext(FetchContext);
const [photoList, setphotoList] = useState([]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
useEffect(() => {
const getphotoList = async () => {
try {
const { data } = await fetchContext.authAxios.get(`/api/photo_data/`);
setphotoList(data.results);
} catch (err) {
console.log(err);
}
};
getphotoList();
}, [fetchContext]);
const initialValues = {
id: '',
options: '',
};
return (
<div className="col-xl-12">
<Formik
initialValues={initialValues}
onSubmit={async (values, { resetForm }) => {
alert(JSON.stringify(values, null, 2));
resetForm();
}}
validationSchema={schema}
>
{({ isSubmitting, values, setFieldValue, handleChange }) => (
<Form>
<FieldArray name="photo">
{({ insert, remove, push }) => (
<>
{values.photo.length > 0 &&
values.photo.map((task, index) => (
<div className="row" key={index}>
<div className="col-11">
<div className="row">
<div className="col">
<div className="form-group">
<label htmlFor={`photo.${index}.step`}>
PHOTO TASK
</label>
<Field
className="form-control"
as="select"
name={`photo.${index}.id`}
onChange={(event) => {
handleChange(event);
const selectedPhotoTask = photoList.find(
(photoList) =>
photoList.id === event.target.value
);
const selectedOptions = YAML.stringify(
selectedPhotoTask.options,
null,
2
);
setFieldValue(
`photo.${index}.options`,
selectedOptions
);
}}
>
<option value="" defaultValue disabled>
...
</option>
{photoList &&
photoList.map((task) => (
<option key={task.id} value={task.id}>
{task.name}
</option>
))}
</Field>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="form-group">
<label htmlFor={`photo.${index}.options`}>
OPTIONS
</label>
<Field
id="options"
component="textarea"
className="form-control"
name={`photo.${index}.options`}
type="text"
rows="4"
onChange={handleChange}
/>
</div>
</div>
</div>
</div>
<div className="col-1">
<button
type="button"
className="btn"
onClick={() => remove(index)}
>
DELETE
</button>
</div>
</div>
))}
<button
type="button"
className="btn"
onClick={() =>
push({
id: '',
options: '',
// options: JSON.stringify('', null, 0),
})
}
>
ADD
</button>
</>
)}
</FieldArray>
<button className="btn" type="submit" disabled={isSubmitting}>
SUBMIT
</button>
</Form>
)}
</Formik>
</div>
);
};
export default NewPhoto;

I needed to save the values into their own variable, and then loop through them and apply a YAML.parse() for each index.
let submitVals = values;
for (let index = 0; index < submitVals.photo.length; index++) {
let optionsText = YAML.parse(submitVals.photo[index].options);
submitVals.photo[index].options = optionsText;
}
console.log(submitVals);
send(submitVals);

Related

How to get value inside formik out using useEffect?

my full code
https://codesandbox.io/s/testproducr02-bg7exz?file=/index.tsx
const [sum, setSum] = useState(0);
const [qty, setQty] = useState(0);
useEffect(() => {
const sum = values.product.reduce((acc, item) => acc + item.qty * 1, 0); //error values is not defined
setSum(sum);
});
useEffect is outside formik, but I don't know how to get the value out
this is inside my formik
<Formik
initialValues={initialValues}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
//alert(JSON.stringify(values, null, 2));
// const sum = values.product.reduce(
// (acc, item) => acc + item.qty * 1,
// 0
// );
// setSum(sum);
}}
>
{({ values }) => (
<Form>
<FieldArray name="product">
{({ insert, remove, push }) => (
<div>
{values.product.length > 0 &&
values.product.map((friend, index) => (
<div className="row" key={index}>
<div className="col">
<label htmlFor={`product.${index}.productname`}>
Productname
</label>
<Field
name={`product.${index}.productname`}
placeholder="productname"
type="text"
/>
</div>
<div className="col">
<label htmlFor={`product.${index}.qty`}>Qty</label>
<Field
name={`product.${index}.qty`}
placeholder="qty"
type="number"
/>
</div>
<div className="col">
<button
type="button"
className="secondary"
onClick={() => remove(index)}
style={{ marginTop: "10px" }}
>
Delete
</button>
</div>
</div>
))}
<label>Sum</label>
<Field
name={`sum`}
placeholder="sum"
type="text"
value={sum}
disabled
/>
<br />
<button
type="button"
className="secondary"
onClick={() =>
push({ productname: "", qty: 0, price: 0, amount: 0 })
}
>
Add Product
</button>
</div>
)}
</FieldArray>
<p>Qty : {qty}</p>
<p>sum : {sum}</p>
<button type="submit">Summit</button>
</Form>
)}
</Formik>
i want to calculate it without pressing onSubmit
You can use Formik's innerRef prop to attach a ref to the Formik component.
// ...
const formikRef = useRef(null);
// ...
return (
<Formik
initialValues={initialValues}
innerRef={formikRef}
// ...
>
...
</Formik>
);
And then in some useEffect
if (formikRef.current) {
const productData = formikRef.current.values.product
// do something
}
I believe there are other methods too, such as "Formik Context"

How to use Radio button to select a different form in each array of useFieldArray of react-hook-form

I have a basic react hook form with field array. With append the form fields replicates as expected. For each array, I want the user to choose some field to enter without entering the other. I am using radio button and useState to achieve this. However, when i change the selection in an array, the selections in the other arrays changes as well. Please how do i correct this ? Or is there a better way to achieve this functionality. Thanks in advance for your help. The code is found below. I also have codeSandbox: https://codesandbox.io/s/usefieldarray-react-hook-form-2yp3vb?file=/src/App.js:0-3753
export default function App() {
const { handleSubmit, control } = useForm({
defaultValues: {
Detail: [
{
userName: {},
officeAddress: {},
homeAddress: {}
}
]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: "Detail"
});
const [checked, setChecked] = useState();
// onChange function for the address forms
const changeAddressForm = (e) => {
setChecked(e.target.value);
};
const onSubmit = async (data) => {};
return (
<div className="App">
<h1>Selecting a different form in each field array</h1>
<form onSubmit={handleSubmit(onSubmit)}>
<ul>
{fields.map((field, index) => {
return (
<li
key={field.id}
className="w3-border w3-border-green w3-padding"
>
<div>
<div className="w3-padding-large">
<label>Username</label>
<Controller
name={`Detail.${index}.userName`}
control={control}
render={({ field }) => (
<Input
onChange={(value) => field.onChange(value)}
style={{ width: 200 }}
/>
)}
/>
</div>
<div>
<Radio.Group onChange={changeAddressForm} value={checked}>
<Radio value={1}>Office address</Radio>
<Radio value={2}>Home address</Radio>
</Radio.Group>
</div>
<div className="w3-padding-large">
{checked === 1 && (
<div>
<label>Office address</label>
<Controller
name={`Detail.${index}.officeAddress`}
control={control}
render={({ field }) => (
<Input
onChange={(value) => field.onChange(value)}
style={{ width: 200 }}
/>
)}
/>
</div>
)}
</div>
<div className="w3-padding-large">
{checked === 2 && (
<div>
<label>Home address</label>
<Controller
name={`Detail.${index}.homeAddress`}
control={control}
render={({ field }) => (
<Input
onChange={(value) => field.onChange(value)}
style={{ width: 200 }}
/>
)}
/>
</div>
)}
</div>
</div>
</li>
);
})}
</ul>
<section>
<button
type="button"
onClick={() =>
append({
userName: {},
homeAddress: {},
officeAddress: {}
})
}
>
Append
</button>
</section>
</form>
</div>
);
}

How to a make dynamic form in React

I'm learning React. My target is to make this object:
"Phones": [
{
"Type": "Mobile",
"Phone_Number": "6546543"
},
{
"Type": "Home",
"Phone_Number": "6546543"
}
]
I did some research and I followed this YouTube video ReactJS - Dynamically Adding Multiple Input Fields.
These values I'll fetch from a form. Th user can keep on adding as many numbers as he wants. My code is:
render() {
return (
<div>
<h1>The Form</h1>
<label>Contact</label>
{this.state.phones.map((phone, index) => {
return (
<div key={index}>
<input onChange={(e) => this.handleChange(e)} value={phone} />
<select>
{this.phoneTypes.map((phoneType, index) => {
return (
<option key={index} value={phoneType}>
{phoneType}
</option>
);
})}
</select>
<button onClick={(e) => this.handleRemove(e)}>Remove </button>
</div>
);
})}
<hr />
<button onClick={(e) => this.addContact(e)}>Add contact</button>
<hr />
<button onClick={(e) => this.handleSubmit(e)}>Submit</button>
</div>
);
}
I'm not pasting the entire code here as I've already created a stackblitz. My code is not working as expected. Please pitch in.
You need to pass index in the handleChange function for input field. I guess you missed that part from video.This will make the input field editable.
<input
onChange={(e) => this.handleChange(e, index)}
value={phone}
/>
For the second part to get the expected object array I have updated some code, please check:
import React, { Component } from 'react';
class Questionnaire extends Component {
state = {
phones: [{ type: '', number: '' }],
};
phoneTypes = ['Mobile', 'Home', 'Office'];
addContact() {
this.setState({ phones: [...this.state.phones, { type: '', number: '' }] });
}
handleChange(e, index) {
this.state.phones[index]['number'] = e.target.value;
this.setState({ phones: this.state.phones });
}
handleRemove(index) {
this.state.phones.splice(index, 1);
console.log(this.state.phones, '$$$');
this.setState({ phones: this.state.phones });
}
handleSubmit(e) {
console.log(this.state, '$$$');
}
render() {
return (
<div>
<h1>The Form</h1>
<label>Contact</label>
{this.state.phones.map((phone, index) => {
return (
<div key={index}>
<input
onChange={(e) => this.handleChange(e, index)}
value={phone.number}
name="number"
/>
<select>
{this.phoneTypes.map((phoneType, index) => {
return (
<option key={index} value={phoneType} name="type">
{phoneType}
</option>
);
})}
</select>
<button onClick={(e) => this.handleRemove(e)}>Remove </button>
</div>
);
})}
<hr />
<button onClick={(e) => this.addContact(e)}>Add contact</button>
<hr />
<button onClick={(e) => this.handleSubmit(e)}>Submit</button>
</div>
);
}
}
export default Questionnaire;
You can use input objects like formik
import { useState } from 'react';
const Component = () => {
const [inputs, setInputs] = useState({ // Initial inputs
email: "",
phoneNumber: "",
});
const handleSubmit = (e) => {
e.preventDefault();
// Do
};
const handleChange = (e) => {
setInputs({...inputs, [e.target.id]: e.target.value});
};
const addInput = () => {
const name = window.prompt("Type a name", "");
setInputs({...inputs, [name]: ""});
};
return (
<form onSubmit={handleSubmit}>
{object.keys(inputs).map((name, i) => {
<input type="text" onChange={handleChange} name={name} value={inputs[name]} />
})}
<button
type="button"
onClick={addInput}
>
Add Input
</button>
</form>
);
}

Formik React set values onChange with setFieldValue outside Array of Fields

My goal is to select a Student and with onChange to dynamically setFieldValues for the FieldArray of their nested schedule array of objects.
When I select Student: 1, I should get an array of fields for each of their 3 schedule items, Student 2: will have example 4 items. Example relates to something such as this older example with Fruit on CodeSandbox. A second CodeSandbox also related for this.
My problem is that when I try to map a student's schedule it shows schedule.map is a TypeError. Thus not populating my FieldArray with the fields to be edited.
TypeError: values.schedule.map is not a function
My JSON data:
{
"count": 2,
"results": [
{
"id": "1",
"name": "Tom",
"schedule": [
{
"class": "GYM",
"time": "9:00,
},
{
"class": "History",
"time": "10:00",
},
{
"class": "Lunch",
"time": "11:00",
},
},
{
"id": "2",
"name": "Nancy",
"schedule": [
{
"class": "Math",
"time": "9:00,
},
{
"class": "Science",
"time": "10:00",
},
{
"class": "English",
"time": "11:00",
},
{
"class": "History",
"time": "12:00",
},
}
]
}
My Formik Component:
const NewStudentSchedule = () => {
const [studentSchedule, setSchedule] = useState([]);
useEffect(() => {
const getSchedule = async () => {
try {
const { data } = await fetchContext.authAxios.get(
`/api/schedule/`
);
setSchedule(data.results);
// console.log(data);
} catch (err) {
console.log(err);
}
};
getSchedule();
}, [fetchContext]);
const initialValues = {
id: '',
schedule: [
{
class: '',
time: '',
},
],
};
return (
<Formik
initialValues={initialValues}
onSubmit={async (values, { resetForm }) => {
alert(JSON.stringify(values, null, 2));
resetForm();
}}
>
{({
isSubmitting,
values,
setFieldValue,
handleChange,
errors,
touched,
}) => (
<Form>
<div className="row">
<div className="col-md-6 mr-auto">
<div className="form-group">
<label htmlFor="status">STUDENT</label>
<Field
className="form-control"
as="select"
name="id"
onChange={(event) => {
handleChange(event);
const selectedTask = schedule.find(
(selectStudent) => selectStudent.id === event.target.value
);
setFieldValue(
`schedule.step`,
selectedTask.schedule.step
);
}}
>
<option value="" defaultValue disabled>
...
</option>
{schedule &&
schedule.map((i) => (
<option key={i.id} value={i.id}>
{i.name}
</option>
))}
</Field>
</div>
</div>
</div>
<div className="col-md-12">
<h3 className="pt-3">CREATE SCHEDULE</h3>
<FieldArray name="schedule">
{({ insert, remove, push }) => (
<>
{values.schedule.length > 0 &&
values.schedule.map((task, index) => (
<div className="row" key={index}>
<div className="col-11">
<div className="row">
<div className="col">
<div className="form-group">
<label
htmlFor={`schedule.${index}.class`}
>
CLASS
</label>
<Field
className="form-control"
name={`schedule.${index}.class`}
type="text"
/>
</div>
</div>
<div className="col">
<div className="form-group">
<label
htmlFor={`schedule.${index}.time`}
>
TIME
</label>
<Field
className="form-control"
name={`schedule.${index}.time`}
type="text"
/>
</div>
</div>
</div>
</div>
<div className="col-1">
<button
type="button"
className="btn delete"
onClick={() => remove(index)}
>
Delete
</button>
</div>
</div>
))}
<button
type="button"
className="btn add"
onClick={() => push({ class: '', time: '' })}
>
Add
</button>
</>
)}
</FieldArray>
<button
className="btn float-right mb-5"
type="submit"
disabled={isSubmitting}
>
SUBMIT
</button>
</div>
</Form>
)}
</Formik>
)
}
Screenshots of my App:
This is a tricky one 😉
It looks like you want to pre-populate a form with data fetched from the API. Also, you'd like the user to be able to switch between students (Tom and Nancy). In that case, you need to set all students' data inside Formik's initialValues. And when handling the class values inside the <FieldArray>, you need to ensure you are referring to the correct class for that particular student.
There are a few issues with your code, for example studentSchedule is never used and schedule is undefined. I went ahead and made some assumptions and came up with this solution below. It refers to the correct class by using two indexes inside the <FieldArray>: selectedStudentIndex and taskIndex
const App = () => {
const [studentSchedule, setSchedule] = useState([]);
useEffect(() => {
const getSchedule = async () => {
try {
const { data } = await getData();
setSchedule(data.results);
// console.log(data);
} catch (err) {
console.log(err);
}
};
getSchedule();
}, []);
return <ScheduleForm students={studentSchedule} />;
};
const ScheduleForm = ({ students }) => {
const initialValues = {
id: "",
students
};
return (
<Formik
enableReinitialize={true}
initialValues={initialValues}
onSubmit={async (values, { resetForm }) => {
console.log(JSON.stringify(values, null, 2));
}}
>
{({
isSubmitting,
values,
setFieldValue,
handleChange,
errors,
touched
}) => {
const selectedStudentIndex = values.students.findIndex(
({ id }) => id === values.id
);
return (
<Form>
<div className="row">
<div className="col-md-6 mr-auto">
<div className="form-group">
<label htmlFor="status">STUDENT</label>
<Field
className="form-control"
as="select"
name="id"
onChange={(event) => {
handleChange(event);
const selectedTask = students.find(
(selectStudent) =>
selectStudent.id === event.target.value
);
setFieldValue(
`schedule.step`,
selectedTask.schedule.step
);
}}
>
<option value="" defaultValue disabled>
...
</option>
{values.students &&
values.students.map((i) => (
<option key={i.id} value={i.id}>
{i.name}
</option>
))}
</Field>
</div>
</div>
</div>
<div className="col-md-12">
<h3 className="pt-3">CREATE SCHEDULE</h3>
<FieldArray name={`students.${selectedStudentIndex}.schedule`}>
{({ insert, remove, push }) => (
<div>
{values.id !== "" &&
values.students[selectedStudentIndex].schedule.map(
(task, taskIndex) => (
<div className="row" key={taskIndex}>
<div className="col-11">
<div className="row">
<div className="col">
<div className="form-group">
<label
htmlFor={`students.${selectedStudentIndex}.schedule.${taskIndex}.class`}
>
CLASS
</label>
<Field
className="form-control"
name={`students.${selectedStudentIndex}.schedule.${taskIndex}.class`}
type="text"
/>
</div>
</div>
<div className="col">
<div className="form-group">
<label
htmlFor={`students.${selectedStudentIndex}.schedule.${taskIndex}.time`}
>
TIME
</label>
<Field
className="form-control"
name={`students.${selectedStudentIndex}.schedule.${taskIndex}.time`}
type="text"
/>
</div>
</div>
</div>
</div>
<div className="col-1">
<button
type="button"
className="btn delete"
onClick={() => remove(taskIndex)}
>
Delete
</button>
</div>
</div>
)
)}
<button
type="button"
className="btn add"
onClick={() => push({ class: "", time: "" })}
>
Add
</button>
</div>
)}
</FieldArray>
<button
className="btn float-right mb-5"
type="submit"
disabled={isSubmitting}
>
SUBMIT
</button>
</div>
</Form>
);
}}
</Formik>
);
};
Live Demo
I'd be interested if anyone comes up with a simpler approach.

Final form array fields async validation

I am trying to implement async validation react-final-form-array. But its returning promise but not showing error at all.
I took reference of this code for asnyc validate and created my own version with final form array here.
Form:
<Form
onSubmit={onSubmit}
mutators={{
...arrayMutators
}}
initialValues={{ customers: [{ placeholder: null }] }}
validate={formValidate}
render={({
handleSubmit,
mutators: { push, pop }, // injected from final-form-arrays above
pristine,
reset,
submitting,
values,
errors
}) => {
return (
<form onSubmit={handleSubmit}>
<div>
<label>Company</label>
<Field name="company" component="input" />
</div>
<div className="buttons">
<button type="button" onClick={() => push("customers", {})}>
Add Customer
</button>
<button type="button" onClick={() => pop("customers")}>
Remove Customer
</button>
</div>
<FieldArray name="customers" validate={nameValidate}>
{({ fields }) =>
fields.map((name, index) => (
<div key={name}>
<label>Cust. #{index + 1}</label>
<Field
name={`${name}.firstName`}
component={testInput}
placeholder="First Name"
/>
<Field
name={`${name}.lastName`}
component={testInput}
placeholder="Last Name"
/>
<span
onClick={() => fields.remove(index)}
style={{ cursor: "pointer" }}
>
❌
</span>
</div>
))
}
</FieldArray>
<div className="buttons">
<button type="submit" disabled={submitting || pristine}>
Submit
</button>
<button
type="button"
onClick={reset}
disabled={submitting || pristine}
>
Reset
</button>
</div>
<pre>
nameValidate Function:{"\n"}
{JSON.stringify(nameValidate(values.customers), 0, 2)}
</pre>
<pre>
Form errors:{"\n"}
{JSON.stringify(errors, 0, 2)}
</pre>
<pre>
Form values:{"\n"}
{JSON.stringify(values, 0, 2)}
</pre>
</form>
);
}}
/>
Validate Function:
const duplicateName = async value => {
await sleep(400);
if (
~["john", "paul", "george", "ringo"].indexOf(value && value.toLowerCase())
) {
return { firstName: "name taken!" };
}
};
const nameValidate = values => {
if (!values.length) return;
const errorsArray = [];
values.forEach(value => {
if (value) {
let errors = {};
if (!value.firstName) errors.firstName = "First Name Required";
if (Object.keys(errors).length === 0) {
errors = duplicateName(value.firstName);
console.log("errors", errors);
}
errorsArray.push(errors);
}
});
return errorsArray;
};
Okay, you're mixing the validation in a way that's not allowed. You can't embed a Promise inside an array of errors, the whole validation function must return a promise. Here's the fixed version. The await duplicateName() is the important bit.
const nameValidate = async values => {
if (!values.length) return
return await Promise.all(
values.map(async value => {
let errors = {}
if (value) {
if (!value.firstName) errors.firstName = 'First Name Required'
if (Object.keys(errors).length === 0) {
errors = await duplicateName(value.firstName)
}
}
console.error(errors)
return errors
})
)
}

Resources