I have a formik form like this:-
import Drawer from "components/atoms/Drawer";
/* import Input from "components/atoms/Input";
import InputGroup from "components/atoms/InputGroup";
import Label from "components/atoms/Label"; */
import Scrim from "components/atoms/Scrim";
import DrawerBody from "components/molecules/DrawerBody";
import { Field, FieldArray, Form, FormikErrors, FormikProps, withFormik } from "formik";
import { ITrackedPage } from "hocs/withAppDynamics";
import * as React from "react";
import { Box, Flex, Text } from "rebass";
import * as AmenitiesActions from "store/amenities/actions";
import { IAmenity, IAmenityRanking } from "store/amenities/models";
import DrawerHeader from "./DrawerHeader";
// import ErrorMessage from 'components/atoms/ErrorMessage';
interface IAmenitiesDrawerProps {
drawerOpen: boolean;
onDrawerClose: () => void;
tenantAssessmentId: string;
actions: typeof AmenitiesActions;
maxRank?: number;
}
interface IAmenitiesDrawerValues {
amenitieslist: IAmenity[];
}
const InnerForm: React.FC<
IAmenitiesDrawerProps & ITrackedPage & FormikProps<IAmenitiesDrawerValues>
> = ({
errors,
drawerOpen,
onDrawerClose,
handleChange,
values,
setValues,
isValid,
tenantAssessmentId,
sendAnalyticsData,
actions
}) => {
const handleDrawerClose = () => {
onDrawerClose();
};
return (
<>
<Scrim isOpen={drawerOpen} onClose={handleDrawerClose} />
<Drawer isOpen={drawerOpen} direction="right" drawerWidth="700px">
<DrawerHeader handleCloseDrawer={handleDrawerClose} />
<DrawerBody p={5}>
<Flex mb={4}>
<Box flex={1}>
<Text fontWeight="light" fontSize={4} mt={3} mb={4}>
Add custom amenities
</Text>
</Box>
</Flex>
<Form>
<FieldArray
name="amenitieslist"
render={arrayHelpers => (
<div>
{// values.amenitieslist && values.amenitieslist.length > 0 ? (
values.amenitieslist.map((amenity, index) => (
<div key={index}>
<Field name={`amenitieslist.${index}.name`} />
<button
type="button"
onClick={() => arrayHelpers.remove(index)} // remove a amenity from the list
>
-
</button>
{errors.amenitieslist}
</div>
))}
<button type="button" onClick={() => arrayHelpers.push({ id: "", name: "", imageUrl: '', description: '', tenantAssessmentId })}>
{/* show this when user has removed all amenities from the list */}
Add a Amenity
</button>
</div>
)}
/>
<div>
<button type="submit">Submit</button>
</div>
</Form>
</DrawerBody>
</Drawer>
</>
);
};
export const AmenitiesDrawer = withFormik<IAmenitiesDrawerProps, IAmenitiesDrawerValues>({
enableReinitialize: true,
handleSubmit: (values, {props}) => {
const seed: number = props.maxRank? props.maxRank : 0;
const amenityRankings: IAmenityRanking[] = values.amenitieslist.map((a, index)=>({
amenityId: 1,
rank: index + 1 + seed,
amenityName: a.name,
customAmenity: true
}));
console.log(amenityRankings);
console.log(props.actions.customSaveAmenities);
console.log(props.tenantAssessmentId);
props.actions.customSaveAmenities(props.tenantAssessmentId, amenityRankings);
},
mapPropsToValues: ({tenantAssessmentId}) => ({
amenitieslist:[{id: 0, name: '', imageUrl: '', description: '', tenantAssessmentId}]
}),
validate: values => {
const errors: FormikErrors<{ validAmenity: string }> = {};
console.log('In the Validate method');
const { amenitieslist } = values;
const amenityValid = amenitieslist[0].name.length < 28;
if (!amenityValid) {
console.log('Amenity is not valid');
errors.validAmenity = "Amenity needs to be atmost 28 characters";
console.log(errors);
}
return errors;
}
})(InnerForm);
As you all can see I have a text input. I want to throw a error message below the text field when the length is more than 28 characters long.
How is this possible ? Please help me with this.
I find the most convenient way to validate Formik forms is using yup as recommended in their documentation. You can define a validation schema and pass it as a prop to the main Formik component (or HOC as it appears you're using) and remove your custom validation function:
validationSchema: yup.object().shape({
amenitieslist: yup.array()
.of(yup.object().shape({
name: yup.string().max(28, "Max 28 chars")
// Rest of your amenities object properties
}))
})
And then in your FieldArray:
<FieldArray
name="amenitieslist"
render={arrayHelpers => (
<div>
{ values.amenitieslist && values.amenitieslist.length > 0 ? (
values.amenitieslist.map((amenity, index) => (
<div key={index}>
<Field name={`amenitieslist[${index}].name`} />
<button
type="button"
onClick={() => arrayHelpers.remove(index)} // remove a amenity from the list
>
-
</button>
{errors.amenitieslist[index].name}
</div>
))}
<button type="button" onClick={() => arrayHelpers.push({ id: "", name: "", imageUrl: '', description: '', tenantAssessmentId })}>
{/* show this when user has removed all amenities from the list */}
Add a Amenity
</button>
</div>
)}
/>
I just changed the accessors you were using to designate the field name (to use an index for an array element, you have to use bracket notation) and where to find the errors, yup should generate them automatically. Tough to know for sure I'm not missing anything without testing it, hope this helps!
I wasn't using yup so the other solutions weren't useful for me. What I did was is have another value that can represent your error. I loop through my fieldarray and assign an error.
values.payments.forEach((element) => {
if (Number(element.isBTC) === 1 && !values.btcCompanyId) {
errors.btcCompany =
"For this payment account, BTC Company must be selected";
}
});
You can make something like this for yourself. I sometimes have an _action value given to the last button and have errors show there. You'll figure it out. Not the right solution if you want the error to display for each fieldarray.
Related
I am trying to make a form that has to create and update functionality. At the moment, when I get to the update form, I can enter new values and they do save to psql, but the form loads with blank fields instead of the values used to create the existing object. I can't find the settings I need to apply to use the existing object values in the update form so that it loads with the fields displaying the currently saved values.
My form has:
import * as React from "react"
import { CgTrash, CgEditMarkup } from "react-icons/cg"
import { VscIssues } from "react-icons/vsc"
import { gql } from "#apollo/client"
import {
Box,
Button,
ButtonGroup,
IconButton,
useDisclosure,
Wrap,
Stack,
Table,
Tbody,
Tr,
Td,
TableContainer,
Text
} from "#chakra-ui/react"
import Head from "next/head"
import { useToast } from "lib/hooks/useToast"
import { useForm } from "lib/hooks/useForm"
import { Form } from "components/Form"
import { FormError } from "components/FormError"
import { Input } from "components/Input"
import { Textarea } from "components/Textarea"
import {
// IssueGroupInput,
useAllIssueGroupsQuery,
useCreateIssueGroupMutation,
useUpdateIssueGroupMutation,
useDeleteIssueGroupMutation,
IssueGroup as IssueGroupGQLType,
} from "lib/graphql"
//
// import { AdminCreateIssueGroupForm } from "components/AdminCreateIssueGroupForm"
// import { AdminUpdateIssueGroupForm } from "components/AdminUpdateIssueGroupForm"
import { AdminLayout } from "components/AdminLayout"
import { AuthedHomeLayout } from "components/AuthedHomeLayout"
import { Modal } from "components/Modal"
import Yup from "lib/yup"
const __ = gql`
mutation CreateIssueGroup($data: IssueGroupInput!) {
createIssueGroup(data: $data) {
id
title
}
}
query AllIssueGroups {
allIssueGroups {
id
title
}
}
mutation updateIssueGroup($id: String!) {
updateIssueGroup(id: $id) {
id
title
}
}
mutation deleteIssueGroup($id: String!) {
deleteIssueGroup(id: $id) {
id
title
}
}
`
const IssueGroupSchema = Yup.object().shape({
title: Yup.string().required(),
description: Yup.string().required(),
// issues: Yup.string()
})
function IssueGroups() {
const toast = useToast()
// const [search, setSearch] = React.useState("")
// const [selectedIssueGroups, setSelectedIssueGroups] = React.useState<string[]>([])
const modalProps = useDisclosure()
const modalPropsUpdate = useDisclosure()
// // const [sort, setSort] = React.useState<Sort>({ createdAt: SortOrder.Desc })
const [createIssueGroup] = useCreateIssueGroupMutation()
const [updateIssueGroup] = useUpdateIssueGroupMutation()
const [deleteIssueGroup] = useDeleteIssueGroupMutation()
const { data: issueGroups } = useAllIssueGroupsQuery()
const defaultValues = {
title: "",
description: "",
// issues: "",
}
const form = useForm({ defaultValues, schema: IssueGroupSchema })
const [selectedIssueGroup, setSelectedIssueGroup ] = React.useState<Omit<
IssueGroupGQLType,
"createdAt" | "updatedAt" | "issues"
> | null>(null)
const { isOpen, onOpen, onClose } = useDisclosure()
const onCreateSubmit = async (data: Yup.InferType<typeof IssueGroupSchema>) => {
return await form.handler(
() =>
createIssueGroup({ variables: { data } }),
{
onSuccess: async (res, toast) => {
// await refetchAllIssueGroups()
toast({
title: "IssueGroup created",
status: "success",
})
form.reset()
onClose()
}
}
)
}
const handleUpdateSubmit = async (data: Yup.InferType<typeof IssueGroupSchema>) => {
console.log(selectedIssueGroup.id)
return await form.handler(
() =>
updateIssueGroup({ variables: {
id: selectedIssueGroup.id,
// issues: "none",
data: { ...data }
} }),
)
}
const onDeleteIssueGroup = async (id: string) => {
return
form.handler(
() =>
deleteIssueGroup({ variables: { id } }),
{
onSuccess: async () => {
await fetch("api/issueGroups", { method: "delete"})
toast({
title: "IssueGroup deleted",
status: "success",
})
},
// refetchAllIssueGroups()
},
)
}
return (
<Box>
<Head>
<title>trying to figure out how to crud in prisma react hook forms</title>
</Head>
<Wrap mb={4} spacing={2}>
<Button
onClick={modalProps.onOpen}
leftIcon={<Box boxSize="18px" as={VscIssues} />}
color="brand.white"
fontWeight="normal"
backgroundColor="brand.orange"
_hover={{
backgroundColor: "brand.green",
color: "brand.white",
}}
>
Create issueGroup
</Button>
</Wrap>
<Modal {...modalProps} title="Create IssueGroup">
{/* <AdminCreateIssueGroupForm onClose={modalProps.onClose} /> */}
<Form {...form} onSubmit={onCreateSubmit}>
<Stack>
<Input name="title" label="Title" />
{/* <Input name="description" label="Description" /> */}
{/* <Text mb='8px' fontWeight="medium" fontSize="sm" > Description</Text> */}
<Textarea name="description" label="Describe" rows={4} />
<FormError />
<ButtonGroup>
<Button onClick={modalProps.onClose}>Cancel</Button>
<Button
type="submit"
isLoading={form.formState.isSubmitting}
isDisabled={form.formState.isSubmitting}
color="brand.white"
fontWeight="normal"
backgroundColor="brand.orange"
_hover={{
backgroundColor: "brand.green",
color: "brand.white",
}}
>
Create
</Button>
</ButtonGroup>
</Stack>
</Form>
</Modal>
<TableContainer maxW="80%">
<Table variant='simple'>
<Tbody>
{issueGroups?.allIssueGroups.map((issueGroup) => (
<Tr key={issueGroup.id}>
<Td>
<Text textStyle="h6">{issueGroup.title}</Text>
</Td>
<Td>
<ButtonGroup>
<IconButton
variant='outline'
color="brand.blue"
fontWeight="normal"
backgroundColor="brand.white"
aria-label='Update Issue Group'
fontSize='15px'
ml={4}
icon={<CgEditMarkup />}
// onClick={modalPropsUpdate.onOpen}
onClick={() => {
setSelectedIssueGroup(issueGroup)
modalPropsUpdate.onOpen()
}}
_hover={{
backgroundColor: "brand.blue",
color: "brand.white",
}}
/>
{/* {
selectedIssueGroup && isOpen &&
<Modal issueGroup={selectedIssueGroup} isOpen={isOpen} onClose={onClose} title="Edit IssueGroup" >
} */}
<Modal {...modalPropsUpdate } title="Edit IssueGroup" >
{/* <AdminUpdateIssueGroupForm
onClose={modalPropsUpdate.onClose}
issueGroup={selectedIssueGroup} */}
{/* /> */}
<Form {...form} onSubmit={handleUpdateSubmit}>
<Stack>
<Input name="title" label="Title" defaultValue={selectedIssueGroup.title} />
<Textarea name="description" label="Describe" rows={4} defaultValue={selectedIssueGroup.description} />
// I also tried using the defaultValue as issueGroup.title/description, but those don't load the values either. When I try with issueGroup.title, the modal loads with empty fields (there are values saved in psql). When I try with selectedIssueGroup.title, the error message says:
TypeError: Cannot read properties of null (reading 'title').
I know there is a title because it renders on the page when I list existing objects. I also know the update is working because I can see psql update.
<FormError />
<ButtonGroup>
<Button onClick={modalPropsUpdate.onClose}>Cancel</Button>
<Button
type="submit"
isLoading={form.formState.isSubmitting}
isDisabled={form.formState.isSubmitting}
color="brand.white"
fontWeight="normal"
backgroundColor="brand.orange"
_hover={{
backgroundColor: "brand.green",
color: "brand.white",
}}
>
Save changes
</Button>
</ButtonGroup>
</Stack>
</Form>
</Modal>
<IconButton
variant='outline'
color="brand.tomato"
fontWeight="normal"
backgroundColor="brand.white"
aria-label='Call Sage'
fontSize='15px'
ml={4}
icon={<CgTrash />}
onClick={() => onDeleteIssueGroup(issueGroup.id)}
_hover={{
backgroundColor: "brand.tomato",
color: "brand.white",
}}
/>
</ButtonGroup>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
)
}
IssueGroups.getLayout = (page: React.ReactNode) => <AuthedHomeLayout><AdminLayout>{page}</AdminLayout></AuthedHomeLayout>
export default IssueGroups
ARJUN'S SUGGESTIONS
I tried each of Arjun's suggestions in his answer below. The results were as follows:
First suggestion - being to add a use effect statement to the issues.tsx to try and set the value of issueGroup. To do this, I added this effect beneath the definition of setSelectedIssueGroup:
useEffect(()=>{
if(selectedIssueGroup)
modalPropsUpdate.onOpen()
}, [selectedIssueGroup]);
When I try this, I get an error in my vsCode that says:
React Hook useEffect has a missing dependency: 'modalPropsUpdate'.
Either include it or remove the dependency array.
modalPropsUpdate is defined above that useEffect as:
const modalPropsUpdate = useDisclosure()
To address that error, I tried commenting out that line. In that case, the useEffect is:
useEffect(()=>{
if(selectedIssueGroup)
// modalPropsUpdate.onOpen()
}, [selectedIssueGroup]);
The error message says:
expression expected.
I don't know what this means.
When I try to load the local host with this error, the error message is:
Error: x Unexpected token }. Expected this, import, async,
function, [ for array literal, { for object literal, # for decorator,
function, class, null, true, false, number, bigint, string, regexp, `
I think Arjun might be expecting that I would put async and await somewhere in this solution, but I don't know where to put them.
| for template literal, (, or an identifier
The onClick handler for update still has:
onClick={() => {
setSelectedIssueGroup(issueGroup)
modalPropsUpdate.onOpen()
}}
Second suggestion - being to use value instead of defaultValue in the input fields.
When I try this, by removing defaultValue and adding: value={issueGroup?.title} to the inputs, the modal loads, with blank fields, but which update with the saved value when I click inside the input field.
However, I can't update the value even if this UX issue were okay (it isn't). The field becomes fixed and I can't type in it.
Third suggestion - being to define the defaultValues by using setValue. To implement this, I tried:
const { setValue } = useForm();
const defaultValues = {
title: setValue("title", selectedIssueGroup?.title),
description: setValue("description", selectedIssueGroup?.description),
// issues: "",
}
const form = useForm({ defaultValues, schema: IssueGroupSchema })
The modal loads (but with a blacked-out screen behind it, whereas other successful attempts just blur the background), but the saved values are not revealed in the input fields of the update form.
SANDBOX
I tried to make a sandbox for this form here. I don't know how to add the server connections to generate the types or mutations inside the code sandbox, so instead, I copied the prisma db extracts.
Try 1:
Possibly the state update completes after you open the modal because you wrote
setSelectedIssueGroup(issueGroup)
modalPropsUpdate.onOpen() // immediately opening modal
Here state update will take some time to set selectedIssueGroup. before that modal gets opened and you have null as default state value so you might get the error.
You need to open the modal once the state got set. You can achieve this using useEffect.
useEffect(()=>{
if(selectedIssueGroup)
modalPropsUpdate.onOpen()
}, [selectedIssueGroup]);
Try 2:
if your modal gets mounted to DOM even before opening it, then your defaultValue will be set null. In that case you have to use value prop in <Input /> instead of defaultValue to set your values.
and handle null in input like
<Input name="title" label="Title" defaultValue={selectedIssueGroup?.title} />
Try 3:
you can get the setValue from useForm() like this,
const { setValue } = useForm();
and use it to set any field value with name
setValue("title", selectedIssueGroup.title);
I have a custom form imported from node_module as import FormRenderer from "#data-driven-forms/react-form-renderer/form-renderer";
this form requires schema in json object to then convert them to html element, I want to add icon and onClick functionality to show the password using this form?
schema = {
name: "password",
label: "Password",
type: "password",
component: componentTypes.TEXT_FIELD,
placeholder: "Password",
placeholdericon: "/images/lock.svg",
},
You need a component that supports this functionality. In this case we need a custom component.
Have a look at this codepen for an example.
import React, { useState } from "react";
import FormRenderer from "#data-driven-forms/react-form-renderer/form-renderer";
import useFieldApi from "#data-driven-forms/react-form-renderer/use-field-api";
import useFormApi from "#data-driven-forms/react-form-renderer/use-form-api";
const Button = ({ children, label, ...props }) => (
<button {...props}>{label}</button>
);
const FormTemplate = ({ formFields }) => {
const { handleSubmit, onCancel } = useFormApi();
return (
<form
onSubmit={(event) => {
event.preventDefault();
handleSubmit();
}}
>
{formFields}
<Button type="submit" label="Submit" />
<Button type="button" label="cancel" onClick={onCancel} />
</form>
);
};
const Password = (props) => {
const {
customProp,
label,
input,
isRequired,
meta: { error, touched },
icon,
...rest
} = useFieldApi(props);
const [hidden, hide] = useState(true);
return (
<div>
<label htmlFor={input.name}>
{isRequired && <span>* </span>}
{label}
</label>
<input
type={hidden ? "password" : "text"}
id={input.name}
{...input}
{...rest}
/>
<button onClick={() => hide(!hidden)} type="button">
{icon} {hidden ? "Show" : "Hide"}
</button>
{touched && error && <p>{error}</p>}
</div>
);
};
const componentMapper = {
password: Password,
};
const schema = {
fields: [
{
component: "password",
name: "password",
label: "Your password",
icon: "🤐",
},
],
};
const ComponentMapper = () => {
return (
<FormRenderer
componentMapper={componentMapper}
FormTemplate={FormTemplate}
schema={schema}
onSubmit={(values) => console.log(values)}
onCancel={() => console.log("cancel action")}
/>
);
};
export default ComponentMapper;
Hi I want to create a form set like this:
If I click Add One, it adds one. If I press x, it deletes one.
How can I produce this in formik?
Is there any easy way to do it?
In my opinion I'll use useFormik hook I think this hook is easy to understand.
(The final code is place at the end)
First: declare initialValues to have array field like this AND it should have unique id because if you use index as key when map sometimes react will not render correctly. In this example I use uuid
const formik = useFormik({
initialValues: {
contacts: [
{
id: uuid(),
name: "",
email: "",
},
],
}
});
Then: I'll create 2 function for handle adding and handle removing
const handleAddField = () => {
formik.setFieldValue("contacts", [
...formik.values.contacts,
{ id: uuid(), name: "", email: "" },
]);
};
const handleRemoveField = (id) => {
formik.setFieldValue(
"contacts",
formik.values.contacts.filter((contact) => contact.id !== id)
);
};
Finally: Just render it with map like this
<form onSubmit={formik.handleSubmit}>
{formik.values.contacts.map((contact, index) => (
<div key={contact.id}>
<label>Name</label>
<input {...formik.getFieldProps(`contacts[${index}].name`)} />
<label>Email</label>
<input {...formik.getFieldProps(`contacts[${index}].email`)} />
<button type="button" onClick={() => handleRemoveField(contact.id)}>
Delete
</button>
</div>
))}
<button type="submit">Submit</button>
<button type="button" onClick={handleNewField}>
Add Field
</button>
</form>
The final code look like this - here is a codesandbox
import React from "react";
import { v4 as uuid } from "uuid";
import { useFormik } from "formik";
function App() {
const formik = useFormik({
initialValues: {
contacts: [
{
id: uuid(),
name: "",
email: "",
},
],
},
onSubmit: (values) => {
console.log(values);
},
});
const handleNewField = () => {
formik.setFieldValue("contacts", [
...formik.values.contacts,
{ id: uuid(), name: "", email: "" },
]);
};
const handleRemoveField = (id) => {
formik.setFieldValue(
"contacts",
formik.values.contacts.filter((contact) => contact.id !== id)
);
};
return (
<div>
<form onSubmit={formik.handleSubmit}>
{formik.values.contacts.map((contact, index) => (
<div key={contact.id}>
<label>Name</label>
<input {...formik.getFieldProps(`contacts[${index}].name`)} />
<label>Email</label>
<input {...formik.getFieldProps(`contacts[${index}].email`)} />
<button type="button" onClick={() => handleRemoveField(contact.id)}>
Delete
</button>
</div>
))}
<button type="submit">Submit</button>
<button type="button" onClick={handleNewField}>
Add Field
</button>
</form>
</div>
);
}
export default App;
What do you mean with easy?
These are the steps you need in order to create a dynamic form in Formik
Save the fields in a state variable
const [fields, setFields] = useState([{name: "facebook", value: "facebook.com"}])
Generate the initialValues for the form, something like
const [initialValues, setInitialValues] = useState({})
useEffect(() => {
const tempInitialValues = {};
fields.forEach(field => {
if (!tempInitialValues[field.name]) {
tempInitialValues[field.name] = field.value;
}
});
setInitialValues(tempInitialValues);
}, [fields]);
Add fields to the dependency array so it is re-generated every time you add or remove a field
Add your functions to add and remove fields
const addNewField = () => {
setFields(fields => [
...fields,
{
name: "instagram",
value: "instagram.com"
}
]);
};
Render your Form dynamically
<Formik initialValues={initialValues}>
{props => (
<div>
<form>{fields.map(field => {
<Field/>
}}</form>
</div>
)}
</Formik>
I am having trouble understanding why I am seeing this unexpected behavior. If I have some data that I am passing into a prop on an object that has a modal with a form done as shown below and I click on a button, then cancel, then click on a different button, the form is showing the previously clicked state each time. Within the useEffect code, I can see that the correct person is loaded, though the form initializes the previous person.
import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Button, Modal, Form, Input } from "antd";
const PersonEditForm = ({ person, onCreate, onCancel }) => {
const [form] = Form.useForm();
const prevRef = useRef();
useEffect(() => {
if (prevRef.current === null && person) {
form.resetFields();
}
prevRef.current = person;
});
return (
person && (
<Modal
visible={!!person}
title="Person Editor"
okText="Create"
cancelText="Cancel"
onCancel={onCancel}
onOk={() => {
form
.validateFields()
.then(values => {
onCreate(values);
})
.catch(info => {
console.log("Validate Failed:", info);
});
}}
>
<Form
form={form}
layout="vertical"
name="form_in_modal"
initialValues={person}
>
<Form.Item
name="name"
label="Name"
rules={[
{
required: true,
message: "Please input the person's name!"
}
]}
>
<Input />
</Form.Item>
</Form>
</Modal>
)
);
};
const CollectionsPage = () => {
const [person, setPerson] = useState(null);
const onCreate = values => {
console.log("Received values of form: ", values);
setPerson(null);
};
return (
<div>
<Button
type="primary"
onClick={() => {
setPerson({ name: "Bob" });
}}
>
Edit Bob
</Button>
<Button
type="primary"
onClick={() => {
setPerson({ name: "John" });
}}
>
Edit John
</Button>
<Button
type="primary"
onClick={() => {
setPerson({ name: "Phil" });
}}
>
Edit Phil
</Button>
<PersonEditForm
person={person}
onCreate={onCreate}
onCancel={() => {
setPerson(null);
}}
/>
</div>
);
};
ReactDOM.render(<CollectionsPage />, document.getElementById("container"));
However, if I remove the person && ( from my <PersonEditForm> component, it works as expected. Can anyone explain the subtle difference I'm running into here? I have down-stream complex form components that weren't handling a null state, so I wanted to only create the modal if a person was being edited.
The code doesn't work because of some funny race condition arising from misusing form.resetForm to change initialValues. You would probably have to go into the actual implementation of initialValues to figure out what is going wrong.
However, if instead of reseting the form inside of useEffect, you were to assign the clicked person value using form.setFieldsValue(person), your code would work as intended.
https://codesandbox.io/s/hungry-christian-cdn7t?file=/src/index.js
I need to generate a certain number of fields according to a value that the user prompts in the first step of the form.
Since I'm doing a multi-step form with a "Wizard" class <Condition /> component doesn't work.
Basically I need to access to values (pull them from the class inside the functional component) in order to tell React the number of "pages" it needs to generate.
Something like this:
export default () => {(
<Wizard
initialValues={{ }}
onSubmit={onSubmit}
>
<Wizard.Page >
<FirstStep />
</Wizard.Page>
{values.items.map((item, index) => (
<Wizard.Page key="index">
<SecondStep stepName="item.name" key="index" />
</Wizard.Page>
) )}
</Wizard>
)}
In order to generate N pages according to the number of items the user created, one page for each item.
This approach does not work because it tells me that the values are not defined.
This is my Wizard React Component:
import React from 'react'
import PropTypes from 'prop-types'
import { Form } from 'react-final-form'
import arrayMutators from 'final-form-arrays'
export default class Wizard extends React.Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired
}
static Page = ({ children }) => children
constructor(props) {
super(props)
this.state = {
page: 0,
values: props.initialValues || {}
}
}
next = values =>
this.setState(state => ({
page: Math.min(state.page + 1, this.props.children.length - 1),
values
}))
previous = () =>
this.setState(state => ({
page: Math.max(state.page - 1, 0)
}))
validate = values => {
const activePage = React.Children.toArray(this.props.children)[
this.state.page
]
return activePage.props.validate ? activePage.props.validate(values) : {}
}
handleSubmit = values => {
const { children, onSubmit } = this.props
const { page } = this.state
const isLastPage = page === React.Children.count(children) - 1
if (isLastPage) {
return onSubmit(values)
} else {
this.next(values)
}
}
render() {
const { children } = this.props
const { page, values } = this.state
const activePage = React.Children.toArray(children)[page]
const isLastPage = page === React.Children.count(children) - 1
return (
<Form
initialValues={values}
validate={this.validate}
onSubmit={this.handleSubmit}
mutators={{...arrayMutators }}
>
{({ handleSubmit, submitting, values }) => (
<form onSubmit={handleSubmit}>
{activePage}
<div className="buttons centered">
{page > 0 && <button type="button" onClick={this.previous} >« Previous </button>}
{!isLastPage && <button type="submit">Next »</button>}
{isLastPage && <button type="submit" disabled={submitting}>Submit</button>}
</div>
<pre>{JSON.stringify(values, 0, 2)}</pre>
</form>
)}
</Form>
)
}
}
And this is the FirstStep component (where the user inserts its first values, it generates and array of items. All I need to do is pull the values of the state in order to generate a number of pages according to the length of that first array):
export default () => (
<FieldArray name="items">
{({ fields }) => (
<div className="centered-items">
{fields.map((name, index) => (
<div key={name} className="item-row">
<label htmlFor={`item-name-${index}`}>item: </label>
<Field
id={`item-name-${index}`}
name={`${name}.item`}
component="input"
type="text"
placeholder="Place Item Name"
/>
<button type="button" onClick={() => fields.remove(index)}>
Remove
</button>
</div>
))}
<button
className="centered"
type="button"
onClick={() => fields.push({ tipologia: '', numero_camere: '1' })}
>
Add Item
</button>
</div>
)}
</FieldArray>
)
I manage to solve this problem calling React.cloneElement() method.
This method needs to wrap either activePage variable and the static method "Page", in this way you can pass properties to children, so at least I manage to access the form values inside every page and so create some conditions.
You can see a working example here: Demo - Codesandbox
I still haven't figured out how to access this values outside the page (inside Wizard Class but before Wizard.Page method, this could be the ideal way because I can create conditions in order to generate/(or not generate) pages.