6
votes

Consider the following demo https://stackblitz.com/edit/angular-pur1dt

I have reactive form control with sync validator and error message shown below the field when it is invalidated.

Validation is triggered when control loses focus. Below the control there is a button that has a click handler. The problem is that when I click the button, control loses focus, validation happens, error message shown and moves the button down. And supposedly this prevents click handler from executing. Any suggestions why this happens and how to fix the issue?

I've updated the demo with comments. Note: only button below the input will reproduce the issue. The title wont be updated after you click it for the first time.

4
the link you supplied seems to be working as expected; Both buttons call their click events as expected. The form validation seems to be working as expected onChange too. - joshvito
One alternative is to bind [disable] property with the form like this <button [disabled]="form.invalid" (click)="onClick($event)">click OK</button> If the form is invalid the button will be disabled - Daniel C.
@joshvito, try to reload fro each click. you'll see - Olena Horal
@DanielC that's not my case. The button is not related to form submission. its just some other component that makes control to lose focus. - Olena Horal
reloading will just rebuild the app to its init state. I guess I am not fully following your problem. sorry. I agree with @Daniel C, if you want the buttons to be disabled when the form in invalid bind to the disabled property of the button - joshvito

4 Answers

4
votes

The problem was discussed in issue #7113 on GitHub. It is caused by the fact that validation is performed when the input field loses focus, before the click on the button has a chance to complete and trigger the click event.

A possible workaround is to use a flag to keep the message hidden while clicking the button is under way, as shown in this stackblitz. In the code below, the isClicking flag is set when clicking starts on the mousedown event, and it is reset when the click event completes.

<p #errorMsg [hidden]="(errorMsg.hidden && isClicking) || form.controls.name.valid || form.controls.name.untouched ">
    Invalid :)
</p>
<button (mousedown)="onMouseDown($event)" (click)="onClick2($event)">click NOT ok</button>
export class AppComponent {

  isClicking = false;
  ...

  onMouseDown(event) {
    this.isClicking = true;
    setTimeout(() => {
      // The click action began but was not completed after two seconds
      this.isClicking = false;
    }, 2000);
  }

  onClick2(event) {
    console.log(event);
    this.name = "NOT";
    this.isClicking = false;
  }
}

You can improve that solution by replacing the setTimeout with a procedure to capture the mouse in the mousedown event handler, and resetting isClicking when the capture is lost. It would account for the cases where the user leaves the button without completing the click.

2
votes

The issue seem to relate to DOM events triggering order

According to MDN:

The click event is fired when a pointing device button (usually a mouse's primary button) is pressed and released on a single element.

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

In the given example the element moves the moment you blur the input -- because the validation happens instantly, reveals the error and repositions the button.

Therefore mouse is down when while over the button, but when its up -- the button is repositioned. So click wont be triggered on the button

There are several options to workaround that:

  • on mousedown delay the error reveal
  • hide the error until both mousedown and mouseup happened, if mousedown happened on the button
  • etc

Here's an example with mousedown event handling https://jsfiddle.net/gjbgqqpo/

Hope this helps :)

0
votes

ok, i think I follow. You DO NOT want validation to happen when the lower button is clicked.

The reason validation is triggered is because of the autofocus on the form input. Angular "By default, the value and validity of a control updates whenever the value changes." See FormControl. The control is changing name.untouched from false to true.

you can watch this happen if you add some extra debug bindings to your template. e.g.

<hello name="{{ name }}"></hello>
<form [formGroup]="form">
    <input formControlName="name" autofocus>
    <button (click)="onClick($event)">click OK</button>
</form>
<pre>{{form.controls.name.valid}}</pre>
<pre>{{form.controls.name.untouched}}</pre>
<p [hidden]="form.controls.name.valid || form.controls.name.untouched ">
    Invalid :)
</p>
<button (click)="onClick2($event)">click NOT ok</button>

If you want to hide the error message when the user clicks the lower button, you should remove the autofocus or reset the validation of the form when onClick2() is called. Alternatively you can just show error message when the form has an error. Angular says A control is untouched if the user has not yet triggered a blur event on it. Clicking the lower button causes a blur event.

Why not just change your template binding on the error message to be

<p [hidden]="!form.controls.name.valid || form.controls.name.untouched">
    Invalid :)
</p>
0
votes

A simple solution for this is to add (mousedown)="false" to your dismiss button. This way blur event won't happen, because focus event on your button will be canceled by your mousedown handler. In turn, this won't mark control as touched and your error won't show.

<button type="button" (click)="dismiss()" (mousedown)="false">Cancel</button>

Now for your submit button, you don't want to cancel the blur event, but you probably want to run some kind of validation logic on mousedown. Think about it this way, if the button has moved, that means there was an error in the form. What do you want to do when there is an error in the form? In my case, all I really wanna do is mark all controls as touched so that all validation errors will be shown.

<button type="submit" (mousedown)="form.markAllAsTouched()">Create</button>