199

Is there no equivalent to $scope.emit() or $scope.broadcast() in Angular?

I know the EventEmitter functionality, but as far as I understand that will just emit an event to the parent HTML element.

What if I need to communicate between fx. siblings or between a component in the root of the DOM and an element nested several levels deep?


10 답변


353

There is no equivalent to $scope.emit() or $scope.broadcast() from AngularJS. EventEmitter inside of a component comes close, but as you mentioned, it will only emit an event to the immediate parent component.

In Angular, there are other alternatives which I'll try to explain below.

@Input() bindings allows the application model to be connected in a directed object graph (root to leaves). The default behavior of a component's change detector strategy is to propagate all changes to an application model for all bindings from any connected component.

Aside: There are two types of models: View Models and Application Models. An application model is connected through @Input() bindings. A view model is a just a component property (not decorated with @Input()) which is bound in the component's template.

To answer your questions:

What if I need to communicate between sibling components?

  1. Shared Application Model: Siblings can communicate through a shared application model (just like angular 1). For example, when one sibling makes a change to a model, the other sibling that has bindings to the same model is automatically updated.

  2. Component Events: Child components can emit an event to the parent component using @Output() bindings. The parent component can handle the event, and manipulate the application model or it's own view model. Changes to the Application Model are automatically propagated to all components that directly or indirectly bind to the same model.

  3. Service Events: Components can subscribe to service events. For example, two sibling components can subscribe to the same service event and respond by modifying their respective models. More on this below.

How can I communicate between a Root component and a component nested several levels deep?

  1. Shared Application Model: The application model can be passed from the Root component down to deeply nested sub-components through @Input() bindings. Changes to a model from any component will automatically propagate to all components that share the same model.
  2. Service Events: You can also move the EventEmitter to a shared service, which allows any component to inject the service and subscribe to the event. That way, a Root component can call a service method (typically mutating the model), which in turn emits an event. Several layers down, a grand-child component which has also injected the service and subscribed to the same event, can handle it. Any event handler that changes a shared Application Model, will automatically propagate to all components that depend on it. This is probably the closest equivalent to $scope.broadcast() from Angular 1. The next section describes this idea in more detail.

Example of an Observable Service that uses Service Events to Propagate Changes

Here is an example of an observable service that uses service events to propagate changes. When a TodoItem is added, the service emits an event notifying its component subscribers.

export class TodoItem {
    constructor(public name: string, public done: boolean) {
    }
}
export class TodoService {
    public itemAdded$: EventEmitter<TodoItem>;
    private todoList: TodoItem[] = [];

    constructor() {
        this.itemAdded$ = new EventEmitter();
    }

    public list(): TodoItem[] {
        return this.todoList;
    }

    public add(item: TodoItem): void {
        this.todoList.push(item);
        this.itemAdded$.emit(item);
    }
}

Here is how a root component would subscribe to the event:

export class RootComponent {
    private addedItem: TodoItem;
    constructor(todoService: TodoService) {
        todoService.itemAdded$.subscribe(item => this.onItemAdded(item));
    }

    private onItemAdded(item: TodoItem): void {
        // do something with added item
        this.addedItem = item;
    }
}

A child component nested several levels deep would subscribe to the event in the same way:

export class GrandChildComponent {
    private addedItem: TodoItem;
    constructor(todoService: TodoService) {
        todoService.itemAdded$.subscribe(item => this.onItemAdded(item));
    }

    private onItemAdded(item: TodoItem): void {
        // do something with added item
        this.addedItem = item;
    }
}

Here is the component that calls the service to trigger the event (it can reside anywhere in the component tree):

@Component({
    selector: 'todo-list',
    template: `
         <ul>
            <li *ngFor="#item of model"> {{ item.name }}
            </li>
         </ul>
        <br />
        Add Item <input type="text" #txt /> <button (click)="add(txt.value); txt.value='';">Add</button>
    `
})
export class TriggeringComponent{
    private model: TodoItem[];

    constructor(private todoService: TodoService) {
        this.model = todoService.list();
    }

    add(value: string) {
        this.todoService.add(new TodoItem(value, false));
    }
}

Reference: Change Detection in Angular


  • I've seen the trailing $ in a few posts now for an observable or EventEmitter -- e.g., itemAdded$. Is that an RxJS convention or something? Where does this come from? - Mark Rajcok
  • Nice answer. You stated, "Changes to the App Model are automatically propagated to all components that directly or indirectly bind to the same model." I have a hunch that it doesn't quite work this way (but I'm not sure). Savkin's other blog post gives an example of a component changing the street property of the app model, but since Angular 2 implements change detection by identity/reference, no changes are propagated (onChanges is not called), because the app model reference hasn't changed (cont...) - Mark Rajcok
  • I agree with your comments as well. I'm sure these extended comments are helpful to others:) - pixelbits
  • You might want to update your answer to use an Observable instead of an EventEmitter in the service. See stackoverflow.com/a/35568924/215945 and stackoverflow.com/questions/36076700 - Mark Rajcok
  • You should no manually subscribe to an eventemitter. It might not be an observable in the final release! See this: bennadel.com/blog/… - NetProvoke

44

The following code as an example of a replacement for $scope.emit() or $scope.broadcast() in Angular 2 using a shared service to handle events.

import {Injectable} from 'angular2/core';
import * as Rx from 'rxjs/Rx';

@Injectable()
export class EventsService {
    constructor() {
        this.listeners = {};
        this.eventsSubject = new Rx.Subject();

        this.events = Rx.Observable.from(this.eventsSubject);

        this.events.subscribe(
            ({name, args}) => {
                if (this.listeners[name]) {
                    for (let listener of this.listeners[name]) {
                        listener(...args);
                    }
                }
            });
    }

    on(name, listener) {
        if (!this.listeners[name]) {
            this.listeners[name] = [];
        }

        this.listeners[name].push(listener);
    }

    broadcast(name, ...args) {
        this.eventsSubject.next({
            name,
            args
        });
    }
}

Example usage:

Broadcast:

function handleHttpError(error) {
    this.eventsService.broadcast('http-error', error);
    return ( Rx.Observable.throw(error) );
}

Listener:

import {Inject, Injectable} from "angular2/core";
import {EventsService}      from './events.service';

@Injectable()
export class HttpErrorHandler {
    constructor(eventsService) {
        this.eventsService = eventsService;
    }

    static get parameters() {
        return [new Inject(EventsService)];
    }

    init() {
        this.eventsService.on('http-error', function(error) {
            console.group("HttpErrorHandler");
            console.log(error.status, "status code detected.");
            console.dir(error);
            console.groupEnd();
        });
    }
}

It can support multiple arguments:

this.eventsService.broadcast('something', "Am I a?", "Should be b", "C?");

this.eventsService.on('something', function (a, b, c) {
   console.log(a, b, c);
});


  • What does this do? static get parameters() { return [new Inject(EventsService)]; } - Beanwah
  • In this example I am using Ionic 2 Framework. The static parameters method is called when the constructor method is invoked and is used to supply the dependencies to the constructor. Explanation here stackoverflow.com/questions/35919593/… - jim.taylor.1974
  • Nicely done. Simple and provides an easily adaptable notification system for the entire app, not just a one-off. - Mike M
  • I just created similar service with wildcards support. Hope it helps. github.com/govorov/ng-radio - Stanislav E. Govorov
  • Awesome, used it but added an off function if anymore is interested: off(name, listener) { this.listeners[name] = this.listeners[name].filter(x => x != listener); } - LVDM

15

I'm using a message service that wraps an rxjs Subject (TypeScript)

Plunker example: Message Service

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/filter'
import 'rxjs/add/operator/map'

interface Message {
  type: string;
  payload: any;
}

type MessageCallback = (payload: any) => void;

@Injectable()
export class MessageService {
  private handler = new Subject<Message>();

  broadcast(type: string, payload: any) {
    this.handler.next({ type, payload });
  }

  subscribe(type: string, callback: MessageCallback): Subscription {
    return this.handler
      .filter(message => message.type === type)
      .map(message => message.payload)
      .subscribe(callback);
  }
}

Components can subscribe and broadcast events (sender):

import { Component, OnDestroy } from '@angular/core'
import { MessageService } from './message.service'
import { Subscription } from 'rxjs/Subscription'

@Component({
  selector: 'sender',
  template: ...
})
export class SenderComponent implements OnDestroy {
  private subscription: Subscription;
  private messages = [];
  private messageNum = 0;
  private name = 'sender'

  constructor(private messageService: MessageService) {
    this.subscription = messageService.subscribe(this.name, (payload) => {
      this.messages.push(payload);
    });
  }

  send() {
    let payload = {
      text: `Message ${++this.messageNum}`,
      respondEvent: this.name
    }
    this.messageService.broadcast('receiver', payload);
  }

  clear() {
    this.messages = [];
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

(receiver)

import { Component, OnDestroy } from '@angular/core'
import { MessageService } from './message.service'
import { Subscription } from 'rxjs/Subscription'

@Component({
  selector: 'receiver',
  template: ...
})
export class ReceiverComponent implements OnDestroy {
  private subscription: Subscription;
  private messages = [];

  constructor(private messageService: MessageService) {
    this.subscription = messageService.subscribe('receiver', (payload) => {
      this.messages.push(payload);
    });
  }

  send(message: {text: string, respondEvent: string}) {
    this.messageService.broadcast(message.respondEvent, message.text);
  }

  clear() {
    this.messages = [];
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

The subscribe method of MessageService returns an rxjs Subscription object, which can be unsubscribed from like so:

import { Subscription } from 'rxjs/Subscription';
...
export class SomeListener {
  subscription: Subscription;

  constructor(private messageService: MessageService) {
    this.subscription = messageService.subscribe('someMessage', (payload) => {
      console.log(payload);
      this.subscription.unsubscribe();
    });
  }
}

Also see this answer: https://stackoverflow.com/a/36782616/1861779

Plunker example: Message Service


  • very valuable. Thanks for the answer. I just found out you can't communicate with two components in two different modules using this way. To achieve the goal I had to register MessageService in the app.module level by adding providers there. Any way this is a really cool way. - Rukshan Dangalla

11

DO Not Use EventEmitter for your service communication.

You should use one of the Observable types. I personally like BehaviorSubject.

Simple example:

You can pass initial state, here I passing null

let subject = new BehaviorSubject(null);

When you want to update the subject

subject.next(myObject)

Observe from any service or component and act when it gets new updates.

subject.subscribe(this.YOURMETHOD);

Here is more information..



9

You can use EventEmitter or observables to create an eventbus service that you register with DI. Every component that wants to participate just requests the service as constructor parameter and emits and/or subscribes to events.

See also



2

I have created a pub-sub sample here:

http://www.syntaxsuccess.com/viewarticle/pub-sub-in-angular-2.0

The idea is to use RxJs Subjects to wire up an Observer and and Observables as a generic solution for emitting and subscribing to custom events. In my sample I use a customer object for demo purposes

this.pubSubService.Stream.emit(customer);

this.pubSubService.Stream.subscribe(customer => this.processCustomer(customer));

Here is a live demo as well: http://www.syntaxsuccess.com/angular-2-samples/#/demo/pub-sub


2

My favorite way to do is by using behavior subject or event emitter (almost the same) in my service to control all my subcomponent.

Using angular cli, run ng g s to create a new service then use a BehaviorSubject or EventEmitter

export Class myService {
#all the stuff that must exist

myString: string[] = [];
contactChange : BehaviorSubject<string[]> = new BehaviorSubject(this.myString);

   getContacts(newContacts) {
     // get your data from a webservices & when you done simply next the value 
    this.contactChange.next(newContacts);
   }
}

When you do that every component using your service as a provider will be aware of the change. Simply subscribe to the result like you do with eventEmitter ;)

export Class myComp {
#all the stuff that exists like @Component + constructor using (private myService: myService)

this.myService.contactChange.subscribe((contacts) => {
     this.contactList += contacts; //run everytime next is called
  }
}


1

We implemented a ngModelChange observable directive that sends all model changes through an event emitter that you instantiate in your own component. You simply have to bind your event emitter to the directive.

See: https://github.com/atomicbits/angular2-modelchangeobservable

In html, bind your event emitter (countryChanged in this example):

<input [(ngModel)]="country.name"
       [modelChangeObservable]="countryChanged" 
       placeholder="Country"
       name="country" id="country"></input>

In your typescript component, do some async operations on the EventEmitter:

import ...
import {ModelChangeObservable} from './model-change-observable.directive'


@Component({
    selector: 'my-component',
    directives: [ModelChangeObservable],
    providers: [],
    templateUrl: 'my-component.html'
})

export class MyComponent {

    @Input()
    country: Country

    selectedCountries:Country[]
    countries:Country[] = <Country[]>[]
    countryChanged:EventEmitter<string> = new EventEmitter<string>()


    constructor() {

        this.countryChanged
            .filter((text:string) => text.length > 2)
            .debounceTime(300)
            .subscribe((countryName:string) => {
                let query = new RegExp(countryName, 'ig')
                this.selectedCountries = this.countries.filter((country:Country) => {
                    return query.test(country.name)
                })
            })
    }
}


1

This is my version:

export interface IEventListenr extends OnDestroy{
    ngOnDestroy(): void
}

@Injectable()
export class EventManagerService {


    private listeners = {};
    private subject = new EventEmitter();
    private eventObserver = this.subject.asObservable();


    constructor() {

        this.eventObserver.subscribe(({name,args})=>{



             if(this.listeners[name])
             {
                 for(let listener of this.listeners[name])
                 {
                     listener.callback(args);
                 }
             }
        })

    }

    public registerEvent(eventName:string,eventListener:IEventListenr,callback:any)
    {

        if(!this.listeners[eventName])
             this.listeners[eventName] = [];

         let eventExist = false;
         for(let listener of this.listeners[eventName])
         {

             if(listener.eventListener.constructor.name==eventListener.constructor.name)
             {
                 eventExist = true;
                 break;
             }
         }

        if(!eventExist)
        {
             this.listeners[eventName].push({eventListener,callback});
        }
    }

    public unregisterEvent(eventName:string,eventListener:IEventListenr)
    {

        if(this.listeners[eventName])
        {
            for(let i = 0; i<this.listeners[eventName].length;i++)
            {

                if(this.listeners[eventName][i].eventListener.constructor.name==eventListener.constructor.name)
                {
                    this.listeners[eventName].splice(i, 1);
                    break;
                }
            }
        }


    }


    emit(name:string,...args:any[])
    {
        this.subject.next({name,args});
    }
}

use:

export class <YOURCOMPONENT> implements IEventListener{

  constructor(private eventManager: EventManagerService) {


    this.eventManager.registerEvent('EVENT_NAME',this,(args:any)=>{
       ....
    })


  }

  ngOnDestroy(): void {
    this.eventManager.unregisterEvent('closeModal',this)
  }

}

emit:

 this.eventManager.emit("EVENT_NAME");


0

Service Events: Components can subscribe to service events. For example, two sibling components can subscribe to the same service event and respond by modifying their respective models. More on this below.

But make sure to unsubscribe to that on destroy of the parent component.

Linked


Latest