Child component events broadcast to parent
Asked Answered
L

3

5

I'd like to implement the common Angular 1.x pattern of having child directives within a parent directive in Angular 2. Here's my desired structure.

<foo>
  <bar>A</bar>
  <bar>B</bar>
  <bar>C</bar>
</foo>

I'd like for these Bar components to have click events that get emitted to the Foo component.

Here's my Foo so far:

@Component({
  selector: 'foo',
  template: `
    <div>
      <ng-content></ng-content>
    </div>
  `
})
export class Foo {
   @ContentChildren(Bar) items: QueryList<Bar>;
}

And here's my Bar:

@Component({
  selector: 'Bar',
  template: `
    <div (click)="clickity()">
      <ng-content></ng-content>
    </div>
  `
})
export class Bar {
  clickity() {
    console.log('Broadcast this to the parent please!');
  }
}

How do I go about notifying Foo whenever one of its Bars is clicked?

Lauretta answered 15/1, 2016 at 0:13 Comment(4)
You can use EventEmitter methods to .emit() in Bar, and .subscribe() in Foo Component. Either as @Output() or via service, like in this examplePalmary
@Palmary can you clarify? I know how to use @Output() from child to parent but only if I can actually place the attribute on the child component. I don't know how to do it dynamically with user-defined children.Lauretta
tbh, I didn't use @Output() that much... I prefer the service, I import it in both classes, ask for same instance of emitter and register emit/subscribe where I need, I'll try to create example in a min...Palmary
Take a look at this example. Doesn't matter how components are created, just import service in their files...Palmary
I
5

The other answer does a very poor job of solving the problem. EventEmitters are only meant to be used in conjunction with @Outputs as well as this problem not taking advantage of the dependency injection built into Angular 2 or the features of RxJS.

Specifically, by not using DI, you're forcing yourself into a scenario where if you reuse the components dependent on the static class they will all receive the same events, which you probably don't want.

Please take a look at the example below, by taking advantage of DI, it is easy to provide the same class multiple times, enabling more flexible use, as well as avoiding the need for funny naming schemes. If you want multiple events, you could provide multiple versions of this simple class using opaque tokens.

Working Example: http://plnkr.co/edit/RBfa1GKeUdHtmzjFRBLm?p=preview

// The service
import 'rxjs/Rx';
import {Subject,Subscription} from 'rxjs/Rx';

export class EmitterService {
  private events = new Subject();
  subscribe (next,error,complete): Subscriber {
    return this.events.subscribe(next,error,complete);
  }
  next (event) {
    this.events.next(event);
  }
}

@Component({
  selector: 'bar',
  template: `
    <button (click)="clickity()">click me</button>
  `
})
export class Bar {
  constructor(private emitter: EmitterService) {}
  clickity() {
    this.emitter.next('Broadcast this to the parent please!');
  }
}

@Component({
  selector: 'foo',
  template: `
    <div [ngStyle]="styl">
      <ng-content></ng-content>
    </div>
  `,
  providers: [EmitterService],
  directives: [Bar]
})
export class Foo {
  styl = {};
  private subscription;
  constructor(private emitter: EmitterService) {
    this.subscription = this.emitter.subscribe(msg => {
      this.styl = (this.styl.background == 'green') ? {'background': 'orange'} : {'background': 'green'};
    });
  }
  // Makes sure we don't have a memory leak by destroying the
  // Subscription when our component is destroyed
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}
Incorporate answered 2/5, 2016 at 20:13 Comment(1)
See also angular.io/docs/ts/latest/cookbook/…Lifesaver
P
14

You can use a service to send data between components if you can't do it using @Output() decorator. Here's an example:

import {EventEmitter} from 'angular2/core';

export class EmitterService {
  private static _emitters: { [channel: string]: EventEmitter<any> } = {};
  static get(channel: string): EventEmitter<any> {
    if (!this._emitters[channel]) 
      this._emitters[channel] = new EventEmitter();
    return this._emitters[channel];
  }
}

You import it wherever you need to emit or subscribe to an event:

// foo.component.ts
import {EmitterService} from '../path/to/emitter.service'

class Foo {
  EmitterService.get("some_id").subscribe(data => console.log("some_id channel: ", data));
  EmitterService.get("other_id").subscribe(data => console.log("other_id channel: ", data));
}

// bar.component.ts
import {EmitterService} from '../path/to/emitter.service'

class Bar {

  onClick() {
    EmitterService.get("some_id").emit('you clicked!');
  }
  onScroll() {
    EmitterService.get("other_id").emit('you scrolled!');
  }
}

another example: plunker

Palmary answered 15/1, 2016 at 8:33 Comment(0)
G
8

Why not using @ContentChildern?

in bar.component.ts we expose clicked event

@Output() clicked = new EventEmitter<BarComponent>();
onClick(){
    this.clicked.emit(this);
}

in foo.component.ts we subscribe to the clicked event of each

 @ContentChildren(BarComponent) accordionComponents: QueryList<BarComponent>;

 ngAfterViewInit() {
 this.accordionComponents.forEach((barComponent: BarComponent) => {
        barComponent.clicked.subscribe((bar: BarComponent) => doActionsOnBar(bar));           
    });
}
Gasiform answered 5/6, 2018 at 12:28 Comment(0)
I
5

The other answer does a very poor job of solving the problem. EventEmitters are only meant to be used in conjunction with @Outputs as well as this problem not taking advantage of the dependency injection built into Angular 2 or the features of RxJS.

Specifically, by not using DI, you're forcing yourself into a scenario where if you reuse the components dependent on the static class they will all receive the same events, which you probably don't want.

Please take a look at the example below, by taking advantage of DI, it is easy to provide the same class multiple times, enabling more flexible use, as well as avoiding the need for funny naming schemes. If you want multiple events, you could provide multiple versions of this simple class using opaque tokens.

Working Example: http://plnkr.co/edit/RBfa1GKeUdHtmzjFRBLm?p=preview

// The service
import 'rxjs/Rx';
import {Subject,Subscription} from 'rxjs/Rx';

export class EmitterService {
  private events = new Subject();
  subscribe (next,error,complete): Subscriber {
    return this.events.subscribe(next,error,complete);
  }
  next (event) {
    this.events.next(event);
  }
}

@Component({
  selector: 'bar',
  template: `
    <button (click)="clickity()">click me</button>
  `
})
export class Bar {
  constructor(private emitter: EmitterService) {}
  clickity() {
    this.emitter.next('Broadcast this to the parent please!');
  }
}

@Component({
  selector: 'foo',
  template: `
    <div [ngStyle]="styl">
      <ng-content></ng-content>
    </div>
  `,
  providers: [EmitterService],
  directives: [Bar]
})
export class Foo {
  styl = {};
  private subscription;
  constructor(private emitter: EmitterService) {
    this.subscription = this.emitter.subscribe(msg => {
      this.styl = (this.styl.background == 'green') ? {'background': 'orange'} : {'background': 'green'};
    });
  }
  // Makes sure we don't have a memory leak by destroying the
  // Subscription when our component is destroyed
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}
Incorporate answered 2/5, 2016 at 20:13 Comment(1)
See also angular.io/docs/ts/latest/cookbook/…Lifesaver

© 2022 - 2024 — McMap. All rights reserved.