ตรวจจับการคลิกภายนอกส่วนประกอบเชิงมุม


คำตอบ:


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

ตัวอย่างการทำงาน - คลิกที่นี่


14
สิ่งนี้ใช้ไม่ได้เมื่อมีองค์ประกอบที่ควบคุมโดย ngIf ภายในองค์ประกอบทริกเกอร์เนื่องจาก ngIf การลบองค์ประกอบออกจาก DOM จะเกิดขึ้นก่อนเหตุการณ์การคลิก: plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p=preview
J. Frankenstein

มันทำงานกับส่วนประกอบที่สร้าง dynamiclly ผ่าน: const factory = this.resolver.resolveComponentFactory (MyComponent); const elem = this.vcr.createComponent (โรงงาน);
Avi Moraly

1
บทความดีๆในหัวข้อนี้: christianliebel.com/2016/05/…
Miguel Lara

51

ทางเลือกแทนคำตอบของ AMagyar เวอร์ชันนี้ใช้งานได้เมื่อคุณคลิกที่องค์ประกอบที่ถูกลบออกจาก 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;
  }


1
สิ่งนี้ทำงานได้ดีกับ ngif หรือการอัปเดตแบบไดนามิกเช่นกัน
Vikas Kandari

1
นี่มันยอดเยี่ยมมาก
Vladimir Demirev

32

การเชื่อมโยงกับเอกสารคลิกผ่าน @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
  }

5
ฉันคิดว่าคำตอบนี้ควรจะกลายเป็นคำตอบที่ยอมรับได้เนื่องจากสามารถเพิ่มประสิทธิภาพได้หลายอย่างเช่นในตัวอย่างนี้
edoardo849

นี่คือทางออกที่สวยที่สุดที่ฉันได้รับจากอินเทอร์เน็ต
Anup Bangale

2
@lampshade ถูกต้อง. ผมพูดถึงเรื่องนี้ อ่านคำตอบอีกครั้ง ฉันออกจากการดำเนินการยกเลิกการสมัครตามสไตล์ของคุณ (takeUntil (), Subscription.add ()) อย่าลืมยกเลิกการสมัคร!
ginalx

@ginalx ฉันใช้โซลูชันของคุณมันทำงานได้ตามที่คาดไว้ แม้ว่าจะพบปัญหาในแบบที่ฉันใช้ นี่คือคำถามโปรดดู
Nilesh

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

หวังว่านี่จะช่วยคุณได้ แก้ไขฉันหากพลาดอะไรไป


4

คำตอบของ 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>


1
แม้ว่าฉันจะเห็นด้วยกับวิธีที่คุณคิด แต่ฉันไม่แนะนำให้ใช้ตรรกะทั้งหมดในตัวtapดำเนินการ ให้ใช้ skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false))ไฟล์. วิธีนี้ทำให้คุณใช้ประโยชน์จาก RxJ ได้มากขึ้นและข้ามการสมัครสมาชิกบางส่วน
Gizrah



-1

คุณสามารถเรียกใช้ฟังก์ชันเหตุการณ์เช่น (โฟกัส) หรือ (เบลอ) จากนั้นคุณใส่รหัสของคุณ

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

 outsideClick() {
  alert('put your condition here');
   }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.