14

Since there is no template, what's the best way to listen to child elements event in a directive? Is it possible with HostListener? If not, is there any other way?

There's also this similar question: How to listen for child event from parent directive in Angular2, but the suggested approach does not solve my problem, as my directive and child element aren't in the same template (directive is on host).

Cheers!

Edit #1

This is how I'm currently doing it (there has to be a better way):

First injecting ElementRef into my directive:

constructor(private elView: ElementRef) {}

Then binding with jQuery (or plain JS):

$(this.elView.nativeElement)
.on('drag', '#childId', this.slide.bind(this))
Community
  • 1
  • 1
Maxime Dupré
  • 5,319
  • 7
  • 38
  • 72
  • I am not sure, but [this](http://orizens.com/wp/topics/angular-2-attribute-directive-creating-an-infinite-scroll-directive/) may be helpful – ulubeyn Nov 17 '16 at 16:50

3 Answers3

7

If the event you want to listen to is a native DOM event that bubbles, then you can just use @HostListener()

@HostListener('click', ['$event'])
handleClick(event) {
  // handle event
}

If they are outputs of child components, you can query them and subscribe to their outputs

@ContentChildren(ChildComponent) children:QueryList<ChildComponent>;

ngAfterContentInit() {
  this.children.toArray().forEach((item) => {
    item.someOutput.subscribe(...);
  });
}

or you can use the approach used in the answer you linked to in your question.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Thanks for your answer. `HostListener` is not bulletproof enough, as many events will not propagate due to `stopPropagation`. I was hoping for something that would allow me to bind directly to a child element. – Maxime Dupré Nov 21 '16 at 15:40
  • That's what `@ContentChildren()` does or as also suggested in the other answer, a shared service. – Günter Zöchbauer Nov 21 '16 at 15:43
  • Those two techniques would not work, as I'm not necessarily listen to events that have been initiated by components. I'm also (mostly) looking to listen to DOM events that were initiated by "normal" elements. – Maxime Dupré Nov 21 '16 at 15:56
  • You can use `@ContentChildren(ChildComponent, {read: ElementRef}) children:QueryList;` and add event listeners like explained in http://stackoverflow.com/a/36116838/217408 – Günter Zöchbauer Nov 21 '16 at 15:58
  • Ah that's cool! But correct me if I'm wrong, `ContentChildren` only returns direct children. The events I want to listen to don't necessarily come from direct children. And I can't use `querySelector` for web workers. – Maxime Dupré Nov 21 '16 at 16:11
  • If you want to utilize web workers, direct DOM access (`querySelector`) will cause issues. `ContentChildren` by default queries for children and descendants. You can pass `{descendants: false}` to only get direct children. – Günter Zöchbauer Nov 21 '16 at 16:15
  • That's the thing though, I need to listen events on children that might be descendants and I also need it to be web worker safe. – Maxime Dupré Nov 21 '16 at 16:22
1

You might consider to make your components communicate with each other via the service-class.

// service class
class Service {
  private someEventSource = new Subject();
  public someEvent = this.someEventSource.asObservable();

  public invokeAnEvent (data: string) {
    this.someEventSource.next(data);
  }
}

// parentComponent class
Service.someEvent.subscribe(data => {
  console.log(data);
});

// childComponent class
Service.invokeAnEvent('some data to pass');

From Angular documentation:
https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service

Konstantin Vitkovsky
  • 1,198
  • 11
  • 15
0

my solution to this problem is to add in the root component ngAfterContentChecked and there to select all my relevant elements and add the event. example:

  ngAfterContentChecked() {
    //console.log('MASTER ngAfterContentChecked')
    let rgx = /^[0-9,\.]+$/;
    let preventNonNumber = e => {
      //let v = (e.target as HTMLInputElement).value
      if (
        // Allow: Delete, Backspace, Tab, Escape, Enter
        [46, 8, 9, 27, 13].indexOf(e.keyCode) !== -1 || 
        (e.keyCode === 65 && e.ctrlKey === true) || // Allow: Ctrl+A
        (e.keyCode === 67 && e.ctrlKey === true) || // Allow: Ctrl+C
        (e.keyCode === 86 && e.ctrlKey === true) || // Allow: Ctrl+V
        (e.keyCode === 88 && e.ctrlKey === true) || // Allow: Ctrl+X
        (e.keyCode === 65 && e.metaKey === true) || // Cmd+A (Mac)
        (e.keyCode === 67 && e.metaKey === true) || // Cmd+C (Mac)
        (e.keyCode === 86 && e.metaKey === true) || // Cmd+V (Mac)
        (e.keyCode === 88 && e.metaKey === true) || // Cmd+X (Mac)
        (e.keyCode >= 35 && e.keyCode <= 39) // Home, End, Left, Right
      ) {
        return;  // let it happen, don't do anything
      }

      if (rgx.test(e['key']) == false) {
        e.preventDefault();
      }
    };
    let inputs = document.querySelectorAll('label-input[numberOnly] input, cbx-label-input[numberOnly] input')
    //console.log(inputs)
    inputs.forEach(i => (i as HTMLInputElement).onkeydown = preventNonNumber)
  }

NOTICE that the ngAfterContentChecked gets fired everytime you touch an input, so the selector runs everytime again, and also setting of the function, but since its a static function that your just override there isn't so much of a performance problem.

bresleveloper
  • 5,940
  • 3
  • 33
  • 47