form icon indicating copy to clipboard operation
form copied to clipboard

feat(form-core): isDirty flag

Open fulopkovacs opened this issue 1 year ago • 4 comments

Adding the isDirty flag to the form and fields inspired by react-hook-form.

Description

When is it true?
field.state.meta.isDirty the value of the field does not match the default value of the field
form.state.isDirty there's at least one "dirty" field in the form

TODO

  • [x] Implement the flag for fields with primitive values (e.g.: string, number)
  • [x] Implement the flag for fields with non-primitive values (e.g.: array fields)
  • [x] Add tests
  • [x] Update the docs

Motivation

This feature was request on Discord multiple times:

  • https://discord.com/channels/719702312431386674/1100437019857014895/1191884556341620816
  • https://discord.com/channels/719702312431386674/1199336913862135900

fulopkovacs avatar Feb 11 '24 15:02 fulopkovacs

☁️ Nx Cloud Report

CI is running/has finished running commands for commit 5514714f0a67be8b7e1986cedf659d383884d4f0. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this CI Pipeline Execution


✅ Successfully ran 1 target

Sent with 💌 from NxCloud.

nx-cloud[bot] avatar Feb 18 '24 21:02 nx-cloud[bot]

Codecov Report

Attention: 7 lines in your changes are missing coverage. Please review.

Comparison is base (b28f6d8) 87.78% compared to head (4d88b28) 87.54%. Report is 1 commits behind head on main.

Files Patch % Lines
packages/form-core/src/FieldApi.ts 75.00% 6 Missing and 1 partial :warning:

:exclamation: Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #598      +/-   ##
==========================================
- Coverage   87.78%   87.54%   -0.25%     
==========================================
  Files          31       31              
  Lines         819      851      +32     
  Branches      184      200      +16     
==========================================
+ Hits          719      745      +26     
- Misses         95      100       +5     
- Partials        5        6       +1     

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

codecov-commenter avatar Feb 18 '24 21:02 codecov-commenter

Hey @fulopkovacs sorry for not clarifying this much sooner - as I was busy wrapping up my book. Now that I've done so, let my sample some writing from a (currently unpublished) blog post I wrote about forms validation states:

One feature that's added with reactive forms is the concept of an input's state. An input can have many different states:

  • "Touched" - When the user has interacted with a given field, even if they haven't input anything
    • Clicking on the input
    • Tabbing through an input
    • Typing data into input
  • "Pristine" - The user has not yet input data into the field
    • Comes before "touching" said field if the user has not interacted with it in any way
    • Comes between "touched" and "dirty" when the user has "touched" the field but has not put data in
  • "Dirty" - When the user has input data into the field
    • Comes after "touching" said field
    • Opposite of "pristine"
  • "Disabled" - Inputs that the user should not be able to add values into

While some of these states are mutually exclusive, an input may have more than one of these states active at a time. For example, a field that the user has typed into has both "dirty" and "touched" states applied at the same time.

These states can then be used to apply different styling or logic to each of the input's associated elements. For example, a field that is required && touched && pristine, meaning that the user has clicked on the field, not input data into the field, but the field requires a user's input. In this instance, an implementation might show a "This field is required" error message.

As we can see here, dirty isn't quite defined as Value is not the same as defaultValue, since the user could have touched a field, typed a different value, then changed it back to the original.

The way I did this in HouseForm was by:

  • Setting isDirty in setValue (https://github.com/houseform/houseform/blob/main/lib/field/use-field-like.ts#L268)
  • Setting isTouched in onBlur (https://github.com/houseform/houseform/blob/main/lib/field/field.tsx#L60)

I think we should set up our structure to be very similar and even provide pristine as a helper property.

What do you think?

crutchcorn avatar Feb 25 '24 06:02 crutchcorn

What do you think?

I'm personally happy to rely on your experience with form libraries, and use these four form validation states. My only concern is that some users coming from React Hook Form were asking for an isDirty flag that is true when "currentValue !== originalValue" (see this Discord message).

Here's an example that one user gave when I asked them how would they use the isDirty flag:

User deletes some changes in the input field. So default value mattzn => edit mattzn1217 => delete mattzn This situation, field.state.value === defaultValue , so, this form is not dirty. (Discord message)

And here's a use case for this flag from another user:

That’s why we use react-hook-form's isDirty flag to avoid navigating other pages. If isDirty flag is true, If a user tries to make a screen transition with a change in the form, a popup will appear. I can do something simillar with isTouched flag. But user made a mistake and touch a form, isTouched flag change true, doesn't it? In this situation, user want to navigate other pages. (Discord message)

If I understand it correctly, the four states you describe ("Touched", "Pristine", "Dirty", "Disabled") cannot be used to achieve what they want. My question is: what should be the recommended way for these users to get the results they want? Should we add a new flag (that is not called isDirty), or should they use form.options.defaultValues to make these checks themselves (this can get messy if the form is a bit more complex).

I personally would find a flag that does the same thing as the isDirty flag these users wanted, but I'm not super attached to the name. I'd be glad to implement the pristine helper too.

What do you think?

fulopkovacs avatar Feb 25 '24 18:02 fulopkovacs

The isDirty flag functions the way you'd outlined in RHF, that is true. However, I think that implementation of isDirty is flawed and would lead to bug reports in my user testing at work.

Consider the following:

  • The user has typed a value into the form
  • The user has deletes the value in the form
  • The user tries to navigate backwards

Should they be prevented because they did, technically, interact with the form. I'd say "yes" and vote that to be our default functionality.

Instead of changing that, let's provide the isDirty, isPristine and other options as I outlined, but also provide the defaultValue to the form.Field component function so that users can write their own isNotDefault state if they wanted to.

We can even make a documentation note for this if users are coming from RHF

crutchcorn avatar Feb 26 '24 23:02 crutchcorn

Instead of changing that, let's provide the isDirty, isPristine and other options as I outlined, but also provide the defaultValue to the form.Field component function so that users can write their own isNotDefault state if they wanted to.

We can even make a documentation note for this if users are coming from RHF

Nice! I think this is the best solution. It allows us to follow Cruthchley's Four Input States Model™️ , while providing an API for our users to get the data they were asking for. ☺️

I'll update the PR!

fulopkovacs avatar Feb 28 '24 20:02 fulopkovacs

Closing as I know @fulopkovacs has a more up-to-date PR coming soon 🎉🎉

crutchcorn avatar Mar 04 '24 09:03 crutchcorn