dominicbarnes/deku-forms

Pattern for async error handling?

ashaffer opened this issue · 11 comments

For example, a 'username already exists' type of error. Normally in the html5 validation api you'd use setCustomValidity to address this. But since deku is using a virtual-dom, we presumably will not have access to the current element whenever the async call completes.

Do you have a suggested pattern for handling this?

You can still use setCustomValidity(), just bind using onInput as is mentioned here.

export function render({ state }, setState) {
  return (
    <InputField name="username" onInput={checkExistence} />
  );

  function checkExistence(e) {
    userExists(e.target.value, function (exists) {
      e.target.setCustomValidity(exists ? 'user already exists' : '');
    });
  }
}

This particular approach isn't bulletproof though, as you can still technically submit the form while waiting on async actions to complete. One idea is to disable the submit button while stuff is in flight, which you can just do with some state.

But yeah, we need to come up with a solid approach here. It's tricky to keep the form and inputs in sync, since there's no public API for doing such a thing in deku. (part of what made the HTML5 validation so powerful, since that's in the DOM and not deku)

Ya, for my own forms (on the form component) I am passing in a loading prop that blocks submits. Mostly i'm concerned with this pattern:

function render () {
  return (
    <Form onSubmit={createUser}>
      <InputField name='username' />
    </Form>
  )

  function createUser(user) {
    api.user.create(user).then(() => console.log('success'), (err) => {
      // This closure doesn't know about the input control at all
      // so it can't call setCustomValidity on anything
    })
  }
}

The approach I was trying to take in my own form components was to pass in an error prop, rather than a validationMessage function, and that addresses this problem, but creates others.

It also seems somewhat contrary to the notion of functional purity to be performing side-effecting things imperatively in callbacks (other than setting state). I'm not sure how big of a deal this is though.

The onSubmit callback does receive the <form> element as it's 2nd parameter.

You can find the appropriate <input> there and call setCustomValidity():

import element from 'dominicbarnes/form-element';

function createUser(user, form) {
  var name = element(form, 'name');
  name.setCustomValidity('user already exists');
}

I'm totally plugging my form-element library here lol, but you can get by without it for simple cases. (it just lets you get a form control via it's name cleanly and cross-browser)

Of course, that implies that your server gives you a response that helps you determine which fields are invalid, but that shouldn't be unreasonable.

Ah, I see. That seems pretty nice.

In thinking about it though, it seems like you do lose something by having to interact directly with the native elements. Have you thought at all about trying to reimplement all of the html5 constraints API natively in deku, so that you can avoid the impurity of interacting with the DOM?

Some stuff would have to change of course, like setCustomValidity would have to turn into a prop or something, since deku doesn't support react-style refs.

I'm not interested in doing that at all, tbh. The whole point of adopting it here was to limit the scope of this component. The DOM isn't evil, so I don't want to re-invent that entire subsystem for the sake of purity.

I think an elegant solution for async validation is out there, just gotta look harder. :)

Ya I suppose you're right. Your form-element solutions seems good, I think I'll go with that for the time being. Thanks!

Actually on second thought, that isn't going to be sufficient to get the error to display, since your components are only checking errors on input. I'd want the error to get rendered as soon as the input was marked as invalid. Maybe checkError should happen on the onInvalid event?

In order to make your example work with the onInvalid event, you'd also have to do `name.checkValidity()' after setting the error.

checkError does happen for the invalid event. (example)

Calling el.setCustomValidity() triggers an invalid event, so this all works.

Oh, sorry, I see you are doing that too. For me (in chrome) setCustomValidity on its own does not trigger an invalid event without also calling checkValidity. Per the docs I don't think it's supposed to either:

https://developer.mozilla.org/en-US/docs/Web/Events/invalid

Hmmm... bummer I thought I had already accounted for that. I'm open to a PR here, I'll probably look at this over the weekend too.

I've cleaned up and improved a lot of the internals around using setCustomValidity in general, but also for async validation. This should be resolved, let me know if it's not.