Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

focusout on an element conflicting with click on other in Angular

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');
    }); 
 }
like image 271
Addy Avatar asked Dec 02 '22 10:12

Addy


2 Answers

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.

like image 195
Vinod Bhavnani Avatar answered Dec 27 '22 11:12

Vinod Bhavnani


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;
  }
}
like image 43
Simon Lammes Avatar answered Dec 27 '22 12:12

Simon Lammes