-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Hook form lib #41658
Hook form lib #41658
Conversation
Pinging @elastic/es-ui |
💚 Build Succeeded |
subscribe(fn: Listener<T>): Subscription { | ||
this.callbacks.add(fn); | ||
|
||
fn(this.value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth popping this inside a
setTimeout(() => { fn(this.value) }, 0);
so that the JS scheduler runs it async and any logic right after subscription (like setup logic) doesn't run after the sub listener.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point! Thanks for the catch 👍
💚 Build Succeeded |
💚 Build Succeeded |
👏 This is great work Seb! I haven't looked at the code yet, just the PR description. Thanks for taking the time to explain your reasoning so thoroughly and providing examples. Overall I think this makes a ton of sense and will provide enormous value.
When do we expect consumers to use this default? Do we need it?
To give some background here, de/serialization logic is broadly used to convert ES API responses into a format suitable for editing in a form, and convert that form data back into a shape that is suitable for consumption the API. I think of it as the mechanism we use to uncouple the ES APIs from our UI. Some examples of what we currently use it to do are:
Can the form library handle these use cases? Can it handle any arbitrary change we may want to make to the shape of the ES object based on the user input? As a corollary, if we decide to preserve de/serialization in its current form in the codebase, then do we need the
What’s the reason behind the nesting of this parameter? In other words, why
Did you consider an option like this? <UseField
path="title"
form={form}
>
{({ field: { errors, isSubmitted, value, onChange, isValidating } }) => (
<TitleField
errors={errors}
isSubmitted={isSubmitted}
value={value}
onChange={onChange}
isValidating={isValidating}
any={1}
additional={2}
prop={3}
/>
)}
</UseField>
{/* If manually passing through those props is necessary _every_ time,
then the consumer can solve this problem with composition. */}
function passFieldPropsToElement(element) {
// If the consumer defines their props with different names that what is provided by
// field then they could perform that mapping here.
return ({ field }) => (
React.cloneElement(element, { ...field });
);
}
<UseField
path="title"
form={form}
>
{passFieldPropsToElement(
<TitleField
any={1}
additional={2}
prop={3}
/>
)}
</UseField> This still separates concerns and has the added benefit of reducing UseField's interface and decoupling
This schema seems great! Do we even need the config prop? Can we remove support for the
I see a lot of complexity here to enable reusable validators and I’m not convinced of their value yet (or the value of the error codes)… can we simplify this for now and add back functionality later once the value of reusable validators is more obvious to all of us? For example, we could begin by expecting consumers to define the validation array as an array of validator functions (not objects). Then, once the need for reusable validators becomes obvious to all of us, we could add support for validator objects as well, which will behave the way you’ve described them. This change will be backwards compatible because the implementation code can check to see whether a function or an object has been provided. I just want to point out that with this change, the consumer can still create their own reusable validators, by moving the logic for customizing the message to the validator factory function. I think the consumer would gain even more control over how to customize the validator's behavior/messages this way: validation: [
startsWithCharField('.', ({ char }) => (
`The title can't start with: ${char}`
)), // validator factories can have any kind of customization logic you can think of
}] On a separate note, is there any way to validate a field when the user is interacting with a different field? For example, in the Rollup Job Wizard, the rollup index and index pattern fields are both validated when the user interacts with one of them.
Is this necessary? Is it possible to check if the validator function returns a thenable, and use that to infer that it’s async?
Nice!!! :D
This is awesome! I suggest we use as generic a name as possible here, e.g. |
}); | ||
} | ||
}), | ||
Promise.resolve() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool pattern! I've never thought of Promise.resolve()
as the empty case.
I guess this could be slightly less verbose with the imperative alternative:
for (const validation of validations) {
const validationResult = await runValidation(validation);
// ...
(but you don't have to implement, just wanted to point out the cool use of Promise
+reduce
here. 🙂)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, I updated with the imperative alternative. 👍
Great work @sebelga! It was really fun to read this PR and learn more about general form building that happens across applications. I'm sure that this can create a lot of value 👍. I'd like to add another thought in addition to @cjcenizal . My gut feeling at this point is that there are libraries (like formik+yup for forms and rxjs for observables) that do very similar things to this lib and which could be leveraged to reduce our code size. I have not done an investigation here so I can't say what we would gain or lose. So I'd like to know whether there is specific reasoning behind not using them (and wrapping them?) vs. rolling our own? |
It is always a good practice to have a default. Some consumers might have styled their
Nothing has changed, and obviously, business logic needed for serialization will still be needed. I was only referring that there will not be the need of having those anymore: export const serializeAutoFollowPattern = ({ indexPatterns, remoteCluster }) => ({
remote_cluster: remoteCluster,
inex_patterns: indexPatterns
}) As we can set correctly the path: Again, it's the dev that decides how he wants to structure the objects. The form library does not handle any business logic.
Yes, as I might prefer to have my serializer defined on the field and you might decide to have them elsewhere.
What’s the reason behind the nesting of this parameter? In other words, why
Did you consider an option like this?
This schema seems great! Do we even need the config prop? Can we remove support for the
Is this necessary? Is it possible to check if the validator function returns a thenable, and use that to infer that it’s async?
This is awesome! I suggest we use as generic a name as possible here, e.g.
|
💔 Build Failed |
💔 Build Failed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sebelga this looks great! Can't wait to start using it :)
I believe I tested through all the different scenarios you provided in your comment. I found a couple typos that I commented on and had one question about duplicate code.
A couple other comments:
- I noticed when
formatters
is provided, it transform the value in the UI as well. So even if I typed "north carolina" it was transformed to "NORTH CAROLINA". Is that intentional? - I wondering the same thing @cjcenizal mentioned as far as if it's necessary to have both the
config
prop andschema
- I was having trouble testing the validation. Is there anything else I need to do besides providing the validation array with the validator func in the schema, like so:
validation: [
{
validator: ({ value }) => {
if ((value as string).startsWith('.')) {
return {
code: 'ERR_FORMAT_ERROR',
message: `The title can't start with a dot (.)`,
};
}
},
},
],
- I think it would be super helpful if you transferred your comments on this PR into a readme as part of this PR. WDYT?
|
||
useEffect(() => { | ||
subscription.current = form.__formData$.current.subscribe(data => { | ||
// To avoid re-rendering the the children for updates on the form data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: delete extra the
return validationResult; | ||
}; | ||
|
||
// Execute each validations for the field sequencially |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo sequencially --> sequentially
// we will clear the errors as soon as the field value changes. | ||
clearErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]); | ||
|
||
const runValidation = async ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks like the same code as in validateSync
except with async/await. is it possible to remove some of this duplicate code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was my initial goal but did not find a clean way to do it. One requires an async function to run and the other not. Suggestions welcome!
* method allows us to retrieve error messages for certain types of validation. | ||
* | ||
* For example, if we want to validation error messages to be displayed when the user clicks the "save" button | ||
* _but_ in caase of an asynchronous validation (for ex. an HTTP request that would validate an index name) we |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo caase --> case
|
||
/** | ||
* Remove all fields whose path starts with the pattern provided. | ||
* Usefull for removing all the elements of an Array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type Usefull --> Useful
} | ||
|
||
if (throwIfNotFound) { | ||
throw new Error(`Can't acess path "${path}" on ${JSON.stringify(object)}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo acess --> access
Thanks for the review @alisonelizabeth !
This is the purpose of the formatter. 😊 If you have a "numeric" field, you'd want a formatter to convert the string to int for example.
Yes as for small forms you might just have 2/3 simple field and just the need to add a
If you were using the |
@sebelga thanks for explaining! i was able to get the validation to work; i think i had mistakenly defined |
💚 Build Succeeded |
💚 Build Succeeded |
@sebelga Nice work! I read through the changes you made re ...
const { form } = useForm({ ... });
return (
<FormProvider form={form}>
<form onSubmit={form.submit} noValidate>
<UseField path="title" />
<button>Send form</button>
</form>
</FormProvider>
); that we can add something like this: ...
const { form } = useForm({ ... });
return (
<Form>
<UseField path="title" />
<button>Send form</button>
</Form>
); to wire it up for us? Just a very |
I guess you meant I thought of it, but the pattern In Kibana it should be But I agree it would be much nicer :) [EDIT]: Although, based on your idea, we could have
But suddently having 2 Form tag seems strange :/. Maybe keep the name |
Thanks for the clarification! Yes that makes sense now! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did a final look through the code - discussed offline that we'd still add some tests. Happy that my concerns have been addressed, lgtm!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💎 This looks great Seb!! I do think we could ensure a stabler dev path by removing stripWhitespace
for now and introducing it once we've used this in production a couple times to prove the use-case, but I don't want to block this PR on that. I also had a few questions and suggestions for comments and nits and stuff, nothing important though. Great work!
// that we are **not** interested in, we can specify one or multiple path(s) | ||
// to watch. | ||
if (pathsToWatch) { | ||
const valuesToWatchToArray = Array.isArray(pathsToWatch) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I think valuesToWatchArray
works better here. valuesToWatchToArray
sounds like a function for converting something to an array.
export interface ArrayItem { | ||
id: number; | ||
path: string; | ||
isNew: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the role of isNew
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It allows the consumer to know if the item has been created (by an addItem()
call) or if the comes from the defaultValue.
isNew: boolean; | ||
} | ||
|
||
export const UseArray = ({ path, initialNumberOfItems, children }: Props) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd love to see some brief docs around UseArray, for example:
/**
* Use UseArray to control fields that contain a selection of items, e.g. EuiComboBox and
* EuiSelectable.
*/
}: Props) => { | ||
const form = useFormContext(); | ||
|
||
if (typeof defaultValue === 'undefined') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a benefit to this form over defaultValue === undefined
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No real benefit, just the way I used to write it. 😊
export const VALIDATION_TYPES = { | ||
FIELD: 'field', // Default validation error (on the field value) | ||
ASYNC: 'async', // Throw from asynchronous validations | ||
ARRAY_ITEM: 'arrayItem', // If the field value is an Array, this error would be thrown if an _item_ of the array is invalid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is "throw" still accurate in this comment and the comment above since validation doesn't throw errors any more?
formatters = [], | ||
fieldsToValidateOnChange = [path], | ||
isValidationAsync = false, | ||
errorDisplayDelay = form.options.errorDisplayDelay, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just providing one perspective as a consumer of this code -- it's ultimately your decision.
}; | ||
|
||
const runSync = () => { | ||
// Sequencially execute all the validations for the field |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: sequencially -> sequentially
const validateFields: Form<T>['__validateFields'] = async fieldNames => { | ||
// Fields that are dirty have already been validated when their value changed. Thus their errors array | ||
// already contains any possible error. | ||
// So when *no* fieldNames is provided, we only validate fields that haven't been changed by the user. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey Seb, I'm sorry I'm still having trouble wrapping my head around this. Could you explain step by step the state changes made to a field when a user enters input, and how those state changes affect whether the field is validated/not validated here? "Only validate fields that haven't been changed by the user" is just not making sense to me... isn't the point of validation to check if a user's changes are valid or not? Maybe we should Zoom?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a field changes, this method is called with a fieldNames array. It will only validate the fields provided in the fieldNames
array.
If we call this function without fieldNames (probably when a user clicks the sendForm button), we only validate the fields that are pristine (= that haven't been changed by the user).
We have 2 fields: "name" and "lastName"
- A user changes the "name"
form.validateFields(['name']
is called and thus the name field has the validation- The user clicks the sendForm button
form.validateFields()
is called (no fieldNames are provided). ---> we only need to validate the "lastName" fields as "name" has already been validated
Is it clearer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh, I see. Your second example really clarified the behavior of the code for me. I think the desired outcome (optimizing to only validate fields which haven't been validated) makes sense to me. But I don't believe the code communicates this intention and I'm afraid it will be very easy to accidentally break this behavior.
I suggest adding a isValidated
state to fields (forgive me if this already exists; I haven't looked at the code in awhile). I suggest setting this state to true once validation executes for a given field, and to false once the user enters input. I believe this will make the intended behavior explicit and protect against someone accidentally breaking it. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this adds unnecessary complexity and I think the comment above explains well that a dirty field is a field that has been validated (due to the fact that this method is called whenever its value changes).
// use_field.ts (inside the useEffect() { ... }, [value])
await form.__validateFields(fieldsToValidateOnChange);
// use_form.ts (when calling submit())
const isFormValid = await validateFields();
protect against someone accidentally breaking it
When tests will be in place, we shouldn't worry about it.
We can work on the phrasing of the comment if you want to still make it more clear, but I don't think an extra state is needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll move forward with whatever you think makes the most sense. I'll just share a couple parting thoughts:
- If the validation is async, it's possible for a field to be dirty, but not yet validated. I don't know how this affects the system but thought I should point it out.
- Tests are great for letting a dev know they broke something. But to figure out why something is broken (or why it was working in the first place) I usually find myself reading the code. And I did genuinely have a difficult time understanding why this code works the way it does. :) Maybe others will too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the validation is async, it's possible for a field to be dirty, but not yet validated.
If you look at line L114 you will see that the form won't be valid until all the fields have finished validating.
if (options.stripEmptyFields) { | ||
return Object.entries(fields).reduce( | ||
(acc, [key, field]) => { | ||
if (field.value !== '') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In response to my question about multiple whitespace, you replied:
Yes multiple whitespace === an empty field.
But in this check ' ' === ''
will evaluate to false. I just want to double-check that this is what you intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch 👍
// The validator returned a Promise. | ||
// Abort and run the validations asynchronously | ||
hasToRunAsync = true; | ||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this can be simplified a bit by removing hasToRunAsync
(haven't tested this though).
if (!!validationResult.then) {
// The validator returned a Promise.
// Abort and run the validations asynchronously
return runAsync();
}
We also don't need to worry about clearing validationErrors
if each helper defines it in its own scope.
const runAsync = async () => {
const validationErrors: ValidationError[] = [];
/* ... */
}
const runSync = () => {
const validationErrors: ValidationError[] = [];
/* ... */
}
continue; | ||
} | ||
|
||
if (!!validationResult.then) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm following this logic correctly, this means we first call validator
and if it returns a promise, then we call runAsync
which will call validator
a second time. If the validator creates a network request, does that mean the request will be sent twice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point. I added support to cancel the validation. The consumer can optionally add a cancel
method to the promise he returns (in case of an HTTP request the best would be to use axios that has a cancelation API.
This cancel()
method will be called before validating again the field.
Here is a "manual" example for a simple Promise:
let counter = 0;
....
validations: [
{
validator: () => {
let canceled = false;
const current = counter;
console.log('Validating', current);
const promise = new Promise(resolve => {
setTimeout(resolve, 2000);
}).then(() => {
if (canceled) {
console.log('Has been canceled, should not do anything....', current);
} else {
console.log('Finished long validation!', current);
}
});
// Attach a cancel() method
(promise as any).cancel = () => (canceled = true);
counter++;
return promise;
},
},
],
And this is what is logged when entering "abc" in the field
Validating 0
Validating 1
Validating 2
Validating 3
Validating 4
Validating 5
Has been canceled, should not do anything.... 0
Has been canceled, should not do anything.... 1
Has been canceled, should not do anything.... 2
Has been canceled, should not do anything.... 3
Has been canceled, should not do anything.... 4
Finished long validation! 5
In this particular example, this might be a flaw in the combobox implementation. If this is changed in EUI so that createOption() is async, then would this solve the problem? Or do you see more evidence that indicates synchronous validation is a broad requirement? |
@cjcenizal I prefer the library to support synchronous validation at the cost of a few lines in the code. I think it is better than patching everywhere async is not supported. And you heard about the Gorilla and the banana? 😄 You make an async at one place and suddenly the whole chain has to be async (you pulled the jungle! ) |
💔 Build Failed |
retest |
💔 Build Failed |
💚 Build Succeeded |
💚 Build Succeeded |
💔 Build Failed |
Form library with hooks!
Motivation
In the Elasticsearch UI team we build many forms. Many many forms! 😊 For each of them, we manually define the form state & update and validate its value. Basically re-inventing the wheel for each new form. It takes our precious dev time to re-think the approach each time, but even more problematic: it means that each of our form is built slightly differently. Maintaining those forms means that each time we need to remember how the state is updated and how validation works for a specific form. This is far from efficient...
We need a system in place that takes care of the repetitive task of managing a form state and validating its value, so we can dedicate more time doing what we love the most: build amazing UX for our community! 😊
Main building blocks
useForm()
hook &<UseField />
The main building blocks of the library are the
useForm()
hook and the<UseField />
component. The former is used to intantiate a form, you call it once and then forward the form object returned to the<UseField />
component. As you probably have guessed,<UseField />
is the Component you will use to declare a field on the form.Let's go through some example:
That is all we need to create a form with a "title" property and listen for the input changes.
By default,
<UseField />
will render an<input type="text" />
to hold the value. Of course, this is just to illustrate how the two building blocks work, in other examples we will see how to customize the UI.Design decisions
Raw form state
In order to allow the most flexibility and also improve performance, the form data is saved as a flat object. Only when submitting the form, the output object is built and returned to the consumer. This is the reason why a form field is given a
path
and not aname
to identify it.For array values, we would probably have dynamic rows, but this was just to illustrate how paths work. As you can see, updating the output form data shape is straightforward. Also, say goodbye to serialize and unserialize payloads for Elasticsearch. 😊
Performance
A lot of effort has been put in optimizing for performance. In our current forms, we usually have a state object with the form values and errors. On each keystroke, we update the state and thus re-render the whole form. On small form this is OK, but on large forms with many inputs, this is not efficient.
This is the reason why each form Field is wrapped inside its own component, managing its internal state of value + errors. This also implies that you can't directly access the form data and react to changes on a field value. Of course, there is a solution, have a look at "Access the form data" below.
Customize the UI
There are two ways to provide a custom UI:
Approach 1: Render props pattern
As you can see, you have complete control on how a field is rendered in the DOM. The hook form library does not bring any UI with it, it just returns form and field objects to help building forms.
For our concrete Elasticsearch UI forms, this is a lot of boilerplate to simply render a text field with some error message underneath it. In a following PR I have prepared some goodies, let's call them "syntactic sugar Component" that will remove all the repetitive JSX so we can much more easily spot what is unique to a field (name, label, helpText, validation).
Approach 2: Passing a Component
Another way to customize the UI is to pass a Component as a prop.
This has the benefit of separation of concern. On one side we build the form, declaring its fields, on the other, we build the UI for each field.
Field configuration
<UseField />
accepts an optionalconfig
prop to provide field configuration. This is where we will define the default value, formatters, (de)Serializer, validation...Note: All the configuration settings are optional.
The first thing you see is that we have declared 2 default values. The prop
defaultValue
takes over the config one. This is because the config object might be declared statically in a separate file with a default value to be used in create mode. (for example for a toggle, if it'strue
orfalse
by default). Then, on the field, when you are in edit mode, you want to override any default value from the config and provide the value that comes from the API request.This is great, but as our form grows, declaring configuration object inline would quickly clutter our form component. A better way to declare the fields configuration is through a form schema.
Form Schema
A form schema is simply an object mapping to fields configuration. It means: cutting the inline configuration from the JSX above and putting it inside an object.
And we're back to a nice, easy to read,
<MyForm />
component! As you might have guessed, the UseFieldpath
maps to the schema objectproperties
.The schema does not need to be flatten though
Validation
We finally get to it! 😊 You validate a field by providing a set of
Validation
objects to thevalidation
array of the field configuration. TheseValidation
objects have one required property: avalidator()
function. This validator function will receive thevalue
being validated, along with theformData
in case we need to validate the value against other form fields.The
validator()
functions can be synchronous or asynchronous (see below), and they will be executed sequentially until one of them fails. Failing means returning anError
object. If nothing is returned from the validator it means that it succeeded.As we can see, field validator function are pure functions (when doing synchronous validation) that receive a value and return an
Error
object orvoid
. As we know, a lot of the validation we do in our forms check for the same type of error (is the field empty?, does the string start with "x"?, is this a valid index pattern?, is this a valid time value?...). Those field validator functions are a good candidate for other pieces of reusable static code. In my separate "goodies" PR I will include some of them. The idea is that we grow our list of reusable field validators functions and only add business-specific validators inside the form schema whenever it does not make sense to make reusable.Asynchronous validation
In some cases, we need to make an HTTP Request to validate a field value (for example to validate an index name). The good news is... asynchronous validation works exactly the same way as synchronous validation! You can mix both, the only thing to bear in mind is if one of the validation is asynchronous, calling
field.validate()
will need to be "awaited".Default value object
We have seen earlier how we can provide a default value on the
<UseField />
component through props:If we only have a few fields, this is good enough. But if we have a complex form data object, this will quickly become tedious as we might have to drill down the
myFetchedResource
object to child components of our<MyForm />
main component.To solve this, when initiating the form we can provide an optional
defaultValue
object. When a field is added on the form, if there is a value on thisdefaultValue
object at the fieldpath
, it will be used as the default value.Adding the "edit" capability to a form has never been so easy 😊
Dynamic form row with
<UseArray />
In case your form requires dynamic fields, the
<UseArray />
component will help you add and remove them.Access the form data with
<FormDataProvider />
As mentioned above, for performance reason, you can't access the form data on the
form
object and get updates in your view when a field value change. That does not mean it is not possible to do. Whenever you need to react to a field value change, you can use the<FormDataProvider />
component.The
pathsToWatch
is optional. It can be a path or an Array of paths. It allows you to declare which form field(s) you are interested in. If you don't specify thepathsToWatch
, it will update on any field value change. For performance, you should always provide a value for it.cc @cjcenizal