Validating Final Form Array on Click - javascript

I am building an application with React Final Form. In the form, user needs to fill up basic input fields as well as add questions with its choices. For the questions and choices I am using FieldArray feature. So, until here everything is good and working. However, I would like to add another functionality to this form.
As you can see in the image below, this is a Card component rendered inside FieldArray. Every time user clicks Add a Question button, There will be another Card component on the page.
The feature I need is to make the Save button work on top-right corner. At the moment I don't know how should I implement the Save button but what I wanna achieve is that I want to toggle the Card component in the image to another Card component, where I display the input data by using fields.value. So, no input field. However, I want to also validate this portion of the form when I click save. This is what I don't know how to do. So, every Save button will validate its own fields and if validation passes, the Card will be toggled to another Card component where the data is read-only.
So, I need your suggestion for the validation part as well as your opinion to add this functionality.
Thanks
Edit
I've been reading the docs of FinalForm as well as ReduxForm to figure out how can I handle this situation but I couldn't figure it out yet.
I've checked the Wizard example in FinalForm docs. However I am not sure if it's suitable for my situation. Wizard has a single <form> tag present at all times on a page and when a user clicks next button, input fields switch. In my case, I might need multiple form tags as you mentioned.
I've put 3 form structures as an example. Can you tell me which way is to go?
import { Form as FinalForm } from 'react-final-form'
#1 Basic Form:
Classic way of structuring the form and it is the current situation. So, this is not the way to solve the issue.
<FinalForm onSubmit={aCustomSubmitFunction}>
{
({ handleSubmit }) => {
return (
<form onSubmit={handleSubmit}>
<Field name='firstName'>{({input}) => <input {...input} />}</Field>
<Field name='lastName'>{({input}) => <input {...input} />}</Field>
<FieldArray name='questions'>
{({ fields }) => (
<div>
{fields.map((name, index) => {
return (
<div className="card">
<Field name={`${name}.question`}>{({input}) => <input {...input} type="text" />}</Field>
<button type="button" onClick={() => fields.remove(index)}>Delete</button>
<button type="submit">Save</button>
</div>
)
})}
<button type="button" onClick={() => fields.push({ question: undefined })}>Add a Question</button>
</div>
)}
</FieldArray>
<button type="submit">Submit</button>
</form>
)
}
}
</FinalForm>
#2 Multiple forms under a single FinalForm:
Multiple forms inside a FinalForm. This seems to be the way to go, however 'save' button submits the entire form not its own form. It is using the same handleSubmit so this must be the reason, though how can I have a different handleSubmit? Wrapping the form tag that is inside FieldArray with another FinalForm?
<FinalForm onSubmit={aCustomSubmitFunction}>
{
({ handleSubmit }) => {
return (
<>
<form onSubmit={handleSubmit}>
<Field name='firstName'>{({input}) => <input {...input} />}</Field>
<Field name='lastName'>{({input}) => <input {...input} />}</Field>
<button type="submit">Submit</button>
</form>
<FieldArray name='questions'>
{({ fields }) => (
<div>
{fields.map((name, index) => {
return (
<form onSubmit={handleSubmit}>
<div className="card">
<Field name={`${name}.question`}>{({input}) => <input {...input} type="text" />}</Field>
<button type="button" onClick={() => fields.remove(index)}>Delete</button>
<button type="submit">Save</button>
</div>
</form>
)
})}
<button type="button" onClick={() => fields.push({ question: undefined })}>Add a Question</button>
</div>
)}
</FieldArray>
</>
)
}
}
</FinalForm>
#3 Multiple nested forms under a single FinalForm:
This is invalid html. So this must be wrong approach but while I was doing a research I found a thing called React Portals, which might be helpful but I think it's unnecessary.
<FinalForm onSubmit={aCustomSubmitFunction}>
{
({ handleSubmit }) => {
return (
<form onSubmit={handleSubmit}>
<Field name='firstName'>{({input}) => <input {...input} />}</Field>
<Field name='lastName'>{({input}) => <input {...input} />}</Field>
<FieldArray name='questions'>
{({ fields }) => (
<div>
{fields.map((name, index) => {
return (
<form>
<div className="card">
<Field name={`${name}.question`}>{({input}) => <input {...input} type="text" />}</Field>
<button type="button" onClick={() => fields.remove(index)}>Delete</button>
<button type="submit">Save</button>
</div>
</form>
)
})}
<button type="button" onClick={() => fields.push({ question: undefined })}>Add a Question</button>
</div>
)}
</FieldArray>
<button type="submit">Submit</button>
</form>
)
}
}
</FinalForm>

To validate only part of a form, you must split it into multiple forms, and have the "Save" button "submit" the form. The Wizard Example does this, collecting the form values from each "page" in a parent component.
Hope this helps...

Related

Updating default values of a React-Hook-Form, does not populate filelds with useFieldArray

This problem has consumed a lot of time trying to figure out what is wrong. I have a React-Hook-Form (v6.14.1) that needs to populate dynamic data, based on the component state.
On the initial load, everything works fine. If I change the state all updated data are displaying fine, except the dynamic data.
Here is a codesandbox link. If it does not render due to a library error, just hit the preview refresh button.
The goal is that the WAN 1 tab, on initial load displays the dynamic fields (WAN 1 VLAN-1) and WAN2 does not have dynamic fields to display. Hitting the Update Config button, WAN1 should not have dynamic fields to display and WAN2 should display one (WAN 2 VLAN-1). The problem is that WAN2 does not display it.
I have searched for similar questions, but all of them were about the values of the populated fields and not about displaying the fields themselves. I have used the reset method of react-hook-form and the defaltValue for each dynamic field as react-hook-form documentation suggests.
On App.js I have the state, a button that updates the state, and the Form component which has the state as property.
const [configdata, setConfigdata] = useState(config);
return (
<div className="App">
<UpdateConfig onClick={() => setConfigdata(configUpdated)} />
<Form
formData={configdata}
handleFormData={(data) => console.log(data)}
/>
</div>
);
}
On Form.js there is a Rect-hook-form FormProvider and the WanFields component that dynamically populates form fields.
<FormProvider {...methods}>
<form
onSubmit={methods.handleSubmit((data) =>
props.handleFormData(data)
)}
>
<Tab.Content>
{props.formData?.intfs?.length &&
props.formData?.intfs.map((intf, index) => (
<Tab.Pane key={index} eventKey={`wan${index}-tab`}>
<WanFields
key={`wan${index}-fields`}
intfNo={index}
portTypeOptions={props.portTypeOptions}
data={intf}
/>
</Tab.Pane>
))}
</Tab.Content>
</form>
</FormProvider>
Every time the props.formData update, there is a useEffect that reset the forms' default data.
const methods = useForm({ defaultValues: props.formData });
useEffect(() => {
methods.reset(props.formData);
}, [props.formData]);
In WanFields.js, there are all the form fields, and the useFieldArray method, that will populate the dynamic fields based on the forms' default values and a watch field value (watchIntfType ).
const methods = useFormContext();
const { errors, control, watch, register } = methods;
const { fields, append, remove } = useFieldArray({
control,
keyName: "fieldid",
name: `intfs[${intfNo}].subIntfs`
});
const watchIntfStatus = watch(`intfs[${intfNo}].enabledStatus`);
const watchIntfType = watch(`intfs[${intfNo}].enabled`);
Dynamic fields are populated as follows
{watchIntfType?.value >= "2" && (
<>
<div className="form-group">
<div className="btn btn-success" onClick={append}>
Add
</div>
</div>
<div id={`accordion-${intfNo}`}>
<Accordion>
{console.log("FIELDS", fields)}
// This is where the problem starts. fields are empty after updating data
{fields.map((field, index) => {
return (
<Card key={field.fieldid}>
<Accordion.Toggle
as={Card.Header}
variant="link"
eventKey={`${index}`}
style={{ cursor: "pointer" }}
>
<h4>
WAN {parseInt(intfNo) + 1}{" "}
<span style={{ margin: "0px 5px" }}>
<i className="fas fa-angle-right"></i>
</span>{" "}
VLAN-{index + 1}
</h4>
<div className="card-header-action">
<button
type="button"
className="btn btn-danger"
onClick={() => remove(index)}
>
Remove
</button>
</div>
</Accordion.Toggle>
<Accordion.Collapse eventKey={`${index}`}>
<Card.Body>
<div className="form-row">
<div className="form-group col-12 col-md-6">
<label>IP</label>
<input
type="text"
className="form-control"
name={`intfs[${intfNo}].subIntfs[${index}].ipAddress`}
defaultValue={field?.ipAddress}
ref={register()}
/>
</div>
<div className="form-group col-12 col-md-6">
<label>Subnet</label>
<input
type="number"
className="form-control"
min="0"
max="30"
name={`intfs[${intfNo}].subIntfs[${index}].subnet`}
defaultValue={field?.subnet}
ref={register()}
/>
</div>
</div>
</Card.Body>
</Accordion.Collapse>
</Card>
);
})}
</Accordion>
</div>
</>
)}
The problem is that when the state updates, form default values are updated, but the method useFieldArray attribute fields are not updated and stay as an empty array. I really cannot understand, what I am doing wrong. Any help will be much appreciated.
I don't know if is a correct method but i have resolv this probleme with method reset in a useEffect.
https://react-hook-form.com/api/useform/reset
defaultValues:
{
acvDesignOffice: generateRSEnv.acvDesignOffice,
earthQuakeZone: generateRSEnv.earthQuakeZone,
buildings: generateRSEnv.buildings,
},
useEffect(() => {
reset({
acvDesignOffice: generateRSEnv.acvDesignOffice,
earthQuakeZone: generateRSEnv.earthQuakeZone,
buildings: generateRSEnv.buildings,
});
}, [generateRSEnv]);

React - Show modal when user clicks on submit, don't hit endpoint until modal button clicked

I have a standard form in react thats built with react final form. When the user clicks submit, I show a modal to them by updating state to reveal the modal. The modal has two options which update state. I want to wait for one of the options to be clicked before firing the post endpoint.
Here is the form
<div>
<Wrapper>
<Form
onSubmit={onSubmit}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit} noValidate>
// Fields are here
<Modal
isOpen={showModal}
handleAcceptClick={handleAcceptClick}
handleDeclineClick={handleDeclineClick}
/>
<Button
buttonType="submit"
/>
</form>
)}
/>
</Wrapper>
</div>
// This is the onSubmit methiod
async function onSubmit(submission: Payload) {
setShowModal(true);
await api.create({
submission,
});
}
In the modal component theres a callback to setstate in the same file the form is in, based on if they accept or deny. How can I wait for that state happen before I call the api.create method in on submit.
HTML:
<div>
<Wrapper>
<Form
onSubmit={onSubmit}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit} noValidate>
// Fields are here
<Modal
isOpen={showModal}
handleAcceptClick={handleAcceptClick}
handleDeclineClick={handleDeclineClick}
/>
<Button
buttonType="button"
onClick={confirm}
/>
</form>
)}
/>
</Wrapper>
</div>
JS CODE:
function confirm() {
setShowModal(true);
}
// call this function on click of one option in modal
function onConfirm(){
onSubmit();
}

form.validateFields() doesnt work when we have custom antd form component

Considering the following example, this is stopping us to create custom components inside forms using antd4 version.
const handleFormSubmit = () => {
form
.validateFields()
.then((values: any) => {
console.log('success values => ', JSON.stringify(values));
successCallback(values);
})
.catch((errorInfo: any) => {
console.log('failureCallback values => ', JSON.stringify(errorInfo));
failureCallback(errorInfo);
});
};
<Form
form={form}
layout="vertical"
name="normal_login"
className="login-form"
initialValues={store.formData.initialValues}
>
<Form.Item>
<Input placeholder="Name" />
</Form.Item>
<Button type="primary" htmlType="submit" onClick={handleFormSubmit}>
Create
</Button>
</Form>
This works absolutely fine, whereas if the component is custom, then it doesn't work. Example:
function CustomInput(props){
return (
<Form.Item>
<Input placeholder={props.name} />
</Form.Item>
)
}
<Form
form={form}
layout="vertical"
name="normal_login"
className="login-form"
initialValues={store.formData.initialValues}
>
<CustomInput name="Name" />
Will display the field and also validates on change event. HandleFormSubmit is called, but it's not triggering success or failure block.
<Button type="primary" htmlType="submit" onClick={handleFormSubmit}>
Create
</Button>
</Form>
What's wrong here?
Try this instead of your Custom JSX
function CustomInput(props){
return (
<Form.Item name={props.name}> # Update this line only and remove this comment #
<Input placeholder={props.name} />
</Form.Item>
)
}
<Form
form={form}
layout="vertical"
name="normal_login"
className="login-form"
initialValues={store.formData.initialValues}
>
<CustomInput name="Name" />
<Button type="primary" htmlType="submit" onClick={handleFormSubmit}>
Create
</Button>
</Form>
NOTE: In Antd if your using Form.Item then you have to set name there
not on input fields. Form.Item assign its value to its Input.
I hope your doubt is solved comment for more views. I also tired of antd and wasted many days to understand this.

React - Formik - Field Arrays - implementing repeatable form fields

I am trying to follow the Formik documentation on using FieldArrays so that I can add repeatable form elements to my form.
I've also seen this Medium post setting out an example.
I'm slow to learn and can't join the dots between the documentation and the implementation.
I want to have a button in my main form that says: "Add a request for data".
If that button is selected, then a nested form setting out the data profile is displayed, along with "add another data request" and "remove" buttons.
I have made the nested form in another component in my application, but I'm struggling to figure out how to use the example from the medium post to incorporate the nested form (as a repeatable element - ie someone might want 5 data requests).
Are there any examples of how to implement this?
In my code, I have basically followed the medium post, but tried to link the Data Request form component inside the index
<button
type="button"
onClick={() => arrayHelpers.insert(index, <DataRequestForm />)}>
Add a data request
</button>
This is plainly incorrect, but I can't get a handle on how to do this.
Taking Nithin's answer, I've tried to modify the embedded form so that I can use react-select, as follows, but I'm getting an error which says:
TypeError: Cannot read property 'values' of undefined
import React from "react";
import { Formik, Form, Field, FieldArray, ErrorMessage, withFormik } from "formik";
import Select from "react-select";
import {
Button,
Col,
FormControl,
FormGroup,
FormLabel,
InputGroup,
Table,
Row,
Container
} from "react-bootstrap";
const initialValues = {
dataType: "",
title: "",
description: '',
source: '',
}
class DataRequests extends React.Component {
render() {
const dataTypes = [
{ value: "primary", label: "Primary (raw) data sought" },
{ value: "secondary", label: "Secondary data sought"},
{ value: "either", label: "Either primary or secondary data sought"},
{ value: "both", label: "Both primary and secondary data sought"}
]
return(
<Formik
initialValues={initialValues}
render={({
form,
remove,
push,
errors,
status,
touched,
setFieldValue,
setFieldTouched,
handleSubmit,
isSubmitting,
dirty,
values
}) => {
return (
<div>
{form.values.dataRequests.map((_notneeded, index) => {
return (
<div key={index}>
<Table responsive>
<tbody>
<tr>
<td>
<div className="form-group">
<label htmlFor="dataRequestsTitle">Title</label>
<Field
name={`dataRequests.${index}.title`}
placeholder="Add a title"
className={"form-control"}
>
</Field>
</div>
</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="dataRequestsDescription">Description</label>
<Field
name={`dataRequests.${index}.description`}
component="textarea"
rows="10"
placeholder="Describe the data you're looking to use"
className={
"form-control"}
>
</Field>
</div>
</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="dataRequestsSource">Do you know who or what sort of entity may have this data?</label>
<Field
name={`dataRequests.${index}.source`}
component="textarea"
rows="10"
placeholder="Leave blank and skip ahead if you don't"
className={
"form-control"}
>
</Field>
</div>
</td>
</tr>
<tr>
<td>
<div className="form-group">
<label htmlFor="dataType">
Are you looking for primary (raw) data or secondary data?
</label>
<Select
key={`my_unique_select_keydataType`}
name={`dataRequests.${index}.source`}
className={
"react-select-container"
}
classNamePrefix="react-select"
value={values.dataTypes}
onChange={selectedOptions => {
// Setting field value - name of the field and values chosen.
setFieldValue("dataType", selectedOptions)}
}
onBlur={setFieldTouched}
options={dataTypes}
/>
</div>
</td>
</tr>
<tr>
<Button variant='outline-primary' size="sm" onClick={() => remove(index)}>
Remove
</Button>
</tr>
</tbody>
</Table>
</div>
);
})}
<Button
variant='primary' size="sm"
onClick={() => push({ requestField1: "", requestField2: "" })}
>
Add Data Request
</Button>
</div>
)
}
}
/>
);
};
};
export default DataRequests;
You cannot add nested forms inside a form element.
Please refer the below post for mode details.
Can you nest html forms?
If you are looking to nest multiple fields with a nested structure, inside a main form, you can achieve it using FieldArrays.
You can structure the form like.
{
firstName: "",
lastName: "",
dataRequests: []
}
Here firstName and lastName are top level form fields and dataRequests can be an array where each element follows the structure
{
requestField1: "",
requestField2: ""
}
Since dataRequests is an array, for rendering each item of FieldArray you need a map function.
form.values.dataRequests.map( render function() )
And for each rendered item, the change handlers should target their index to update the correct item in FieldArray.
<div key={index}>
<Field
name={`dataRequests.${index}.requestField1`}
placeholder="requestField1"
></Field>
<Field
name={`dataRequests.${index}.requestField2`}
placeholder="requestField2"
></Field>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
In the above snippet name={dataRequests.${index}.requestField1} asks formik to update the key requestField1 of nth element of dataRequests array with the value of the input field.
Finally your <DataRequest /> component might look something like below.
import React from "react";
import { Field } from "formik";
export default ({ form, remove, push }) => {
return (
<div>
{form.values.dataRequests.map((_notneeded, index) => {
return (
<div key={index}>
<Field
name={`dataRequests.${index}.requestField1`}
placeholder="requestField1"
></Field>
<Field
name={`dataRequests.${index}.requestField2`}
placeholder="requestField2"
></Field>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
);
})}
<button
type="button"
onClick={() => push({ requestField1: "", requestField2: "" })}
>
Add Data Request
</button>
</div>
);
};
And using <FieldArray /> you can connect <DataRequest /> to the main form.
You can try out the below sample SO snippet
function DataRequests({ form, remove, push }){
return (
<div>
{form.values.dataRequests.map((_notneeded, index) => {
return (
<div key={index}>
<Formik.Field
name={`dataRequests.${index}.requestField1`}
placeholder="requestField1"
></Formik.Field>
<Formik.Field
name={`dataRequests.${index}.requestField2`}
placeholder="requestField2"
></Formik.Field>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
);
})}
<button
type="button"
onClick={() => push({ requestField1: "", requestField2: "" })}
>
Add Data Request
</button>
</div>
);
};
class Home extends React.Component {
initialValues = {
firstName: "",
lastName: "",
dataRequests: []
};
state = {};
render() {
return (
<div>
<Formik.Formik
initialValues={this.initialValues}
onSubmit={values => {
this.setState({ formData: values });
}}
>
{() => {
return (
<Formik.Form>
<div>
<Formik.Field
name="firstName"
placeholder="First Name"
></Formik.Field>
</div>
<div>
<Formik.Field
name="lastName"
placeholder="Last Name"
></Formik.Field>
</div>
<Formik.FieldArray name="dataRequests" component={DataRequests} />
<button type="submit">Submit</button>
</Formik.Form>
);
}}
</Formik.Formik>
{this.state.formData ? (
<code>{JSON.stringify(this.state.formData, null, 4)}</code>
) : null}
</div>
);
}
}
ReactDOM.render(<Home />, document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/formik/dist/formik.umd.production.js"></script>
<div id="root"></div>
For anyone looking to learn from this post, the answer to this question from nithin is clearly motivated by good intentions, but it isn't a correct deployment of formik. You can check out a code sandbox here: https://codesandbox.io/embed/goofy-glade-lx65p?fontsize=14 for the current attempt at solving this problem (still not fixed) but a better step toward a solution. Thanks just the same for the helpful intentions behind the answer to this question.

Display emoji object to text field or textarea in React

I'm building my app with emoji-mart lib.
I have text input like this:
<FormGroup>
{emoji}
<EmojiMartPicker
set='emojione'
onSelect={(emoji) => console.log(emoji)}
onChange={this.onChange}
>
<Input
type="text"
name="emotion"
bsSize="sm"
autoComplete="off"
value={report.emotion.colons}
onChange={this.onHandleFormChange}
required
/>
</EmojiMartPicker>
</FormGroup>
Now, I wanna display Emoji Object to text input. In value attribute. I want to display emoji, not value text.
How can we do that?
See my detail problem:
https://codesandbox.io/s/646xom9y1z
Sorry, I found that just only add native props to solved my problem.
Like this:
value={report.emotion.native}
That's it..
I solve this problem take a look.
<div className="chatemoji">
<ButtonToolbar >
<div onClick={e => e.preventDefault()}>
{/* <EmojiField
name="textarea"
onChange={this.onChange.bind(this)}
fieldType="input"
/> */}
<EmojiField name="my-textarea" onChange={this.onChange.bind(this)} fieldType="input" />
</div>
</ButtonToolbar>
</div>
In onChange you have to call this code
onChange(e,value) {
this.state.data += value;
this.setState({ data: this.state.data });
}

Categories

Resources