Skip to content
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

feat(form-core): isDirty flag #598

Closed
wants to merge 3 commits into from

Conversation

fulopkovacs
Copy link
Contributor

@fulopkovacs fulopkovacs commented Feb 11, 2024

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

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

Motivation

This feature was request on Discord multiple times:

Copy link

nx-cloud bot commented Feb 18, 2024

☁️ Nx Cloud Report

CI is running/has finished running commands for commit 5514714. 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only updated this file to make the PR more easy to test, we don't have to keep these changes!

@codecov-commenter
Copy link

codecov-commenter commented Feb 18, 2024

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 ⚠️

❗ 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     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Comment on lines +331 to +333
const fieldDefaultValue = getBy(
this.form.options.defaultValues,
this.name,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it OK to assume that passing this.form.options.defaultValues as the first argument to getBy() will never result in an error? defaultValues cannot be null, or any other value that could cause us trouble here, right? 😅

@@ -696,3 +705,46 @@ function getErrorMapKey(cause: ValidationCause) {
return 'onChange'
}
}

export function deepEqual<T>(objA: T, objB: T): boolean {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fulopkovacs fulopkovacs marked this pull request as ready for review February 18, 2024 21:33
Comment on lines +98 to +111
selector={(state) => [
state.canSubmit,
state.isSubmitting,
state.isDirty,
]}
children={([canSubmit, isSubmitting, isDirty]) => (
<>
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
<p>
<code>isDirty:{isDirty ? 'true' : 'false'}</code>
</p>
</>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revert this from the example :)

@crutchcorn
Copy link
Member

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:

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

What do you think?

@fulopkovacs
Copy link
Contributor Author

fulopkovacs commented Feb 25, 2024

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?

@crutchcorn
Copy link
Member

crutchcorn commented Feb 26, 2024

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

@fulopkovacs
Copy link
Contributor Author

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 fulopkovacs force-pushed the add-the-isdirty-flag branch from 4d88b28 to 5514714 Compare March 3, 2024 17:11
@crutchcorn
Copy link
Member

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

@crutchcorn crutchcorn closed this Mar 4, 2024
@fulopkovacs fulopkovacs mentioned this pull request Mar 6, 2024
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants