Dynamicznie dodawaj odbiornik zdarzeń

143

Właśnie zaczynam bawić się Angular 2 i zastanawiam się, czy ktoś może mi powiedzieć najlepszy sposób na dynamiczne dodawanie i usuwanie detektorów zdarzeń z elementów.

Mam skonfigurowany komponent. Po kliknięciu określonego elementu w szablonie chcę dodać detektor mousemovedo innego elementu tego samego szablonu. Następnie chcę usunąć ten odbiornik po kliknięciu trzeciego elementu.

W pewnym sensie to działało, używając zwykłego Javascript do przechwytywania elementów, a następnie wywoływania standardu, addEventListener()ale zastanawiałem się, czy istnieje bardziej " Angular2.0 " sposób na zrobienie tego, który powinienem przyjrzeć się.

popClingwrap
źródło

Odpowiedzi:

262

Renderer stał się przestarzały w Angular 4.0.0-rc.1, przeczytaj aktualizację poniżej

Sposób angular2 polega na użyciu listenlub listenGlobalz Renderer

Na przykład, jeśli chcesz dodać zdarzenie kliknięcia do komponentu, musisz użyć Renderer i ElementRef (daje to również opcję użycia ViewChild lub czegokolwiek, co pobiera nativeElement)

constructor(elementRef: ElementRef, renderer: Renderer) {

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

Można użyć listenGlobal, który daje dostęp do document, bodyitp

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

Należy zauważyć, że zarówno od beta.2 listeni listenGlobalpowrócić do funkcji, aby usunąć słuchacza (patrz łamanie zmienia sekcję z changelogu dla beta.2). Ma to na celu uniknięcie wycieków pamięci w dużych aplikacjach (patrz # 6686 ).

Aby więc dynamicznie usunąć detektor, który dodaliśmy, musimy przypisać listenlub listenGlobaldo zmiennej, która będzie przechowywać zwróconą funkcję, a następnie ją wykonać.

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

Oto plnkr z działającym przykładem. Przykład zawiera użycie listeni listenGlobal.

Używanie RendererV2 z Angular 4.0.0-rc.1 + (Renderer2 od 4.0.0-rc.3)

  • 25/02/2017 : Rendererzostał wycofany, teraz powinniśmy użyć RendererV2(zobacz wiersz poniżej). Zobacz zatwierdzenie .

  • 10/03/2017 : RendererV2nazwa została zmieniona na Renderer2. Zobacz przełomowe zmiany .

RendererV2nie ma już listenGlobalfunkcji dla zdarzeń globalnych (dokument, treść, okno). Ma tylko listenfunkcję, która osiąga obie funkcjonalności.

Dla porównania, kopiuję i wklejam kod źródłowy implementacji DOM Renderer, ponieważ może się zmienić (tak, jest kątowy!).

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

Jak widać, teraz sprawdza, czy przekazujemy ciąg (dokument, treść lub okno), w takim przypadku użyje addGlobalEventListenerfunkcji wewnętrznej . W każdym innym przypadku, gdy przekażemy element (nativeElement), użyje on prostejaddEventListener

Usunięcie słuchacza jest takie samo, jak Rendererw przypadku angular 2.x. listenzwraca funkcję, a następnie wywołuje tę funkcję.

Przykład

// 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 z Angular 4.0.0-rc.1 przy użyciu RendererV2

plnkr z Angular 4.0.0-rc.3 przy użyciu Renderer2

Eric Martinez
źródło
To dopiero mój drugi dzień z Angular2 i ledwie zacząłem się zastanawiać nad wersją 1, więc wiele z tego jest dość nowych i mylących. Dałeś mi sporo rzeczy do przeczytania, więc zamykam to i niewątpliwie wkrótce wrócę z WIELE bardziej powiązanych pytań. Pozdrawiam za szczegółową odpowiedź :)
popClingwrap
3
@popClingwrap możesz również sprawdzić HostListener . W dokumentach sprawdź dyrektywy Attribute w sekcji Respond to user action, aby zobaczyć, jak hostsą one używane.
Eric Martinez
@EricMartinez Czy jest sposób, aby przestać słuchać, albo słuchać, albo słuchaćGlobal? (tak samo jak removeEventListener)
Nik
3
@ user1394625 tak, jak widać w odpowiedzi na ngOnDestroykod, oba listeni listenGlobalzwracają funkcję, która po wywołaniu / wykonaniu nasłuchiwania jest usuwana. Więc jak widzisz, this.functrzyma funkcję zwróconą przez, renderer.listena kiedy to zrobię this.func(), usuwam detektor. To samo dotyczy listenGlobal.
Eric Martinez
@EricMartinez ma dla Ciebie jeszcze jedno pytanie ... jak mogę uzyskać dostęp do `` zdarzenia '' w funkcji zapobieganiaDefault () lub stopPropagation ()
Nik
5

Uważam to również za wyjątkowo zagmatwane. jak wskazuje @EricMartinez, Renderer2 listen () zwraca funkcję usuwającą nasłuchiwanie:

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

Jeśli dodaję słuchacza

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

Spodziewałbym się, że moja funkcja wykona to, co zamierzałem, a nie całkowite przeciwieństwo, czyli usunięcie słuchacza.

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

W danym scenariuszu bardziej sensowne byłoby nazwanie go następująco:

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

Musi być ku temu dobry powód, ale moim zdaniem jest to bardzo mylące i nie intuicyjne.

tahiche
źródło
3
Gdybyś dodawał detektor, dlaczego miałbyś oczekiwać, że funkcja zwrócona przez dodanie tego detektora wywoła ten detektor? To nie ma dla mnie większego sensu. Cały sens dodawania słuchacza polega na odpowiadaniu na zdarzenia, których niekoniecznie możesz wywołać programowo. Myślę, że jeśli spodziewasz się, że ta funkcja wywoła twojego słuchacza, możesz nie rozumieć słuchaczy całkowicie.
Willwsharp
@tahiche mate, to naprawdę zagmatwane, dzięki za wskazanie tego!
godblessstrawberry
Zwraca to, więc możesz również usunąć nasłuchiwanie, gdy później zniszczysz komponent. Podczas dodawania słuchaczy za dobrą praktykę uważa się usuwanie ich później, gdy nie są już potrzebne. Więc zapisz tę wartość zwracaną i wywołaj ją wewnątrz swojej ngOnDestroymetody. Przyznaję, że na początku może się to wydawać zagmatwane, ale w rzeczywistości jest to bardzo przydatna funkcja. Jak inaczej posprzątać po sobie?
Wilt
1

Dodam przykład StackBlitz oraz komentarz do odpowiedzi z @tahiche.

Wartość zwracana to funkcja służąca do usuwania detektora zdarzeń po jego dodaniu. Za dobrą praktykę uważa się usuwanie detektorów zdarzeń, gdy ich już nie potrzebujesz. Możesz więc przechowywać tę wartość zwracaną i wywoływać ją wewnątrz swojej ngOnDestroymetody.

Przyznaję, że na początku może się to wydawać zagmatwane, ale w rzeczywistości jest to bardzo przydatna funkcja. Jak inaczej możesz po sobie posprzątać?

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

Możesz znaleźć StackBlitz tutaj, aby pokazać, jak to może działać w przypadku łapania klikania elementów kotwicy.

Dodałem ciało z następującym obrazem:
<img src="x" onerror="alert(1)"></div>
aby pokazać, że środek odkażający wykonuje swoją pracę.

Tutaj, na tych skrzypcach , znajdziesz to samo ciało przymocowane do niego innerHTMLbez odkażania go i to zademonstruje problem.

Więdnąć
źródło
0

Oto moje obejście:

Stworzyłem bibliotekę za pomocą Angular 6. Dodałem wspólny komponent, commonlib-headerktóry jest używany w taki sposób w zewnętrznej aplikacji.

Zwróć uwagę, serviceReferencektóra jest klasą (wstrzykniętą w składniku, constructor(public serviceReference: MyService)który używa commonlib-header), która przechowuje stringFunctionNamemetodę:

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

Komponent biblioteki jest programowany w ten sposób. Zdarzenie dynamiczne jest dodawane w onClick(fn: any)metodzie:

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

Wielokrotnego użytku 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>
Gus
źródło