Phát hiện nhấp chuột bên ngoài thành phần Angular


Câu trả lời:


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';
  }
}

Một ví dụ làm việc - bấm vào đây


13
Điều này không hoạt động khi có một phần tử được điều khiển bởi ngNếu bên trong phần tử kích hoạt, vì ngNếu xóa phần tử khỏi DOM xảy ra trước sự kiện nhấp chuột: plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p=preview
J. Frankenstein

nó có hoạt động trên một thành phần được tạo dynamiclly thông qua: const factory = this.resolver.resolveComponentFactory (MyComponent); const elem = this.vcr.createComponent (nhà máy);
Avi Moraly

1
Một bài viết tốt đẹp về chủ đề này: christianliebel.com/2016/05/...
Miguel Lara

47

Một thay thế cho câu trả lời của AMagyar. Phiên bản này hoạt động khi bạn nhấp vào phần tử bị xóa khỏi DOM bằng 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;
  }


Này hoạt động hoàn hảo với ngif hoặc nâng cấp động cũng
Vikas Kandari

Điều này thật tuyệt vời
Vladimir Demirev

23

Việc liên kết với tài liệu khi nhấp qua @Hostlistener rất tốn kém. Nó có thể và sẽ có tác động rõ ràng về hiệu suất nếu bạn sử dụng quá mức (ví dụ: khi tạo thành phần tùy chỉnh thả xuống và bạn có nhiều phiên bản được tạo trong một biểu mẫu).

Tôi khuyên bạn nên thêm @Hostlistener () vào sự kiện nhấp vào tài liệu chỉ một lần bên trong thành phần ứng dụng chính của bạn. Sự kiện sẽ đẩy giá trị của phần tử mục tiêu được nhấp vào bên trong một chủ đề công khai được lưu trữ trong một dịch vụ tiện ích toàn cầu.

@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>()
}

Bất cứ ai quan tâm đến phần tử mục tiêu được nhấp nên đăng ký chủ đề công khai của dịch vụ tiện ích của chúng tôi và hủy đăng ký khi thành phần bị phá hủy.

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
Tôi nghĩ rằng câu này sẽ trở thành câu trả lời được chấp nhận vì nó cho phép nhiều tối ưu hóa: như trong ví dụ này
edoardo849

đây là giải pháp hay nhất mà tôi có trên internet
Anup Bangale

1
@lampshade Đúng. Tôi đã nói về điều này. Đọc lại câu trả lời. Tôi để việc triển khai hủy đăng ký theo phong cách của bạn (takeUntil (), Subscription.add ()). Đừng quên hủy đăng ký!
ginalx

@ginalx Tôi đã triển khai giải pháp của bạn, nó hoạt động như mong đợi. Mặc dù đã gặp sự cố theo cách tôi sử dụng nó. Đây là câu hỏi, vui lòng xem
Nilesh

6

Các câu trả lời được đề cập ở trên là đúng nhưng điều gì sẽ xảy ra nếu bạn đang thực hiện một quá trình nặng sau khi mất tập trung từ thành phần liên quan. Đối với điều đó, tôi đã đưa ra một giải pháp có hai cờ mà quá trình sự kiện tiêu điểm sẽ chỉ diễn ra khi chỉ mất tiêu điểm từ thành phần có liên quan.

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

Hy vọng điều này sẽ giúp bạn. Sửa tôi Nếu có bỏ lỡ bất cứ điều gì.



2

Câu trả lời của ginalx nên được đặt làm imo mặc định: phương pháp này cho phép nhiều tối ưu hóa.

Vấn đề

Giả sử rằng chúng tôi có một danh sách các mục và trên mỗi mục, chúng tôi muốn bao gồm một menu cần được chuyển đổi. Chúng tôi bao gồm một nút bật / tắt tự lắng nghe một clicksự kiện (click)="toggle()", nhưng chúng tôi cũng muốn chuyển đổi menu bất cứ khi nào người dùng nhấp vào bên ngoài nó. Nếu danh sách các mục tăng lên và chúng tôi đính kèm một @HostListener('document:click')trên mọi menu, thì mọi menu được tải trong mục sẽ bắt đầu lắng nghe khi nhấp chuột trên toàn bộ tài liệu, ngay cả khi menu được tắt. Bên cạnh các vấn đề về hiệu suất rõ ràng, điều này là không cần thiết.

Ví dụ: bạn có thể đăng ký bất cứ khi nào cửa sổ bật lên được bật lên qua một lần nhấp và chỉ bắt đầu nghe "nhấp chuột bên ngoài" sau đó.


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

Và, trong *.component.html:


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

Mặc dù tôi đồng ý với cách bạn nghĩ, nhưng tôi khuyên bạn không nên nhồi nhét tất cả logic vào một taptoán tử. Thay vào đó, hãy sử dụng skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false)). Bằng cách này, bạn sử dụng nhiều RxJ hơn và bỏ qua một số đăng ký.
Gizrah

0

Cải thiện @J. Frankenstein câu trả lời

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


-1

bạn có thể gọi hàm sự kiện như (tiêu điểm) hoặc (làm mờ) rồi bạn đặt mã của bạn

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

 outsideClick() {
  alert('put your condition here');
   }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.