5
votes

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

6
@Liam he actually provided the code in stackblitz, I just added it to the question to avoid closing.CPHPython
May i ask why you not using (blur) event binding ? is there any required that it must be a directive ?Ethan Vu

6 Answers

3
votes

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();
    }
  }
2
votes

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

1
votes

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;
  }
0
votes

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" />
0
votes

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

0
votes

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 !