I have an antd Modal, i am trying to validate a field and provided validation to it. How can i enable/disable the Ok button based on the validation. If the validation is successful then button should be enabled else disabled.
<Form>
<Modal
title={modalHeader}
okText="ADD FIELD"
cancelText="CANCEL"
visible={visible}
onCancel={onCancelHandler}
onOk={() => onAddFieldHandler(fieldName)}
width={600}
okButtonProps={{disabled:true}}
>
<p>Please provide name</p>
<Form.Item
name="fieldName"
rules={[{ required: true, message: 'Please enter name' }]}
>
<FieldNameInput
placeholder="Field name..."
value={fieldName}
onChange={(event) => setFieldName(event.target.value)}
/>
</Form.Item>
</Modal>
</Form>
You can use onFieldsChange from Antd Forms API togehter with geFieldsError and the okButtonProps from Antd Modal API.
const [form] = Form.useForm();
const [buttonDisabled, setButtonDisabled] = useState(true);
return (
<Modal
...
okButtonProps={{ disabled: buttonDisabled }}
>
<Form
form={form}
...
onFieldsChange={() =>
setButtonDisabled(
form.getFieldsError().some((field) => field.errors.length > 0)
)
}
>
Here is a working Stackblitz.
In my case I had Form inside modal and there is onFieldChange prop when you can pass function to perform some operations due to changes on from so you can sth like that:
const SomeModal = ({ visible }) => {
const [form] = Form.useForm();
const [buttonDisabled, setButtonDisabled] = useState(true);
const handleOk = () => form.submit();
const handleAfterClose = () => {
setButtonDisabled(true);
form.resetFields();
}
const handleCancel = () => ...some action to hide modal;
const handleFormFinish = (values) => {
... some logic here
}
return (
<Modal
title={"Some title..."}
visible={visibile}
onOk={handleOk}
onCancel={handleCancel}
afterClose={handleAfterClose}
okButtonProps={{ disabled: buttonDisabled }}
>
<Form
form={form}
layout="vertical"
name="acceptform"
onFinish={handleFormFinish}
initialValues={{
...initial values here
}}
onFieldsChange={() => {
const actualFieldValues = form.getFieldsValue();
const anyError = form.getFieldsError().some((field) => field.errors.length > 0);
.. some logic if error etc..
if (anyError) {
setButtonDisabled(true);
}
else {
setButtonDisabled(false);
}
}}
>
and of course there is need to have some validators on fields
<Form.Item
label={"someLabel"}
id="field"
name="field"
hasFeedback
rules={[
{
type: "string",
validator: async (rule, value) => inputFieldValidate(value, "custom message")
},
]}
>
and validator looks alike:
const inputFieldValidate = async (value, message) => {
if (someCondition)) {
return Promise.reject(message);
}
return Promise.resolve();
};
here is some nice to know that validator isasync and to make it work without any warnings just handle promises
Having the Form inside the Modal, a way to update modal button status would be just running form instance's validateFields, but there are two things to take into account:
This function is a Promise, so the state must update after an await with the validation results.
I've experienced some looping issues when using onFieldsChange (maybe the validation triggers some kind of field update). Instead, onValuesChange has worked good enough for me.
Running the validation into a setTimeout callback seems to be mandatory. Doing it without the setTimeout returns a validation error even when all the fields are valid because of an outOfDate: true. It seems to be because of how the Antd Form update lifecycle works, and waiting until this process has ended (what we can easily achieve with the setTimeout) solves that problem.
A succesful validation returns the form values object, a failed one returns an errorInfo object with the errors list, the outOfDate status and the current form values. You can use the errors list in the latter to get the validation messages returned by Antd to display more descriptive and specific feedback.
In the end, my approach has this structure:
const MyModal = ({onFinish, ...otherProps}) => {
const [canSubmit, setCanSubmit] = useState(false);
const [form] = Form.useForm();
return (
<Modal
{...otherProps}
okButtonProps={{
disabled: !canSubmit
}}
>
<MyFormComponent
form={form}
onFinish={onFinish}
onValuesChange={() => {
setTimeout(() => {
form
.validateFields()
.then(() => {
/*
values:
{
username: 'username',
password: 'password',
}
*/
setCanSubmit(true);
})
.catch((err) => {
/*
errorInfo:
{
values: {
username: 'username',
password: 'password',
},
errorFields: [
{ name: ['password'], errors: ['Please input your Password!'] },
],
outOfDate: false,
}
*/
setCanSubmit(false);
});
});
}}
/>
</Modal>
);
};
Related
In my React-Admin app, I'd like to leverage react-hook-form's useFormContext for various things, such as, for example, setting the default pre-selected choice in this custom input field:
...
import {
Create, SimpleForm, SelectInput
} from 'react-admin';
import { useFormContext } from 'react-hook-form';
const MyInput = () => {
const formContext = useFormContext();
formContext.setValue('category', 'tech');
return (
<SelectInput source="category" choices={[
{ id: 'tech', name: 'Tech' },
{ id: 'people', name: 'People' },
]}
/>
);
};
...
const ItemCreate = () => {
return (
<Create>
<SimpleForm>
<MyInput />
</SimpleForm>
</Create>
);
};
...
This sets the pre-selected value of the field, just as intended. But it throws a warning: Cannot update a component ("Form") while rendering a different component ("MyInput")...
Is there some way to achieve this without getting the warning?
Note: The only reason I'm using a custom input field here is because when I put useFormContext() directly into the component that contains SimpleForm it returns null (similarly described here).
The warning is related to the fact that the entire body of the MyInput() function is executed during each render, you need to call the setValue() function inside the useEffect hook.
Got this working by moving formContext.setValue into a useEffect hook:
...
import {
Create, SimpleForm, SelectInput
} from 'react-admin';
import { useFormContext } from 'react-hook-form';
const MyInput = () => {
const formContext = useFormContext();
// moved the setValue into a useEffect
useEffect(() => {
formContext.setValue('category', 'tech');
});
return (
<SelectInput source="category" choices={[
{ id: 'tech', name: 'Tech' },
{ id: 'people', name: 'People' },
]}
/>
);
};
...
const ItemCreate = () => {
return (
<Create>
<SimpleForm>
<MyInput />
</SimpleForm>
</Create>
);
};
...
I've been using React hook form and have a compare validation written on the input itself.
I need to move over to Yup validation lib as others on the project are using it so for consistency. Here is what I currently have that is working but having problems when I use a yup.schema.
const {
register,
setValue,
getValues,
formState: { errors, isValid },
clearErrors,
trigger,
} = useForm({
mode: 'onBlur',
defaultValues: {
password: '',
compare: '',
},
})
return(
<div>
<Input
name='password'
{...register('password', { required: true })}
<Input
name='compare'
{...register('compare', {
validate: (value: string) => {
const { password} = getValues()
return password === value || ''
},
})}
</div>
)
As I said this above works but now I thought I could add a schema but that breaks all what I have so I'm trying to figure out how I can achieve this using yup schema.
const schema = yup.object().shape({
password: yup.string().required(),
compare: yup.string().test(
'compare',
(field) => {
... not sure how to compare against another filed here ?
}
),
})
const {
register,
setValue,
getValues,
formState: { errors, isValid },
clearErrors,
trigger,
} = useForm({
mode: 'onBlur',
resolver: yupResolver(schema),
defaultValues: {
password: '',
compare: '',
},
})
I have a simple form that submits an update request to a server. I would like to display a success/error message for a few seconds after the form is submitted and then reset the form. However, if the user happens to submit subsequent requests in between the last submit and the end of the delay to perform the reset, then I don't want to perform the reset.
To accomplish this I was hoping to use the submitCount found in the FormState. However, I am finding that the submitCount is resetting automatically - even when I don't call reset at all. Also when I call reset and pass to it keepSubmitCount: true
Here's a skeleton of what I'm doing:
import { IToolPanelParams, SelectionChangedEvent } from "ag-grid-community";
import React, { useEffect, useMemo, useState } from "react";
import styled from "styled-components";
import { FieldValues, SubmitErrorHandler, SubmitHandler, useForm, useFormState} from "react-hook-form";
interface MyFieldValues extends FieldValues {
note: string;
}
export const MyToolPanel = (props: IToolPanelParams) => {
const [selected, setSelected] = useState<Record[]>();
const { register, handleSubmit, reset, control } = useForm<MyFieldValues>();
const { errors, submitCount, isSubmitting, isSubmitted, isSubmitSuccessful } = useFormState({ control });
...
const onSaveSubmit: SubmitHandler<MyFieldValues> = async (data, e) => {
return peform_update();
}
useEffect(() => {
if(isSubmitted) {
console.log(`successful? (${isSubmitSuccessful}) submit on #${submitCount}`);
const currSubmitCount = submitCount;
setTimeout(() => {
if(!isSubmitting && submitCount == currSubmitCount) {
console.log(`reset after submit #${currSubmitCount}`);
/*reset({
keepValues: true,
keepDefaultValues: true,
keepSubmitCount: true
});
*/
}
}, 3000);
}
}, [isSubmitted, submitCount, reset])
const onError: SubmitErrorHandler<BulkEditNotesFieldValues> = (errors) => {
console.error(`BulkEditNotesToolPanel::onError - errors.note. type: ${errors.note?.type}. types: ${JSON.stringify(errors.note?.types)}. message: ${errors.note?.message}`);
}
return (
<Container>
<Spin spinning={isSubmitting}>
<h2>Bulk Edit Records</h2>
{ !!selected?.length && (<>
<form onSubmit={handleSubmit(onSaveSubmit, onError)}>
<label htmlFor="Note">
<textarea
style={textAreaStyle}
{...register("note", {
required: { value: true, message: 'Note required'},
pattern: { value: /[^\s]/, message: 'Note cannot be empty'}
})}
/>
</label>
{ errors?.note && (
<Error>{errors.note.message}</Error>
)}
<button
type="submit"
style={submitStyle}
disabled={!selected?.length}
>
Save { selected?.length > 1? 'Records' : 'Record' }
</button>
</form>
</>)}
{ isSubmitted && isSubmitSuccessful && (
<SuccessMessage>Successfully Updated The Selected Record(s)</SuccessMessage>
)}
{ updateError && (<>
<Error>An error occurred while attempting to update the selected records...</Error>
</>)}
</Spin>
</Container>
);
};
While I can of course create my own submit counter via useState, I was hoping to figure out why react-hook-form's submitCount is resetting on me and how to prevent that? Thanks!
I am using antd Select and react hook form via 'Controller'. I am populating the Select options from a fetched data with structure;
{
{
"id": "232342",
"term": "hello"
}
{
"id": "232342",
"term": "hello"
}
}
the Select component properly displays the term for selection. However, i want to retrieve both the 'id'and 'term' of the selected and use it to populate another json object.
getValues(" ") retrieves the 'id' only. How do i retrieve and access both the 'id' and 'term'.
Here is a portion of code:
import React from 'react'
import { useForm, Controller } from 'react-hook-form'
import { Select } from 'antd'
const { Option } = Select
export default function PatientRegistrationForm({ options }) {
const { register, handleSubmit, getValues, control, formState: { errors } } = useForm({
defaultValues: {
customer: "",
}
})
const children = []
for (const {id, pt: {term}} of options){
children.push(<Option key={id}>{term}</Option>)
}
// Define the retrieved values from the form
const retrievedID = getValues("customer")
// Use the retreived values to populate this object
const customer = {
customerId = retrievedID
customerName = "nothing happens here"
},
return (
<div className="">
<form onSubmit={handleSubmit(onSubmit)} className="">
<section>
<Controller
control={control}
name="customer"
render={({ field }) => (
<Select {...field} defaultValue=""
bordered={true}
filterOption={true}
className="form-control"
>
{ children }
</Select>
)}
/>
</section>
</form>
</div>
);
}
Thanks in advance for your help.
You have to retrieve the option manually using something like:
const retrievedID = getValues("customer")
const retrievedOption = options.find(option => option.id === retrievedID)
const customer = {
customerId: retrievedID,
customerName: retrievedOption.term
}
thank you #sgarcia.dev for your answer. I know its been a while but i want to put it out here incase it helps someone else. It turns out it had little to do with react hook form. Ant design select component has a prop 'labelInValue' which returns an object containing both the label and value.
I want to trigger the validation only for input "blur" instead of typing on it. So I have used the method validateTrigger:'onBlur' and its worked fine for me as expected. After user focus/click again the input, the validation message should disappear, any solution?
<FormItem>
{getFieldDecorator('email', {
rules: [
{
type: 'email', message: 'Valid E-mail required!',
},
{ required: true, message: 'E-mail required!', whitespace:true }
],
validateTrigger:'onBlur'
})(
<Input
placeholder="Email"
/>
)}
This can be done by using ant design validateStatus parameter. You can dynamically change the statuses from given sets, i.e, 'success', 'warning', 'error', 'validating'.
validateStatus: validate status of form components which could be 'success', 'warning', 'error', 'validating'.
You need to functions, one onBlur and other onFocus. FormItem can be written as:
<FormItem
validateStatus={this.state.showError && userNameError ? "error" : ""}
help={
this.state.showError && userNameError
? "Valid E-mail required!"
: ""
}
>
{getFieldDecorator("userName", {
validateTrigger: "onBlur",
rules: [
{ type: "email", message: "Valid E-mail required!" },
{ required: true, message: "Please input your username!" }
]
})(
<Input
placeholder="Email"
onBlur={this.handleBlur}
onFocus={this.handleFocus}
/>
)}
</FormItem>
And the two methods can be written as:
handleBlur = e => {
this.setState({ showError: true });
};
handleFocus = e => {
this.setState({ showError: false });
};
Working demo is on codesandbox.io.