React antd 4 Form in Modal resetFields showing wrong data - reactjs

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

Related

Prisma, graphql and react hook forms: how to use created values in update form mutation

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

Use array of strings in React Hook Form

In a form that I am making the material that is being created in the form should have multiple width options that can be added. This means that I will have a text input where the user can add an option, and when this option is added, it should be added to the React Hook Form widthOptions array, without using the regular react state. How would one do this? How do you add an item to the total React Hook Form state, I only see options for just one input field corresponding to a property.
This is how i would do it using the regular React state
import { TrashIcon } from "#heroicons/react/24/outline";
import React, { useRef, useState } from "react";
const Test = () => {
const [widthOptions, setWidthOptions] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const removeWidthOption = (widthOption: string) => {
setWidthOptions(widthOptions.filter((option) => option !== widthOption));
};
const addWidthOption = (widthOption: string) => {
setWidthOptions([...widthOptions, widthOption]);
};
const editWidthOptions = (widthOption: string, index: number) => {
const newWidthOptions = [...widthOptions];
newWidthOptions[index] = widthOption;
setWidthOptions(newWidthOptions);
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={() => addWidthOption(inputRef?.current?.value)}>
Add Width Option
</button>
{widthOptions.map((option, index) => (
<div className="flex">
<input
type="text"
value={option}
onChange={() => editWidthOptions(option, index)}
/>
<button type="button" onClick={() => removeWidthOption(option)}>
<TrashIcon className="w-5 h-5 mb-3 text-gray-500" />
</button>
</div>
))}
</div>
);
};
export default Test;
You can just the controller component for this as for all other fields.
Since you have not shared any of you code here is a generic multi-select
<Controller
name={name}
render={({ field: { value, onChange, ref } }) => {
return (
// You can use whatever component you want here, the you get the value from the form and use onChange to update the value as you would with a regular state
<Test
widthOptions={value}
setWidthOptions={onChange}
/>
);
}}
/>;
https://react-hook-form.com/api/usecontroller/controller/
And in you Test component remove the state and get the props instead
const Test = ({widthOptions, setWidthOptions}) => {
const inputRef = useRef<HTMLInputElement>(null);
.
.
.

Hello everyone I would like to make a radio button and make it select by pressing Enter key in React

I Am failing to get the radio button selected when I press the Enter key
I tried this code here :
import { useState, useEffect } from 'react';
const HandleKeypress = () =\> {
const [itWorks, setItWorks] = useState(true)
useEffect(() => {
document.addEventListener('keypress', (e) => {
if (e.code === 'Enter') setItWorks(!itWorks)
e.preventDefault();
})
}, [])
return (
<div>
<p>{itWorks ? 'It works!' : 'It does not'}</p>
<button onClick={() => setItWorks(!itWorks)} >Press me</button>
<input type='radio' aria-selected onKeyPress={() => this.HandleKeypress } />
</div>
)
}
export default HandleKeypress;
You dont have to use onKeyPress() use onClick() instead.
You are using functional component, so, do
<input type='radio' aria-selected onClick={handleClick} />
You cant call the main component which you are working on.
So, the solution for this is
import { useState, useEffect } from 'react';
const HandleKeypress = () => {
const [itWorks, setItWorks] = useState(false)
function handleClick(){
SetItWorks(true)
}
return (
<div>
<p>{itWorks ? 'It works!' : 'It does not'}</p>
<button onClick={() =>
setItWorks(!itWorks)} >Press me</button>
<input type='radio' aria-selected onClick={handleClick} />
</div>
)
}
export default HandleKeypress;
This will work.

Show only one form field on Edit select in iteration with React and Semantic-UI-React?

I'm iterating through notes in a React component.
If the user wants to edit a note, I'd like to replace the note text with an input field, populate it with the current text value, handleChange, and only show the input field for that particular entry.
Right now it changes all of the entries because it's within the iteration in the component.
I'm using Semantic-ui-react.
Here's an image of the undesired results after the user clicks the edit icon:
My component looks like this:
import React, { Component } from "react";
import { connect } from "react-redux";
import { Comment, Table, Form, Button, Icon, Input } from "semantic-ui-react";
import { createNote } from "../actions/createNoteAction";
const uuidv4 = require("uuid/v4");
class ServiceLogsComponent extends Component {
state = {
entry: "",
showInput: false
}
handleChange = (e, { name, value }) => this.setState({ [name]: value })
handleSubmit = (e) => {
e.preventDefault()
const userId = 2;
this.props.createNote(this.state.entry, this.props.serviceId, userId)
this.setState({ entry: "" })
}
handleUpdate = e => {
this.setState({ showInput: true })
}
filterNotes = () => {
const filteredNotes = this.props.notes.filter(note => note.service_id === this.props.serviceId)
return filteredNotes
}
render() {
console.log(this.state)
return (
<>
<div style={{ display: this.props.showServiceLogs }}>
<Table>
<Table.Body>
<Comment>
<Comment.Group>
{this.filterNotes().map((serviceNote) => (
<Comment.Content>
<Table.Row key={uuidv4}>
<Table.Cell>
<Comment.Author as="a">
{serviceNote.created_at}
</Comment.Author>
</Table.Cell>
<Table.Cell>
<Comment.Metadata>
{serviceNote.user.username}
</Comment.Metadata>
</Table.Cell>
{serviceNote.user.id !== 1 ? (
<Table.Cell>
<Icon
name="edit outline"
onClick={(e) => this.handleUpdate(e)}
/>
<Icon name="delete" />
</Table.Cell>
) : null}
</Table.Row>
<Table.Row>
<Table.Cell>
{!this.state.showInput ?
<Comment.Text>{serviceNote.entry}</Comment.Text>
:
<Form onSubmit={(e) => this.handleSubmit(e)}>
<Form.Input value={serviceNote.entry} name="entry" onChange={this.handleChange}>
</Form.Input>
<Button type="submit" size="small">Update</Button>
</Form>
}
</Table.Cell>
</Table.Row>
</Comment.Content>
))}
<Form onSubmit={(e) => this.handleSubmit(e)}>
<Form.TextArea
style={{ height: "50px" }}
onChange={this.handleChange}
name="entry"
value={this.state.entry}
/>
<Form.Button
type="submit"
content="Add Note"
labelPosition="left"
icon="edit"
primary
/>
</Form>
</Comment.Group>
</Comment>
</Table.Body>
</Table>
</div>
</>
);
}
}
const mapStateToProps = state => {
return {
services: state.services.services,
notes: state.notes.notes
};
};
const mapDispatchToProps = dispatch => {
return {
createNote: (entry, serviceId, userId) => dispatch(createNote(entry, serviceId, userId))
}
};
export default connect(mapStateToProps, mapDispatchToProps)(ServiceLogsComponent);
I'm also having trouble with onChange when editing text. It doesn't change.
You have one state flag "showInput" that is shared by all of your notes, therefore when you turn it on, all of your notes go to edit mode.
In this line:
<Form.Input value={serviceNote.entry} name="entry" onChange={this.handleChange}>
The value is not updated as a result of onChange, value should be assigned to something from the state that has been updated by onChange
You should create a child component to display one note, so that each note can have its own showInput flag and value to display.

Formik Validation in a Field Array

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.

Resources