Виявити клацання поза кутовим компонентом


Відповіді:


187
import { Component, ElementRef, HostListener, Input } from '@angular/core';

@Component({
  selector: 'selector',
  template: `
    <div>
      {{text}}
    </div>
  `
})
export class AnotherComponent {
  public text: String;

  @HostListener('document:click', ['$event'])
  clickout(event) {
    if(this.eRef.nativeElement.contains(event.target)) {
      this.text = "clicked inside";
    } else {
      this.text = "clicked outside";
    }
  }

  constructor(private eRef: ElementRef) {
    this.text = 'no clicks yet';
  }
}

Робочий приклад - натисніть тут


13
Це не працює, якщо всередині тригерного елемента є елемент, керований ngIf, оскільки ngIf видалення елемента з DOM відбувається до події натискання: plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p=preview
J. Frankenstein

чи працює він над компонентом, який створив динамічно через: const factory = this.resolver.resolveComponentFactory (MyComponent); const elem = this.vcr.createComponent (factory);
Аві Моралі

1
Приємна стаття на цю тему: christianliebel.com/2016/05/…
Мігель Лара

47

Альтернатива відповіді Амадьяра. Ця версія працює при натисканні на елемент, який видаляється з DOM за допомогою ngIf.

http://plnkr.co/edit/4mrn4GjM95uvSbQtxrAS?p=preview

  private wasInside = false;
  
  @HostListener('click')
  clickInside() {
    this.text = "clicked inside";
    this.wasInside = true;
  }
  
  @HostListener('document:click')
  clickout() {
    if (!this.wasInside) {
      this.text = "clicked outside";
    }
    this.wasInside = false;
  }


Це чудово працює і з ngif або динамічними оновленнями
Vikas Kandari

Це приголомшливо
Володимир Демірев

23

Прив’язка до натискання документа через @Hostlistener дорога. Це може і матиме видимий вплив на ефективність при надмірному використанні (наприклад, при створенні спеціального компонента, що випадає, і у вас створено кілька екземплярів у формі).

Я пропоную додавати @Hostlistener () до події клацання документа лише один раз у вашому головному компоненті додатка. Подія повинна підштовхнути значення клацнутого цільового елемента всередині загальнодоступного предмета, що зберігається в глобальній службі утиліти.

@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {

  constructor(private utilitiesService: UtilitiesService) {}

  @HostListener('document:click', ['$event'])
  documentClick(event: any): void {
    this.utilitiesService.documentClickedTarget.next(event.target)
  }
}

@Injectable({ providedIn: 'root' })
export class UtilitiesService {
   documentClickedTarget: Subject<HTMLElement> = new Subject<HTMLElement>()
}

Хто зацікавлений у вибраному цільовому елементі, повинен підписатися на загальнодоступний предмет нашої служби комунальних послуг та скасувати підписку, коли компонент знищений.

export class AnotherComponent implements OnInit {

  @ViewChild('somePopup', { read: ElementRef, static: false }) somePopup: ElementRef

  constructor(private utilitiesService: UtilitiesService) { }

  ngOnInit() {
      this.utilitiesService.documentClickedTarget
           .subscribe(target => this.documentClickListener(target))
  }

  documentClickListener(target: any): void {
     if (this.somePopup.nativeElement.contains(target))
        // Clicked inside  
     else
        // Clicked outside
  }

4
Я думаю, що ця відповідь повинна стати прийнятою відповіддю, оскільки це дозволяє зробити багато оптимізацій: як у цьому прикладі
edoardo849,

це найкрасивіше рішення, яке я отримав в Інтернеті
Anup Bangale

1
@lampshade Правильно. Я говорив про це. Прочитайте ще раз відповідь. Я залишаю реалізацію підписки на ваш стиль (takeUntil (), Subscription.add ()). Не забудьте скасувати підписку!
ginalx

@ginalx Я реалізував ваше рішення, воно працює як очікувалося. Хоча я стикався з проблемою, як я цим користуюся. Ось питання, будь ласка, подивіться
Нілеш

6

Наведені вище відповіді правильні, але що робити, якщо ви робите важкий процес після втрати уваги від відповідного компонента. Для цього я запропонував рішення з двома прапорами, де процес події фокусування буде відбуватися лише при втраті фокусу лише від відповідного компонента.

isFocusInsideComponent = false;
isComponentClicked = false;

@HostListener('click')
clickInside() {
    this.isFocusInsideComponent = true;
    this.isComponentClicked = true;
}

@HostListener('document:click')
clickout() {
    if (!this.isFocusInsideComponent && this.isComponentClicked) {
        // do the heavy process

        this.isComponentClicked = false;
    }
    this.isFocusInsideComponent = false;
}

Сподіваюся, що це вам допоможе. Виправте мене, якщо щось пропустили.



2

Відповідь ginalx має бути встановлена ​​за замовчуванням: imo: цей метод дозволяє здійснити багато оптимізацій.

Проблема

Скажіть, що у нас є список елементів, і до кожного пункту ми хочемо включити меню, яке потрібно змінити. Ми включаємо кнопку перемикання на кнопку, яка слухає clickподію на собі (click)="toggle()", але ми також хочемо перемикати меню кожного разу, коли користувач натискає його. Якщо список елементів зростає, і ми додаємо до @HostListener('document:click')кожного меню, то кожне завантажене в ньому меню почне прослуховувати клацання по всьому документу, навіть коли меню вимкнено. Окрім очевидних питань щодо ефективності, це зайве.

Наприклад, ви можете підписатися щоразу, коли спливаюче вікно перемикається за допомогою клацання, і тільки потім починати прослуховувати "зовнішні кліки".


isActive: boolean = false;

// to prevent memory leaks and improve efficiency, the menu
// gets loaded only when the toggle gets clicked
private _toggleMenuSubject$: BehaviorSubject<boolean>;
private _toggleMenu$: Observable<boolean>;

private _toggleMenuSub: Subscription;
private _clickSub: Subscription = null;


constructor(
 ...
 private _utilitiesService: UtilitiesService,
 private _elementRef: ElementRef,
){
 ...
 this._toggleMenuSubject$ = new BehaviorSubject(false);
 this._toggleMenu$ = this._toggleMenuSubject$.asObservable();

}

ngOnInit() {
 this._toggleMenuSub = this._toggleMenu$.pipe(
      tap(isActive => {
        logger.debug('Label Menu is active', isActive)
        this.isActive = isActive;

        // subscribe to the click event only if the menu is Active
        // otherwise unsubscribe and save memory
        if(isActive === true){
          this._clickSub = this._utilitiesService.documentClickedTarget
           .subscribe(target => this._documentClickListener(target));
        }else if(isActive === false && this._clickSub !== null){
          this._clickSub.unsubscribe();
        }

      }),
      // other observable logic
      ...
      ).subscribe();
}

toggle() {
    this._toggleMenuSubject$.next(!this.isActive);
}

private _documentClickListener(targetElement: HTMLElement): void {
    const clickedInside = this._elementRef.nativeElement.contains(targetElement);
    if (!clickedInside) {
      this._toggleMenuSubject$.next(false);
    }    
 }

ngOnDestroy(){
 this._toggleMenuSub.unsubscribe();
}

І, в *.component.html:


<button (click)="toggle()">Toggle the menu</button>


Хоча я згоден з тим, як ви думаєте, я б запропонував не заповнювати всю логіку в tapоператорі. Натомість використовуйте skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false)). Таким чином ви використовуєте набагато більше RxJ і пропускаєте деякі підписки.
Gizrah

0

Поліпшення @J. Франкенштейн

  
  @HostListener('click')
  clickInside($event) {
    this.text = "clicked inside";
    $event.stopPropagation();
  }
  
  @HostListener('document:click')
  clickout() {
      this.text = "clicked outside";
  }


-1

Ви можете викликати функцію події, як-от (фокус) або (розмиття), а потім поставити код

<div tabindex=0 (blur)="outsideClick()">raw data </div>
 

 outsideClick() {
  alert('put your condition here');
   }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.