Final form array fields async validation - reactjs

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
})
)
}

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 fix "cannot update during an existing state transition (such as within `render`)" problem?

I get this problem even though my code doesn't update the state directly.
Here's my code:
export class Login extends React.Component {
static contextType = getContext();
_usernameValue = "";
_passwordValue = "";
// adding value to notification variable activates notification
state = { mode: "signin", notification: "" };
constructor(props) {
super(props);
this.showNotificationDeb = debounceOnce(this.showNotification, 200);
this.closeNotificationDeb = debounceOnce(this.closeNotification, 200);
}
signinClicked = () => {
this.context.dispatch({
type: "LOGIN",
payload: {
username: this._usernameValue,
password: this._passwordValue,
callback: this.incorrectCredentials,
},
});
};
forgotPasswordClicked = () => {
this.setState({ mode: "password" });
};
sendPasswordRequest = () => {
this.context.dispatch({
type: "FORGOT_PASSWORD",
payload: {
username: this._usernameValue,
password: this._passwordValue,
callback: this.passwordRequestSent,
},
});
};
incorrectCredentials = (errorMessage) => {
this.showNotificationDeb(errorMessage);
};
passwordRequestSent = (message) => {
this.showNotificationDeb(message);
};
restoreSigninWindow = () => {
this.setState({ mode: "signin" });
};
showNotification = (message) => {
console.log("aa");
this.setState({ notification: message });
};
closeNotification = () => {
if (this.state.notification) this.setState({ notification: "" });
};
render() {
return (
<div className={styles.container}>
<div className={styles.loginContainer}>
<Icon
rotate={90}
path={mdiCog}
size={2}
color="black"
className={styles.loginIcon}
/>
<p className={styles.loginTitle}>Sign in to RAS</p>
<div
className={`${styles.notificationContainer} ${
this.state.notification ? "" : styles.hideNotification
}`}
>
<p className={styles.notificationMessage}>
{this.state.notification}
</p>
<p
className={styles.notificationCloseButton}
onClick={() => this.closeNotification()}
>
x
</p>
</div>
<div className={styles.loginWindow}>
{this.state.mode === "signin" ? (
<React.Fragment>
<label className={styles.inputLabel}>Username</label>
<input
id="usernameInput"
className={styles.input}
onChange={(event) => {
this._usernameValue = event.target.value;
}}
></input>
<div className={styles.passwordLabelContainer}>
<label className={styles.inputLabel}>Password</label>
<p
className={styles.forgotPasswordLabel}
onClick={() => this.forgotPasswordClicked()}
>
Forgot password?
</p>
</div>
<input
id="passwordInput"
type="password"
className={styles.input}
onChange={(event) => {
this._passwordValue = event.target.value;
}}
></input>
<Button
variant="contained"
className={styles.button}
onClick={() => this.signinClicked()}
>
Sign in
</Button>
</React.Fragment>
) : (
<React.Fragment>
<div className={styles.backButtonContainer}>
<div onClick={() => this.restoreSigninWindow()}>
<Icon
path={mdiKeyboardBackspace}
size={0.85}
color="black"
className={styles.backIcon}
/>
<p>Back</p>
</div>
</div>
<label className={`${styles.inputLabel}`}>
Enter your email address. Password reset link will be send to
your email address.
</label>
<input
id="usernameInput"
className={styles.input}
placeholder="Enter your email address"
></input>
<Button
variant="contained"
className={styles.button}
onClick={() => this.sendPasswordRequest()}
>
Send
</Button>
</React.Fragment>
)}
</div>
</div>
</div>
);
}
}
As you see, I don't change the state directly. I do it always by calling the functions above. However, according to the React I am changing the state from inside render() function.
I do use lambda functions but I still see the problem.

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.

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

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);

How to access field data in other field

I have a modal form with a form that uses Formik. Here are two pictures that show two states of that form that can be toggled with a switch.Initially I fill text into fields which can be added dynamically and stored as an array with .
The second picture shows how I toggled to textarea. There you can also add text with commas that will be turned into an array.
Is there any way to fill data in input fields from the first screen, toggle into textarea and access already inputted data.
I understand formik keeps that state somewhere. But at the moment these fields have a separate state.
Here is my component:
class ModalForm extends React.Component {
constructor(props) {
super(props);
this.state = {
disabled: true,
};
}
onChange = () => {
this.setState({
disabled: !this.state.disabled,
});
};
render() {
var {
visible = false,
onCancel,
onRequest,
submitting,
setSubscriberType,
editing,
subscriptionTypeString,
tested,
selectedGates,
} = this.props;
const { gateId } = selectedGates.length && selectedGates[0];
const handleSubmit = values => {
console.log(values);
onRequest && onRequest({ gateId, ...values });
};
const { disabled } = this.state;
return (
<Modal
footer={null}
closable
title="Список абонентов для выбранного гейта"
visible={visible}
onCancel={onCancel}
onOk={handleSubmit}
destroyOnClose
width="600px"
>
<StyledDescription>
<Switch onChange={this.onChange} />
<StyledLabel>массовый ввод</StyledLabel>
</StyledDescription>
<Formik
initialValues={{ abonents: [''] }}
onSubmit={handleSubmit}
render={({ values, handleChange }) => (
<Form>
{disabled ? (
<FieldArray
name="abonents"
render={arrayHelpers => {
return (
<div>
{values.abonents.map((value, index) => (
<div key={index}>
<MyTextInput
placeholder="Абонент ID"
name={`abonents.${index}`}
value={value}
onChange={handleChange}
/>
<Button
shape="circle"
icon="delete"
onClick={() => {
arrayHelpers.remove(index);
}}
/>
</div>
))}
<Button type="dashed" onClick={() => arrayHelpers.push('')}>
<Icon type="plus" />Добавить абонента
</Button>
</div>
);
}}
/>
) : (
<StyledField
placeholder="Введите ID абонентов через запятую"
name="message"
component="textarea"
/>
)}
<Footer>
<Button type="primary" htmlType="submit">
Запросить
</Button>
</Footer>
</Form>
)}
/>
</Modal>
);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.0.0/umd/react-dom.production.min.js"></script>
Pretty easy, formik stores values inside values.abonents, hence you can use it inside textarea
let { Formik, Form, Field, ErrorMessage, FieldArray } = window.Formik;
function App () {
const [disabled, setDisabled] = React.useState(false) // some boilerplate code
function submit (values) {
console.log('submit', values)
}
return (
<Formik
initialValues={{ abonents: [] }}
onSubmit={submit}
render={({ values, handleChange, setFieldValue }) => (
<Form>
<FieldArray
name='abonents'
render={arrayHelpers => {
if (!disabled) {
return (
<textarea onChange={(e) => {
e.preventDefault()
setFieldValue('abonents', e.target.value.split(', '))
}} value={values.abonents.join(', ')}></textarea>
)
}
return (
<div>
{
values.abonents.map((value, index) => (
<div key={index}>
<input
placeholder='Абонент ID'
name={`abonents.${index}`}
value={value}
onChange={handleChange}
/>
<button onClick={(e) => {
e.preventDefault()
arrayHelpers.remove(index)
}}>
-
</button>
</div>
))
}
<button onClick={(e) => {
e.preventDefault()
arrayHelpers.push('')
}}>
+
</button>
</div>
)
}}
/>
<button type='submit'>Submit</button>
<button onClick={e => {
e.preventDefault()
setDisabled(!disabled)
}}>toggle</button>
</Form>
)}
/>
)
}
ReactDOM.render(<App />, document.querySelector('#root'))
<script src="https://unpkg.com/react#16.9.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/formik/dist/formik.umd.production.js"></script>
<div id='root'></div>
I found a solution. You have got to give the same name to the input and textarea, so whe you add text in input will automatically change text in textarea
<FieldArray
name="abonents"
render={arrayHelpers => {
and
<StyledField
placeholder="Введите ID абонентов через запятую"
name="abonents"
component="textarea"
/>
These two fields have same name so they are rendered interchangeably but have common text inside them

Resources