5

I have one requirement where We want the user to resolve input field errors and until that is done user is not allowed to do any other operation on-screen.

For the same, I have implemented HostListener on the input field and onBlur I am setting the focus back to the input field if there are validation scenarios fail.

And, i am doing e.preventDefault() and e.stopPropagtion() to stop all other event callback to be executed on the page (as after setting focus back, blur will be the first event to be executed).

But somehow on any external event, blur does get executed but it is not restricting other events to be executed. Those are also getting executing without which I am not able to achieve the desired functionality.

import { Directive, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appNumberFormat]'
})
export class NumberFormatDirective {

  constructor() { }

  @HostListener('blur', ['$event']) blur(evt) {
    evt.preventDefault();
    console.log('field focus...');
    evt.target.focus();
    return false;
    // if (evt.target.value.trim() === '') {
    //     this.formControl.control.setValue(this.defaultValue);
    // }
  }
}

I have replicated the same scenario in stackBlitz. Please have a look.

https://stackblitz.com/edit/angular-54ermg

Manish Kumar
  • 1,131
  • 15
  • 28
  • 1
    @Liam he actually provided the code in stackblitz, I just added it to the question to avoid closing. – CPHPython Sep 19 '19 at 14:11
  • May i ask why you not using (blur) event binding ? is there any required that it must be a directive ? – Ethan Vu Sep 23 '19 at 17:09

6 Answers6

3

Blur isn't preventing another event because it's running independently. Setting pointer-events to none will prevent clicking.

onBlur(e) {
    e.preventDefault();
    e.stopPropagation();
    document.body.style.pointerEvents = 'none';
    console.log('field focus...');
    setTimeout(() => {
      e.target.focus();
    }, 10);

    setTimeout(() => {
      document.body.style.pointerEvents = 'initial';
    }, 300);

    return;
  }

You can also add an overlay when input is focused instead of using pointer-events: none. In both cases, you will need to handle tab presses.

In order to prevent tab + enter add on the input element:

(keydown)="onKeyDown($event)" 

In component ts:

onKeyDown(event) {
    if (event.code === 'Tab') {
      event.preventDefault();
      event.stopPropagation();
    }
  }
RandomCode
  • 403
  • 4
  • 9
  • 1
    this solution tries to block page actions when onBlur method is executing but enables all events as soon as focus is set back. This is not restricting other actions form being executing. – Manish Kumar Sep 28 '19 at 19:12
  • You are right I've managed to sneak a couple of clicks if I've tried hard enough. I have edited my answer to set pointer events to initial after a longer timeout. It seems 10ms was too short. – RandomCode Sep 28 '19 at 20:09
2

One way you could do is disable all the event on the page using this solution (and make a custom for keypress that do not allow enter key).

Stop propagation for all events

  disableAllUserEvents = () => {
    const events = ["click", "contextmenu", "dblclick", "mousedown", "mouseenter", "mouseleave", "mousemove",
        "mouseover", "mouseout", "mouseup", "blur", "change", "focus", "focusin",
        "focusout", "input", "invalid", "reset", "search", "select", "submit", "drag", "dragend", "dragenter",
        "dragleave", "dragover", "dragstart", "drop", "copy", "cut", "paste", "mousewheel", "wheel", "touchcancel",
        "touchend", "touchmove", "touchstart"];

    const handler = event => {
      event.stopPropagation();
      event.preventDefault();

      return false;
  };

    for (let i = 0, l = events.length; i < l; i++) {
        document.addEventListener(events[i], handler, true);
    }

    return () => {
        for (let i = 0, l = events.length; i < l; i++) {
            document.removeEventListener(events[i], handler, true);
        }
    };
  };

and re enable the event when all is ok. It's a bit brute force, but it does work.

https://stackblitz.com/edit/angular-68vdrq

Crocsx
  • 2,534
  • 1
  • 28
  • 50
1

I don't think it's an ideal way to achieve the required functionality. I think other buttons/anchor-links should remain disabled when there are errors in the form. You can bind the disabled property for both buttons & anchor-links to a function which will return true/false after checking the validations.

Nevertheless, browser executes the events in a specific order and if you need to stop the click event from progressing, you should return false. I've implemented the same with the use of a boolean variable which will set to true/false on based on validation checks, I've created a fork of your code here - https://stackblitz.com/edit/angular-uay8rm

  preventOtherActions: boolean = true;
  txtBoxValue: string = "";

  submitForm($event) {
    if(this.preventOtherActions) {
      return false;
    }
    console.log('submitForm');
  }

  onBlur(e) {
    console.log('field focus...');
    setTimeout(() => {
       e.target.focus();
      }, 10);
    this.preventOtherActions = this.txtBoxValue.length < 6;    
    return;
  }
Hemendra
  • 378
  • 5
  • 13
  • Maintaining the variable to hold error state not a viable solution as I have dynamic controls on-page and can be (n) number of fields. Also, there are multiple links (routerLink), button, and dropdowns on the page, Do you think this can be implemented there? I checked the same implementation with vanilla/jQuery... these it worked fine. on returning false, other events got stopped. It something with Angular only that, events are not stopping. – Manish Kumar Sep 28 '19 at 19:06
  • @ManishKumar I think you are trying to handle diff things in single onBlur func. Instead each scenario should be handled separately. For e.g 1. routerLink scenario should be handled using angular's canDeactivate guard 2. put all controls inside angular form and buttons/links should remain disabled if form is not valid 3. let user click anywhere else even if there's error and highlight the input field (e.g. make the border red) which has error. – Hemendra Sep 29 '19 at 09:13
0

You could add an extra Input property to enable/disable the focus funcionality. If you have a Reactive form you can hook this to the form's VALID state.

export class NumberFormatDirective {
  @Input() enableTargetFocus: boolean;

  @HostListener('blur', ['$event']) blur(evt) {
    if (this.enableTargetFocus === false) {
      return;
    }
    ...
  }
}

HTML:

<input type="text" appNumberFormat [enableTargetFocus]="form.invalid" />
Ritchie
  • 502
  • 4
  • 13
0

A way fo solving this would be through a variable on your component (that indicates if the form is valid) and a simple css element that is blocking all the clicks.

You would have to be careful though, and make sure you focus on the right element, and also that this doesn't go on forever because I have to say it seems fairly annoying from a user experience perspective.

On your html template:

<div *ngIf="formIsInvalid" class="click-blocker"></div>

And on your css:

.click-blocker {
  position: absolute;
  background-color: rgba(1, 1, 1, 0.1); // adjust the opacity as you wish
  bottom: 0;
  left: 0;
  right: 0;
  top: 0;
  height: 100vh;
  width: 100vw;
  z-index: 1;
}

I implemented it into your stackblitz: https://stackblitz.com/edit/angular-za1lgx

RTYX
  • 1,244
  • 1
  • 14
  • 32
  • I need to restrict even the header navigation/left panel navigation and any other user actions avilable anywhere on the screen not just in the component itself. And, also there 100's field for this task has to be done. single variable for maintaining the invalid state won't help here. – Manish Kumar Sep 27 '19 at 09:26
0

I'm not really sure what you're trying to achieve but a trick could be to restrict your focus() execution based on different cases whe you're called in the onBlur() because you will be called everytime the blur is request since you binded the event.

In order to prevent execution you could trigger it only when you need but that's not really ideal : let's say when your e.target or e.relatedTarget is not the one you want or something like this or when the document.activeElement is already the input well you don't need to refocus it onblur() but again ... Not ideal

Hope this helps !