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

Support debounce for expensive validation functions on form fields #40

Merged
merged 9 commits into from
Nov 30, 2018

Conversation

thomashoneyman
Copy link
Owner

@thomashoneyman thomashoneyman commented Nov 27, 2018

What does this pull request do?

As proposed, would close #37 by giving users the ability to debounce successive modifications to a form field's input. They choose the number of milliseconds that must elapse before the validation runs on the field.

This implementation allows you to set a debounce time directly on a field using asyncSetValidate or asyncModifyValidate. You can still cause validation to run immediately if you use modifyValidate, setValidate or validate directly. Validation will not be debounced if you use validateAll, either -- all validations are immediately triggered.

I went with this implementation so that the impact on those not using async fields don't have to do anything differently. You can tell this is the case because none of the examples needed to change.

  • Add a new example demonstrating these async fields
  • Fix so that fields are marked 'validating' only after the debouncer expires
  • Put the debouncer in a Ref to avoid unnecessary state updates
  • Update the README to use the new types

Major changes and caveats

  • You can now debounce async validation on a field when using the ModifyValidate query, which has been updated to (optionally) take a number of milliseconds for debouncing. This lets you selectively debounce expensive validation. This is a change in the Component.Types file and a corresponding implementation change in Internal.Transform and Component.

  • There are now new helpers called asyncModifyValidate, asyncSetValidate, which let you assign n milliseconds to debounce the given field. Modify, Validate, ValidateAll do not run with a debouncer. The existing modifyValidate and setValidate functions run as usual. This is a change in the Query file.

  • There is now a new data type for result field: FormFieldResult. This type lets you inspect whether the field is NotValidated, Validating (use this to show a spinner, if you’d like), Error, or Success. The type includes instances for Functor, Apply, Applicative, Bind, etc.; prisms to access the two constructors containing data, and helper functions like toMaybe and fromEither. This is a new file at Data.FormFieldResult.

  • Due to the new result type, the prisms for accessing a form field have been updated and renamed to _FieldInput, _FieldResult, etc instead of _Input, _Result. This is necessary because some prisms have the same name and would cause conflicts when exported. The outer functions are unchanged, so almost all code should work as before.

  • Users will probably have to update helper functions that operate on the result field because of these changes. When updating the Formless examples I just had to update a single showError helper function.

  • The Initialize query is now actually used for initialization, and LoadForm is used to load a new form remotely.

How should this be manually tested?

I plan to put together an example using the new support for async fields, but it may be a while before I can get to it, so I'm opening this PR right away. I'll update with the example as soon as I can. In the meantime, if you have a project using a custom-built async solution, try replacing it!

Other Notes:

Unfortunately, this introduces even more query helpers (the async* variations), which is turning the query helpers file into an absolute monster. I'm not a fan, but because of the pervasive newtypes, I just don't see an ergonomic solution without these helpers.

@thomashoneyman thomashoneyman added the enhancement New feature or request label Nov 27, 2018
@thomashoneyman thomashoneyman added this to the 0.5.0 milestone Nov 27, 2018
@thomashoneyman thomashoneyman self-assigned this Nov 27, 2018
@thomashoneyman
Copy link
Owner Author

thomashoneyman commented Nov 27, 2018

I've added a minimal async example which can be built locally with

yarn && yarn build-all && open dist/index.html

An example of the asyncSetValidate in use:
https://github.com/thomashoneyman/purescript-halogen-formless/pull/40/files#diff-0eba3ef5678c476d98525588b809dee3R104

It reveals some issues:

  • The Validating status gets triggered between modification and the field updating. This is a problem because it'll show up for the entire duration before the validation even begins. What's more desirable is to remain in NotValidated until the debouncer has stopped and validation begins.

  • The debouncer applies to all validation, not a particular one. In other words, the debouncer is going to prevent instantaneous validations from running immediately. Consider an email validation which first checks that the email is non-empty, then that it is well-formed, and finally whether it is in use on the server. All of this validation will be debounced, even though only the final piece really needs to be. I don't quite know what to do with this; by the time Formless gets the validator it has no idea what the 'stages' are.

@dariooddenino I'd be curious to hear your experiences with this for your problem in #37.

@dariooddenino
Copy link

Hi!
Yeah it's actually way more complex than it looked at first.
In my example I ended up not using a debouncer, as having a data RemoteField a = Validating a | NotValidating a helped me to avoid triggering continuous validations (and tbh it's a very simple example).

Of your points, n.2 doesn't look like a real issue: debouce times are probably going to be in the range of a few hundred milliseconds most of the time. So, while this might not be optimal (e.g. not getting errors while typing), it's hardly a deal breaker.

Point n.1 is obviously worse. Having a loading gif appear while typing (without anything actually happening) is bad.

Another possibly-impossible idea: what if the async/debouncer status is carried by the validators and not by the field?

@thomashoneyman
Copy link
Owner Author

Point n.1 is obviously worse. Having a loading gif appear while typing (without anything actually happening) is bad.

Fixed! Requires one extra state modification. I think this solves n.1:

2018-11-28 08 42 09

Another possibly-impossible idea: what if the async/debouncer status is carried by the validators and not by the field?

The problem is that the validators aren't able to update the state of the form component. They receive it as an argument and can transform things however they like, but they must simply return either an error or a successful validation.

I thought about using the validators at first, but this would require giving them the full capability of modifying state ("set this field to validating, run validation, set field to validated").

I'd like to avoid that because:

  • then everyone has to deal with the new, even-more-complex type signature, even if they are trying to write a simple "is this int over 10?" validation
  • I'm not sure it's a good idea for validators to be able to modify state beyond the result field
  • i don't think it's possible to implement without giving the validator access to the entire state

In the end, it's not fantastic to have to stick a debounce time on the asyncModifyInput field, but at least the impact on users not using that functionality is close to zero.

@dariooddenino
Copy link

Well since you solved that issue I think it wouldn't be necessary anyways! Great work!

I need to write that blog post before it becomes obsolete :D

@thomashoneyman thomashoneyman merged commit 2e0abc4 into master Nov 30, 2018
@thomashoneyman thomashoneyman deleted the async-validation branch November 30, 2018 06:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Gracefully handle components with async behaviour
2 participants