Tự động thêm trình nghe sự kiện


143

Tôi mới bắt đầu loay hoay với Angular 2 và tôi tự hỏi liệu có ai có thể cho tôi biết cách tốt nhất để tự động thêm và xóa người nghe sự kiện khỏi các yếu tố không.

Tôi có một thành phần được thiết lập. Khi một phần tử nào đó trong mẫu được nhấp vào, tôi muốn thêm một người nghe cho mousemovemột phần tử khác của cùng một mẫu. Sau đó tôi muốn loại bỏ trình nghe này khi nhấp vào phần tử thứ ba.

Tôi đã làm việc này chỉ bằng cách sử dụng Javascript đơn giản để lấy các phần tử và sau đó gọi tiêu chuẩn addEventListener()nhưng tôi tự hỏi liệu có cách nào " Angular2.0 " hơn để làm điều này mà tôi nên xem xét không.

Câu trả lời:


262

Trình kết xuất đã bị phản đối trong Angular 4.0.0-rc.1, hãy đọc bản cập nhật bên dưới

Cách angular2 là sử dụng listenhoặc listenGlobaltừ Renderer

Ví dụ: nếu bạn muốn thêm một sự kiện nhấp chuột vào Thành phần, bạn phải sử dụng Renderer và ElementRef (điều này cũng cung cấp cho bạn tùy chọn sử dụng ViewChild hoặc bất cứ thứ gì lấy ra nativeElement)

constructor(elementRef: ElementRef, renderer: Renderer) {

    // Listen to click events in the component
    renderer.listen(elementRef.nativeElement, 'click', (event) => {
      // Do something with 'event'
    })
);

Bạn có thể sử dụng listenGlobalmà sẽ cung cấp cho bạn truy cập vào document, bodyvv

renderer.listenGlobal('document', 'click', (event) => {
  // Do something with 'event'
});

Lưu ý rằng kể từ phiên bản beta.2 listenlistenGlobaltrả về một hàm để loại bỏ trình nghe (xem phần ngắt thay đổi từ thay đổi cho phiên bản beta.2). Điều này là để tránh rò rỉ bộ nhớ trong các ứng dụng lớn (xem # 6686 ).

Vì vậy, để loại bỏ trình nghe, chúng ta đã thêm động, chúng ta phải gán listenhoặc listenGlobalcho một biến sẽ giữ hàm trả về, và sau đó chúng ta thực thi nó.

// listenFunc will hold the function returned by "renderer.listen"
listenFunc: Function;

// globalListenFunc will hold the function returned by "renderer.listenGlobal"
globalListenFunc: Function;

constructor(elementRef: ElementRef, renderer: Renderer) {
    
    // We cache the function "listen" returns
    this.listenFunc = renderer.listen(elementRef.nativeElement, 'click', (event) => {
        // Do something with 'event'
    });

    // We cache the function "listenGlobal" returns
    this.globalListenFunc = renderer.listenGlobal('document', 'click', (event) => {
        // Do something with 'event'
    });
}

ngOnDestroy() {
    // We execute both functions to remove the respectives listeners

    // Removes "listen" listener
    this.listenFunc();
    
    // Removs "listenGlobal" listener
    this.globalListenFunc();
}

Đây là một plnkr với một ví dụ hoạt động. Ví dụ chứa cách sử dụng listenlistenGlobal.

Sử dụng RendererV2 với Angular 4.0.0-rc.1 + (Renderer2 kể từ 4.0.0-rc.3)

  • 25/2/2017 : Rendererđã không được dùng nữa, bây giờ chúng ta nên sử dụng RendererV2(xem dòng bên dưới). Xem các cam kết .

  • 10/03/2017 : RendererV2được đổi tên thành Renderer2. Xem những thay đổi phá vỡ .

RendererV2không có thêm listenGlobalchức năng cho các sự kiện toàn cầu (tài liệu, cơ thể, cửa sổ). Nó chỉ có một listenchức năng đạt được cả hai chức năng.

Để tham khảo, tôi sao chép và dán mã nguồn của triển khai DOM Renderer vì nó có thể thay đổi (vâng, nó góc cạnh!).

listen(target: 'window'|'document'|'body'|any, event: string, callback: (event: any) => boolean):
      () => void {
    if (typeof target === 'string') {
      return <() => void>this.eventManager.addGlobalEventListener(
          target, event, decoratePreventDefault(callback));
    }
    return <() => void>this.eventManager.addEventListener(
               target, event, decoratePreventDefault(callback)) as() => void;
  }

Như bạn có thể thấy, bây giờ nó xác minh nếu chúng ta truyền một chuỗi (tài liệu, phần thân hoặc cửa sổ), trong trường hợp đó, nó sẽ sử dụng một addGlobalEventListenerhàm bên trong . Trong mọi trường hợp khác, khi chúng ta vượt qua một phần tử (localEuity), nó sẽ sử dụng một đơn giảnaddEventListener

Để loại bỏ người nghe, nó giống như Renderertrong góc 2.x. listentrả về một hàm, sau đó gọi hàm đó

Thí dụ

// Add listeners
let global = this.renderer.listen('document', 'click', (evt) => {
  console.log('Clicking the document', evt);
})

let simple = this.renderer.listen(this.myButton.nativeElement, 'click', (evt) => {
  console.log('Clicking the button', evt);
});

// Remove listeners
global();
simple();

plnkr với Angular 4.0.0-rc.1 bằng RendererV2

plnkr với Angular 4.0.0-rc.3 bằng Renderer2


Đây chỉ là ngày thứ hai của tôi với Angular2 và tôi hầu như không bắt đầu tìm hiểu về v1 nên rất nhiều điều này khá khó hiểu. Bạn đã cho tôi rất nhiều thứ để đọc lên mặc dù vậy tôi sẽ đóng cái này xuống và chắc chắn sẽ quay lại sớm với nhiều câu hỏi liên quan hơn. Chúc mừng cho phản hồi chi tiết :)
popClingwrap

3
@popClingwrap bạn cũng có thể kiểm tra HostListener . Trong các tài liệu, hãy kiểm tra các chỉ thị thuộc tính trong phần Phản hồi hành động của người dùng để xem cách hostsử dụng.
Eric Martinez

@EricMartinez có cách nào để ngừng nghe hoặc ngheGlobal không? (giống như removeEventListener)
Nik

3
@ user1394625 có, như bạn có thể thấy trong câu trả lời ngOnDestroymã, cả hai listenlistenGlobaltrả về một hàm mà khi được gọi / thực thi, người nghe sẽ bị loại bỏ. Vì vậy, như bạn thấy this.funcđang giữ chức năng được trả về renderer.listenvà khi tôi thực hiện, this.func()tôi sẽ loại bỏ trình nghe. Cũng vậy listenGlobal.
Eric Martinez

@EricMartinez có thêm một câu hỏi cho bạn ... làm cách nào tôi có thể truy cập vào 'sự kiện' bên trong chức năng để ngăn chặn Default () hoặc stopPropagation ()
Nik

5

Tôi thấy điều này vô cùng khó hiểu. như @EricMartinez chỉ ra Renderer2 lắng nghe () trả về hàm để loại bỏ trình nghe:

ƒ () { return element.removeEventListener(eventName, /** @type {?} */ (handler), false); }

Nếu tôi thêm một người nghe

this.listenToClick = this.renderer.listen('document', 'click', (evt) => {
    alert('Clicking the document');
})

Tôi chỉ mong chức năng của mình thực thi những gì tôi dự định, chứ không phải hoàn toàn ngược lại, đó là loại bỏ người nghe.

// I´d expect an alert('Clicking the document'); 
this.listenToClick();
// what you actually get is removing the listener, so nothing...

Trong kịch bản đã cho, Nó thực sự có ý nghĩa hơn để đặt tên cho nó như sau:

// Add listeners
let unlistenGlobal = this.renderer.listen('document', 'click', (evt) => {
    console.log('Clicking the document', evt);
})

let removeSimple = this.renderer.listen(this.myButton.nativeElement, 'click', (evt) => {
    console.log('Clicking the button', evt);
});

Phải có một lý do chính đáng cho việc này nhưng theo tôi thì nó rất sai lệch và không trực quan.


3
Nếu bạn đã thêm một trình nghe, tại sao bạn lại mong đợi rằng hàm được trả về bằng cách thêm trình nghe đó sẽ gọi trình nghe đó? Điều đó không có ý nghĩa nhiều với tôi. Toàn bộ quan điểm của việc thêm người nghe là phản hồi các sự kiện mà bạn không nhất thiết phải kích hoạt theo chương trình. Tôi nghĩ rằng nếu bạn mong đợi chức năng đó gọi người nghe của bạn, bạn có thể không hiểu người nghe hoàn toàn.
Willwsharp

@tahiche mate này thực sự khó hiểu, cảm ơn vì đã chỉ ra điều này!
godblessstrawberry

Nó trả về điều này để bạn cũng có thể loại bỏ người nghe một lần nữa khi bạn phá hủy thành phần của mình sau này. Khi thêm người nghe, nên loại bỏ chúng sau này khi bạn không cần chúng nữa. Vì vậy, lưu trữ giá trị trả về này và gọi nó trong ngOnDestroyphương thức của bạn . Tôi thừa nhận rằng ban đầu nó có vẻ khó hiểu, nhưng nó thực sự là một tính năng rất hữu ích. Làm thế nào khác dọn dẹp sau khi chính mình?
Héo

1

Tôi sẽ thêm một ví dụ StackBlitz và một nhận xét cho câu trả lời từ @tahiche.

Giá trị trả về là một hàm để loại bỏ trình nghe sự kiện sau khi bạn đã thêm nó. Nó được coi là thực hành tốt để loại bỏ người nghe sự kiện khi bạn không cần chúng nữa. Vì vậy, bạn có thể lưu trữ giá trị trả về này và gọi nó trong ngOnDestroyphương thức của bạn .

Tôi thừa nhận rằng ban đầu nó có vẻ khó hiểu, nhưng nó thực sự là một tính năng rất hữu ích. Làm thế nào khác bạn có thể làm sạch sau khi chính mình?

export class MyComponent implements OnInit, OnDestroy {

  public removeEventListener: () => void;

  constructor(
    private renderer: Renderer2, 
    private elementRef: ElementRef
  ) {
  }

  public ngOnInit() {
    this.removeEventListener = this.renderer.listen(this.elementRef.nativeElement, 'click', (event) => {
      if (event.target instanceof HTMLAnchorElement) {
        // Prevent opening anchors the default way
        event.preventDefault();
        // Your custom anchor click event handler
        this.handleAnchorClick(event);
      }
    });
  }

  public ngOnDestroy() {
    this.removeEventListener();
  }
}

Bạn có thể tìm thấy một StackBlitz ở đây để cho thấy cách thức này có thể hoạt động để bắt nhấp vào các phần tử neo.

Tôi đã thêm một cơ thể với một hình ảnh như sau:
<img src="x" onerror="alert(1)"></div>
để cho thấy rằng chất khử trùng đang làm công việc của nó.

Ở đây trong câu đố này, bạn tìm thấy cùng một cơ thể gắn liền với innerHTMLmà không vệ sinh nó và nó sẽ chứng minh vấn đề.


0

Đây là cách giải quyết của tôi:

Tôi đã tạo một thư viện với Angular 6. Tôi đã thêm một thành phần phổ biến commonlib-headerđược sử dụng như thế này trong một ứng dụng bên ngoài.

Lưu ý serviceReferencelớp nào (được chèn trong thành phần constructor(public serviceReference: MyService)sử dụng commonlib-header) giữ stringFunctionNamephương thức:

<commonlib-header
    [logo]="{ src: 'assets/img/logo.svg', alt: 'Logo', href: '#' }"
    [buttons]="[{ index: 0, innerHtml: 'Button', class: 'btn btn-primary', onClick: [serviceReference, 'stringFunctionName', ['arg1','arg2','arg3']] }]">
    </common-header>

Thành phần thư viện được lập trình như thế này. Sự kiện động được thêm vào trong onClick(fn: any)phương thức:

export class HeaderComponent implements OnInit {

 _buttons: Array<NavItem> = []

 @Input()
  set buttons(buttons: Array<any>) {
    buttons.forEach(navItem => {
      let _navItem = new NavItem(navItem.href, navItem.innerHtml)

      _navItem.class = navItem.class

      _navItem.onClick = navItem.onClick // this is the array from the component @Input properties above

      this._buttons[navItem.index] = _navItem
    })
  }

  constructor() {}

  ngOnInit() {}

  onClick(fn: any){
    let ref = fn[0]
    let fnName = fn[1]
    let args = fn[2]

    ref[fnName].apply(ref, args)
  }

Việc tái sử dụng header.component.html:

<div class="topbar-right">
  <button *ngFor="let btn of _buttons"
    class="{{ btn.class }}"
    (click)="onClick(btn.onClick)"
    [innerHTML]="btn.innerHtml | keepHtml"></button>
</div>
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.