Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

forms: validation. don't show errors until the user has had a chance to do something #1633

Closed
kentcdodds opened this issue Feb 24, 2015 · 14 comments

Comments

@kentcdodds
Copy link
Member

When a user opens a page that has a bunch of required fields, all they see is a bunch of red. Not very inviting. I would much rather that validation is hidden until $touched === true. I would also like to have the ability to control when validation is shown.

I appreciate how helpful angular-material is, but I really like that bootstrap allows me to specify when to show validation errors by requiring me to add the has-error class myself. This gives me a lot of flexibility.

I haven't looked at the implementation for angular-material, but perhaps I could specify a function which would determine whether validation should be visible for a specific field?

However it's done, I just don't want to invite users to my page with a bunch of invalid fields. Let them touch the fields first, then we'll tell them it's invalid if it still is after they've had a chance to do something with it.

@kentcdodds
Copy link
Member Author

Sorry, just realized that it doesn't greet the user with a bunch of red. However, it goes red as soon as focus comes into the input. I would rather wait until $touched is set to true which means the user has blurred the input.

@gkalpak
Copy link
Member

gkalpak commented Feb 24, 2015

There is a way to specify when to show an error: By using the md-is-error attribute.
However there are some issues/bugs with properly detecting the $touched state (there are several issues about this, I believe).

@kentcdodds
Copy link
Member Author

I hadn't ever heard of issues with the $touched state. I switched my demo to bootstrap and everything is working really well: http://jsbin.com/zuguxi/edit

@kentcdodds
Copy link
Member Author

Sorry, just realized I never gave the original example here: http://jsbin.com/cadeko/edit

@gkalpak
Copy link
Member

gkalpak commented Feb 25, 2015

This turned out to be longer than anticipated, so long story short:

Due to some bugs in the current implementation:

  1. Any input inside an mdInputContainer gets set to touched upon focus.
  2. This state change is not properly propagated (because of improper digest lifecycle handling).

This is a bug with setting the touched state of the input. It can be found in input.js#L183-192:

element
  .on('focus', function(ev) {
    containerCtrl.setFocused(true);

    // Error text should not appear before user interaction with the field.
    // So we need to check on focus also
    ngModelCtrl.$setTouched();
    if ( isErrorGetter() ) containerCtrl.setInvalid(true);
  })

Here is what is going on:

  1. There is the mdIsError attribute which can be used to determine when the error should be "applied"/shown.
  2. If it is present it is $parsed and (incorrectly) used for determining if the error should be applied using the isErrorGetter() function.
  3. If it is not present, a default function is created that returns ngModelCtrl.$invalid && ngModelCtrl.$touched. <-- (this is the case in your example)
  4. The isErrorGetter() is $watched and setInvalid() is called on the mdInputContainer's controller according to its return value.
  5. For reasons that are not clear to me, upon focusing the input, the following happens:
    ngModelCtrl.$setTouched();
    if ( isErrorGetter() ) containerCtrl.setInvalid(true);

As a result of (5):

  • The element's state is set to touched (although this is not consistent with the default semantics of $touched).

  • isErrorGetter() (which checks the controller's internal state) evaluates to true, which results in calling setInvalid(true) on the mdInputContainer's controller.

  • mdInputContainer receives the md-input-invalid class, thus stying the containing input with the warn-500 color (by default red).

  • Since the event happens outside of Angular's $digest cycle and there is not explicit $apply, the new state is not properly propagated, thus:

    • The input does not receive the ng-touched class.
    • The ngShow on the messages' div is not re-evaluated (it would evaluate to true and show the errors, if a digest cycle was properly triggered).

    Both of the above can be "fixed" by triggering a digest "manually" from the console (for illustration pusposes only).

@gkalpak
Copy link
Member

gkalpak commented Feb 25, 2015

(Needless to say, that the Bootstrap version (which does not rely on mdInputContainer) works as expected as it avoids the aforementioned bugs.)

@kentcdodds
Copy link
Member Author

Thanks for the explanation @gkalpak. Totally makes sense where this came from. Obviously triggering the digest manually is not an option. In angular-formly we have logic like this:

// scope.fc = the ngModelController
// scope.options = something that the directive user can manipulate
scope.$watch(function() {
  if (typeof scope.options.validation.show === 'boolean') {
    return scope.fc.$invalid && scope.options.validation.show;
  } else {
    return scope.fc.$invalid && scope.fc.$touched;
  }
}, function(show) {
  options.validation.errorExistsAndShouldBeVisible = show;
  scope.showError = show; // <-- just a shortcut for the longer version for use in templates
});

This is really handy because in templates, people can do something like this (contrived bootstrap example):

<div class="form-group" ng-class="{'has-error': showError}"> <!-- <-- notice that -->
  <label for="my-input" class="control-label">My Label</label>
  <input ng-model="my.model" name="myForm" class="form-control" id="my-input" />
  <div ng-messages="fc.$error" ng-if="showError"> <!-- <-- and that -->
    <!-- messages here -->
  </div>
</div>

I think the md-is-error works great because it gives the same kind of control that options.validation.show gives, where you can show errors even if the user hasn't touched an input which is useful sometimes. But the more common case is after the user has interacted with the field.

If it were me, I would do what we're doing in angular-formly. Don't explicitly set the ngModelController to invalid and $touched on focus, but instead only show error state if it's $invalid && ($touched || isErrorGetter()).

@jasonayre
Copy link

@gkalpak regarding issue 5, I'm guessing the reason its thrown into an error state on focus is because, in the docs for example, all the fields have required directive. So you focus the input, it checks to see if its required, but you havent started typing so no value is present yet, throwing into immediate error state.

@gkalpak
Copy link
Member

gkalpak commented Feb 26, 2015

@jasonayre: The inputs are invalid from the beginning (because they are required and empty). The problem is when they show their invalidity (which in the intended default case should happen after the first blur, but happens on focus).

@ThomasBurleson
Copy link
Contributor

Fixed with SHA 747eb9c

@kentcdodds
Copy link
Member Author

Thanks @ThomasBurleson :D

@mxab
Copy link

mxab commented Mar 18, 2015

Hi, maybe I did something really wrong but it does not work with 0.8.3
this is my mini example: http://codepen.io/mxab/pen/JoewqY?editors=101

The myForm.myValue.$touched seems to work correctly at the ng-messages block,
but still if I simply focus the input it turns red

@ilianaza
Copy link

ilianaza commented Apr 4, 2015

Yes, looks like it doesn't work in 0.8.3.

@TimFillmore
Copy link

+1

@Splaktar Splaktar added this to the 0.8.3 milestone Dec 13, 2017
@angular angular locked as resolved and limited conversation to collaborators Mar 17, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

8 participants