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

useValidation and useValidationObserver - Reset validations #337

Merged
merged 22 commits into from
Jan 2, 2024

Conversation

collincchoy
Copy link
Contributor

@collincchoy collincchoy commented Nov 1, 2023

The Problem

useValidation and useValidationObserver are lazy by default and don't trigger until their watched value changes. This is convenient so that initially, the value(s) can be "invalid" but validations have yet to run so there are no errors like in the case of an empty form with required fields.

However in the same example of having a form with required fields, once the values have been modified, there isn't currently a way to reset the internal useValidation (and consequently useValidationObserver) state to reset the form back to its initial state (without any validation errors).

The Proposed Solution

This PR adds a reset method to both useValidation and useValidationObserver. Both methods have the same signature where the latter simply loops over all registered validations and applies the respective reset with parameters drilled down.

It looks like:

const value = ref(0)
const { valid, error, reset, validate } = useValidation(value, isEvenNumber)
console.log(valid)  // true

value.value = 1
await validate()
console.log(valid)  // false

reset()
console.log(valid)  // true

An extra wrinkle

So resetting the internal state is great, but what about when the value is reset also? Well because useValidation watches the passed in value and automatically re-validates on change, just calling reset() like above isn't enough.

Consider the following code:

const value = ref<number | undefined>(undefined)
const { valid, error, reset, validate } = useValidation(value, isEvenNumber)
console.log(valid)  // true
await validate()
console.log(valid)  // false because value is undefined

value.value = 1
await validate()
console.log(valid)  // false

reset()
console.log(valid)  // true but `value.value` is still 1
value.value = undefined
console.log(valid)  // false because validations reran automatically on change
console.log(error)  // non-empty error message

When used for form validations, on resetting the form, the above code ends up with an extra round of validations done on the reset value which could add/leave validation messages onscreen.

To get around this, I've added 2 more exposed functions to useValidation - pause and resume. When called, the automatic validation on value change is temporarily paused/resumed respectively. Additionally, the reset method can take an optional callback parameter which runs in between a pause and resume block so that a consumer can run code without triggering the change effect.

const value = ref<number | undefined>(undefined)
const { valid, error, reset, validate } = useValidation(value, isEvenNumber)
console.log(valid)  // true
await validate()
console.log(valid)  // false because value is undefined

value.value = 1
await validate()
console.log(valid)  // false

reset(() => value.value = undefined)
console.log(valid)  // true
console.log(error)  // empty ""

Warning
The rest of this description is OUTDATED. It's been left here for historical purposes. Changes were made in an attempt to smooth out some of the "sharp edges". Namely that the reset method can now take a callback function which should automatically run within a paused validation context and resume afterwards.

To get around this, I've added an opt-in flag to the reset method which skips the next on-change-validation execution to allow a consumer to update the value once without triggering validations.

const value = ref<number | undefined>(undefined)
const { valid, error, reset, validate } = useValidation(value, isEvenNumber)
console.log(valid)  // true
await validate()
console.log(valid)  // false because value is undefined

value.value = 1
await validate()
console.log(valid)  // false

reset({ skipNextValidateOnChange: true })
console.log(valid)  // true but `value.value` is still 1
value.value = undefined
console.log(valid)  // true
console.log(error)  // empty ''

The sharp edges

On the one hand this now gives a consumer the ability to reset validations and supports the use-case I outlined above for resetting the value along with any related validation state.

On the other hand, this seems a little clunky of an api and also there's potential for race conditions if done out-of-order -- reset should ideally be called before the values themselves are updated otherwise the skipNextValidateOnChange might not be in effect by the time the watcher's effect triggers.

@collincchoy collincchoy requested a review from a team as a code owner November 1, 2023 16:19
@pleek91
Copy link
Collaborator

pleek91 commented Nov 1, 2023

@collincchoy haven't looked at the code yet. But one question. What happens if you call reset on the validation AFTER you reset the form? That is the order I expected. And seems like that would eliminate the need for a validation skip argument. But I'm guessing that has some other side effect

@collincchoy
Copy link
Contributor Author

collincchoy commented Nov 1, 2023

@collincchoy haven't looked at the code yet. But one question. What happens if you call reset on the validation AFTER you reset the form? That is the order I expected. And seems like that would eliminate the need for a validation skip argument. But I'm guessing that has some other side effect

Timing issue, the reset could happen before the watch effect hits("could" as in that seems to be what I'm observing in practice). Although maybe that can be handled as part of the resetting 🤔

edit: I thought I could do something w/ the pending state but my idea didn't work out.

@pleek91
Copy link
Collaborator

pleek91 commented Nov 1, 2023

@collincchoy haven't looked at the code yet. But one question. What happens if you call reset on the validation AFTER you reset the form? That is the order I expected. And seems like that would eliminate the need for a validation skip argument. But I'm guessing that has some other side effect

Timing issue, the reset could happen before the watch effect hits("could" as in that seems to be what I'm observing in practice). Although maybe that can be handled as part of the resetting 🤔

edit: I thought I could do something w/ the pending state but my idea didn't work out.

@collincchoy interesting. What do you think about adding some sort of stop/start functionality along with reset? I'm not to keen to introduce skipNextValidateOnChange. But this devx seems like it would be useful. Its more verbose but gives control to implementation. Its also a useful feature on its own.

const value = ref(0)
const { reset, pause, resume } = useValidation(value, isEvenNumber)

reset()
pause()
value.value = 0
resume()

@collincchoy
Copy link
Contributor Author

collincchoy commented Nov 3, 2023

@collincchoy haven't looked at the code yet. But one question. What happens if you call reset on the validation AFTER you reset the form? That is the order I expected. And seems like that would eliminate the need for a validation skip argument. But I'm guessing that has some other side effect

Timing issue, the reset could happen before the watch effect hits("could" as in that seems to be what I'm observing in practice). Although maybe that can be handled as part of the resetting 🤔
edit: I thought I could do something w/ the pending state but my idea didn't work out.

@collincchoy interesting. What do you think about adding some sort of stop/start functionality along with reset? I'm not to keen to introduce skipNextValidateOnChange. But this devx seems like it would be useful. Its more verbose but gives control to implementation. Its also a useful feature on its own.

const value = ref(0)
const { reset, pause, resume } = useValidation(value, isEvenNumber)

reset()
pause()
value.value = 0
resume()

Cool I think those would be useful levers to pull. what if reset took an optional callback that runs within a pause/resume block?

const value = ref(0)
const { reset, pause, resume } = useValidation(value, isEvenNumber)

reset(() => value.value = 0)

@pleek91
Copy link
Collaborator

pleek91 commented Nov 3, 2023

We talked about this already but commenting here for future us. I really like the idea of a callback in the reset function that allows you to do some stuff while resetting 👍

@collincchoy collincchoy requested a review from pleek91 November 9, 2023 17:19
Copy link
Collaborator

@pleek91 pleek91 left a comment

Choose a reason for hiding this comment

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

One question but otherwise this looks great. Especially thankful for all the test coverage. Thank you!

@collincchoy
Copy link
Contributor Author

Some dependency bump broke the wrapper.vm.* types.. need to dig in later.

@collincchoy
Copy link
Contributor Author

Some dependency bump broke the wrapper.vm.* types.. need to dig in later.

A-ha. I finally found it. bumping @testing-library/vue seems to have bumped @vue/test-utils to > 2.4.0 where 2.4.0 contained breaking typing changes where previously it was probably at @vue/[email protected].

@collincchoy collincchoy requested a review from pleek91 January 2, 2024 18:36
Copy link
Collaborator

@pleek91 pleek91 left a comment

Choose a reason for hiding this comment

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

This looks great. One error we should log out but otherwise this is ready!

Copy link
Collaborator

Choose a reason for hiding this comment

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

❤️

@collincchoy collincchoy merged commit 14b0060 into main Jan 2, 2024
3 checks passed
@collincchoy collincchoy deleted the reset-validations branch January 2, 2024 19:17
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.

2 participants