focusout on an element conflicting with click on other in Angular
Asked Answered
C

4

7

I have focusout() event on element1 and click() event on element2, and when element1 goes out of focus because was performed a click event on element2, only focusout is fired, click event is not.

This works fine [on jQuery][1] but not in Angular.

I found a work around by adding a window.setTimeout() which works for angular too. Unfortunately I can not do this.

Another suggestion is much appreciated.

Please find the code with setTimeout:



$('#txtName').on("focusout",function(event) {
    //Alternate solution required for `setTimeout`.
    window.setTimeout( function() {alert('focus gone');},1000); }); 

    $('#button1').on('click',function(){
       alert('button click');
    }); 
 }
Chammy answered 27/8, 2018 at 10:38 Comment(3)
can you create stackblitz example please?Confraternity
In angular there are not focusout event. in Angular it's used (focus)="..." and (blur)="...". I don't see the use of (click) in an input.Aesthete
setTimeout solution does not work sometimePeregrination
B
19

It is a problem with the click event.

A click event consists of 2 events, mousedown and mouseup.

The sequence of events in your case is this

1) mousedown 2) focusout 3) mouseup

Where 1 and 3 make a click event.

This can happen when an additional element, such as an error message is displayed on the page and the button on which the click is supposed to happen, moves from it's original x and y co-ordinates. Hence the mouseup happens on some other place and not where the mousedown had happened.

So basically what I think is that your mousedown works, focusout works but the mouseup does not.

The solution to this is to use mousedown event instead of click. So your click should not wait for mouseup to happen to work.

Example:

<input type="text" (focusout)="someMethod()">
<button (mousedown)="someMethod()">Click Me!</button> //Changed (click) to (mousedown)

Hope this helps.

Binky answered 27/8, 2018 at 11:0 Comment(3)
Eventhough it worked for my test code, in my actual productive code, i am not able to use it. I need focuout to be called first and then only the the next event because i have to run some user input validations.Chammy
This does work, but the one downside is that buttons normally process a click after the mouse up, which gives the user a chance to abort a click by moving the mouse off of the button before releasing the mouse. Using this approach here prevents that from happening. Probably not a big deal in most cases, but it's something to keep in mind.Tenebrae
Using (mousedown) instead of (click) worked for me, thanksHerpetology
O
2

I developed a solution that does not need 'setTimeout' and does not force you to use the 'mouseup' event instead of the click event. This is user friendly because the click event "gives the user a chance to abort a click by moving the mouse off of the button before releasing the mouse." (comment by piccy)

Problem

As stated in the answer by Vinod, this is a problem in the chronology of the events:

  1. mousedown: The button registers a mousedown event.
  2. focusout: Registered due to the mousedown on the button. In this unique scenario, the focusout handler makes the button move to another position.
  3. mouseup: Due to the changed position of the button, it does not register a mouseup event. Thus, a click event is also not registered because that would require a mousedown followed by a mouseup on the same element.

Solution

My solution is a directive that exposes a delayed focusout event that happens after the mousedown and mouseup event. Therefore, the click event is registered before the event handler for the (delayed) focusout event changes the position of the button.

This is done by a BehaviourSubject storing whether the mouse is currently down or not. When a focusout event is registered while the mouse is down, we do not trigger the delayed focusout event immediately (otherwise we would end up with the same old problem). Instead, we wait for the mouse going back up again and then emit the delayed focusout event. This leads to the following order:

  1. mousedown
  2. focusout (ignore this event)
  3. mouseup
  4. delayed focusout + click

Code solution

The directive is used like this:

<input appDelayedFocusout (delayedFocusout)="yourLayoutChangingHandler()">

My directives implementation makes use of the until-destroy library to prevent memory leaks from never ending subscriptions but feel free to modify.

import {Directive, EventEmitter, HostListener, OnInit, Output} from '@angular/core';
import {BehaviorSubject, fromEvent} from 'rxjs';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {filter, map, take} from 'rxjs/operators';

/**
 * This directive exposes a special variant of the 'focusout' event. The regular 'focusout' event has a quirk:
 * Imagine the user clicks on some button on the page. This triggers the following events in the following order:
 * mousedown, focusout, mouseup. But the focusout event handler might change the layout of the website so that
 * the button on which the mousedown event occurred moves around. This leads to no mouseup event registered on
 * that button. Therefore a click event is also not registered because a click event consists of
 * a mousedown AND a mouseup event on that button. In order to fix that problem, this directive exposes a delayed focusout
 * event that is triggered AFTER the mousedown and mouseup events. When the delayed focusout event handler changes
 * positions of buttons, click events are still registered as you would expect.
 */
@UntilDestroy()
@Directive({
  selector: '[appDelayedFocusout]'
})
export class DelayedFocusoutDirective implements OnInit {
  @Output() delayedFocusout = new EventEmitter<boolean>();
  isMouseDownSubject = new BehaviorSubject(false);

  ngOnInit(): void {
    fromEvent(document.body, 'mousedown').pipe(untilDestroyed(this))
      .subscribe(() => this.isMouseDownSubject.next(true));
    fromEvent(document.body, 'mouseup').pipe(untilDestroyed(this))
      .subscribe(() => this.isMouseDownSubject.next(false));
  }

  @HostListener('focusout') onFocusout() {
    // If the mouse is currently down, we subscribe to the the event of
    // 'mouse being released' to then trigger the delayed focusout.
    // If the mouse is currently not down, we can trigger the delayed focusout immediately.
    if (this.isMouseDown()) {
      this.mouseRelease().subscribe(() => {
        // This code is executed once the mouse has been released.
        this.delayedFocusout.emit(true);
      });
    } else {
      this.delayedFocusout.emit(true);
    }
  }

  /**
   * Emits the value true once the mouse has been released and then completes.
   * Also completes when the mouse is not released but this directive is being destroyed.
   */
  mouseRelease() {
    return this.isMouseDownSubject.pipe(
      untilDestroyed(this),
      // Just negate isDown to get the value isReleased.
      map(isDown => !isDown),
      // Only proceed when the the mouse is released.
      filter(isReleased => isReleased),
      take(1)
    );
  }

  isMouseDown() {
    return this.isMouseDownSubject.value;
  }
}
Oilcloth answered 2/4, 2020 at 19:49 Comment(0)
P
1

I have implemented the DelayedFocusout directive shared by @simon-lammes in this answer without the use of until-destroy library. I have used shareReplay to destroy the subscriptions.

import { Directive, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core';
import { BehaviorSubject, fromEvent, ReplaySubject } from 'rxjs';
import { filter, map, take, takeUntil } from 'rxjs/operators';

/**
 * This directive exposes a special variant of the 'focusout' event. The regular 'focusout' event has a quirk:
 * Imagine the user clicks on some button on the page. This triggers the following events in the following order:
 * mousedown, focusout, mouseup. But the focusout event handler might change the layout of the website so that
 * the button on which the mousedown event occurred moves around. This leads to no mouseup event registered on
 * that button. Therefore a click event is also not registered because a click event consists of
 * a mousedown AND a mouseup event on that button. In order to fix that problem, this directive exposes a delayed focusout
 * event that is triggered AFTER the mousedown and mouseup events. When the delayed focusout event handler changes
 * positions of buttons, click events are still registered as you would expect.
 */
@Directive({
  selector: '[appDelayedFocusout]'
})
export class DelayedFocusoutDirective implements OnInit, OnDestroy {

  @Output() delayedFocusout = new EventEmitter<boolean>();
  isMouseDownSubject = new BehaviorSubject(false);

  private destroyed$: ReplaySubject<boolean> = new ReplaySubject(1);

  ngOnInit(): void {
    fromEvent(document.body, 'mousedown').pipe(takeUntil(this.destroyed$))
      .subscribe(() => this.isMouseDownSubject.next(true));
    fromEvent(document.body, 'mouseup').pipe(takeUntil(this.destroyed$))
      .subscribe(() => this.isMouseDownSubject.next(false));
  }

  @HostListener('focusout') onFocusout() {
    // If the mouse is currently down, we subscribe to the the event of
    // 'mouse being released' to then trigger the delayed focusout.
    // If the mouse is currently not down, we can trigger the delayed focusout immediately.
    if (this.isMouseDown()) {
      this.mouseRelease().subscribe(() => {
        // This code is executed once the mouse has been released.
        this.delayedFocusout.emit(true);
      });
    } else {
      this.delayedFocusout.emit(true);
    }
  }

  /**
   * Emits the value true once the mouse has been released and then completes.
   * Also completes when the mouse is not released but this directive is being destroyed.
   */
  mouseRelease() {
    return this.isMouseDownSubject.pipe(
      // Just negate isDown to get the value isReleased.
      takeUntil(this.destroyed$),
      map(isDown => !isDown),
      // Only proceed when the the mouse is released.
      filter(isReleased => isReleased),
      take(1)
    );
  }

  isMouseDown() {
    return this.isMouseDownSubject.value;
  }

  ngOnDestroy() {
    this.destroyed$.next();
    this.destroyed$.complete();
  }
}
Peregrination answered 6/9, 2021 at 10:0 Comment(0)
R
0

For me the mouse click event set the focus on a different element, so when I tried to catch focusout event I did not catch it.

My scenario was a text element that was replaced with an input element once you clicked the text element.

Solution was to focus on the edit element after a click on the text element was done with a setTimeout

(setTimeout is required in order to let rendering process complete before trying to call element.focus())

Rhizopod answered 18/11, 2021 at 12:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.