Динамически добавлять прослушиватель событий


146

Я только начинаю возиться с Angular 2, и мне интересно, может ли кто-нибудь сказать мне, как лучше всего динамически добавлять и удалять прослушиватели событий из элементов.

У меня настроен компонент. При щелчке по определенному элементу в шаблоне я хочу добавить слушателя для mousemoveдругого элемента того же шаблона. Затем я хочу удалить этого слушателя при нажатии третьего элемента.

Я вроде как заставил это работать, просто используя простой Javascript для захвата элементов, а затем вызывая стандарт, addEventListener()но мне было интересно, есть ли способ сделать это более " Angular2.0 ", который я должен изучить .

Ответы:


268

Renderer устарел в Angular 4.0.0-rc.1, прочтите обновление ниже

Angular2 способ заключается в использовании listenили listenGlobalот видеообработки

Например, если вы хотите добавить событие щелчка к компоненту, вы должны использовать Renderer и ElementRef (это также дает вам возможность использовать ViewChild или что-нибудь, что извлекает nativeElement)

constructor(elementRef: ElementRef, renderer: Renderer) {

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

Вы можете использовать listenGlobalэто даст вам доступ к document, bodyи т.д.

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

Обратите внимание , что с beta.2 как listenи listenGlobalвозвращает функцию для удаления слушателя (см отличий раздела из журнала изменений для beta.2). Это сделано для того, чтобы избежать утечки памяти в больших приложениях (см. № 6686 ).

Итак, чтобы удалить динамически добавленного слушателя, мы должны назначить listenили listenGlobalпеременной, которая будет содержать возвращаемую функцию, а затем мы ее выполним.

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

Вот plnkr с примером работы. Пример содержит использование listenи listenGlobal.

Использование RendererV2 с Angular 4.0.0-rc.1 + (Renderer2 начиная с 4.0.0-rc.3)

  • 25.02.2017 : Rendererустарело, теперь мы должны использовать RendererV2(см. Строку ниже). Смотрите коммит .

  • 03.10.2017 : RendererV2переименован в Renderer2. Смотрите критические изменения .

RendererV2больше не имеет listenGlobalфункции для глобальных событий (документ, тело, окно). У него есть только listenфункция, которая выполняет обе функции.

Для справки, я копирую и вставляю исходный код реализации DOM Renderer, поскольку он может измениться (да, это угловой!).

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

Как видите, теперь он проверяет, передаем ли мы строку (документ, тело или окно), и в этом случае он будет использовать внутреннюю addGlobalEventListenerфункцию. В любом другом случае, когда мы передаем элемент (nativeElement), он будет использовать простойaddEventListener

Чтобы удалить слушателя, он такой же, как и Rendererв angular 2.x. listenвозвращает функцию, затем вызывает эту функцию.

пример

// 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 с Angular 4.0.0-rc.1 с использованием RendererV2

plnkr с Angular 4.0.0-rc.3 с использованием Renderer2


Это всего лишь мой второй день работы с Angular2, и я только начал разбираться в v1, так что многое из этого довольно запутанно. Вы дали мне много материала для чтения, поэтому я закрываю его и, несомненно, скоро вернусь с ОЧЕНЬ большим количеством связанных вопросов. Приветствую вас за подробный ответ :)
popClingwrap

3
@popClingwrap, вы также можете проверить HostListener . В документации проверьте директивы атрибутов в разделе « Ответить на действия пользователя», чтобы узнать, как они hostиспользуются.
Эрик Мартинес

@EricMartinez есть ли способ перестать слушать, чтобы слушать или listenGlobal? (то же, что и removeEventListener)
Nik

3
@ user1394625 да, как вы можете видеть в ответе ngOnDestroyкода, оба listenи listenGlobalвозвращают функцию, которая при вызове / выполнении слушателя удаляется. Итак, как вы видите this.func, удерживает функцию, возвращаемуюrenderer.listen и когда я это this.func()делаю, я удаляю слушателя. То же самое и с listenGlobal.
Эрик Мартинес,

@EricMartinez получил еще один вопрос к вам ... как мне получить доступ к «событию» внутри функции, чтобы предотвратитьDefault () или stopPropagation ()
Ник,

6

Я также считаю это крайне запутанным. как указывает @EricMartinez, Renderer2 listen () возвращает функцию для удаления слушателя:

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

Если я добавляю слушателя

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

Я ожидал, что моя функция выполнит то, что я задумал, а не полную противоположность, а именно удаление слушателя.

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

В данном сценарии было бы разумнее назвать его так:

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

Для этого должна быть веская причина, но, на мой взгляд, это вводит в заблуждение и не интуитивно понятно.


3
Если вы добавляли слушателя, почему вы ожидали, что функция, возвращаемая добавлением этого слушателя, вызовет этот слушатель? Для меня это не имеет особого смысла. Весь смысл добавления слушателя заключается в том, чтобы реагировать на события, которые вы не можете запускать программно. Я думаю, что если вы ожидали, что эта функция вызовет вашего слушателя, возможно, вы не полностью понимаете слушателей.
Willwsharp

@tahiche, приятель, это действительно сбивает с толку, спасибо, что указали на это!
godblessstrawberry

Он возвращает это, так что вы также можете снова удалить слушателя, когда позже вы уничтожите свой компонент. При добавлении слушателей считается хорошей практикой удалить их позже, когда они вам больше не нужны. Поэтому сохраните это возвращаемое значение и вызовите его внутри своего ngOnDestroyметода. Признаюсь, сначала это может показаться запутанным, но на самом деле это очень полезная функция. Как еще убрать за собой?
Уилт

0

Вот мой обходной путь:

Я создал библиотеку с Angular 6. Я добавил общий компонент commonlib-header который так используется во внешнем приложении.

Обратите внимание на serviceReferenceкласс (внедренный в компонент, constructor(public serviceReference: MyService)который использует commonlib-header), содержащий stringFunctionNameметод:

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

Библиотечный компонент запрограммирован так. В onClick(fn: any)метод добавлено динамическое событие :

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

Многоразовые 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>

0

Я добавлю пример StackBlitz и комментарий к ответу @tahiche.

Возвращаемое значение - это функция для удаления прослушивателя событий после того, как вы его добавили. Считается хорошей практикой удалять прослушиватели событий, когда они вам больше не нужны. Таким образом, вы можете сохранить это возвращаемое значение и вызвать его внутри своего ngOnDestroyметода.

Признаюсь, сначала это может показаться запутанным, но на самом деле это очень полезная функция. Как еще можно убрать за собой?

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

Вы можете найти StackBlitz здесь чтобы показать, как это может работать для отлова щелчка по элементам привязки.

Я добавил тело с изображением следующим образом:
<img src="x" onerror="alert(1)"></div>
чтобы показать, что дезинфицирующее средство выполняет свою работу.

Здесь, в этой скрипке, вы найдете то же тело, прикрепленное к innerHTMLобъекту без его дезинфекции, и это продемонстрирует проблему.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.