Événements mondiaux en angulaire

224

N'y a-t-il pas d'équivalent $scope.emit()ou $scope.broadcast()en angulaire?

Je connais la EventEmitterfonctionnalité, mais pour autant que je sache , cela ne fera qu'émettre un événement vers l'élément HTML parent.

Et si j'ai besoin de communiquer entre fx. frères et sœurs ou entre un composant à la racine du DOM et un élément imbriqué plusieurs niveaux en profondeur?

skovmand
la source
2
J'avais une question similaire liée à la création d'un composant de dialogue accessible à partir de n'importe quel point du dom: stackoverflow.com/questions/34572539/… Fondamentalement, une solution consiste à mettre un émetteur d'événements dans un service
brando
1
Voici mon implémentation d'un tel service en utilisant RXJS qui permet d'obtenir les nième dernières valeurs lors de la souscription. stackoverflow.com/questions/46027693/…
Codewarrior

Réponses:

385

Il n'y a pas d'équivalent à $scope.emit()ou $scope.broadcast()de AngularJS. EventEmitter à l'intérieur d'un composant se rapproche, mais comme vous l'avez mentionné, il n'émettra qu'un événement au composant parent immédiat.

Dans Angular, il existe d'autres alternatives que je vais essayer d'expliquer ci-dessous.

Les liaisons @Input () permettent au modèle d'application d'être connecté dans un graphe d'objets dirigé (de la racine aux feuilles). Le comportement par défaut de la stratégie de détection des modifications d'un composant consiste à propager toutes les modifications apportées à un modèle d'application pour toutes les liaisons à partir de n'importe quel composant connecté.

À part: Il existe deux types de modèles: Afficher les modèles et Modèles d'application. Un modèle d'application est connecté via des liaisons @Input (). Un modèle de vue est juste une propriété de composant (non décorée avec @Input ()) qui est liée dans le modèle du composant.

Pour répondre à tes questions:

Que faire si j'ai besoin de communiquer entre des composants frères?

  1. Modèle d'application partagé : les frères et sœurs peuvent communiquer via un modèle d'application partagé (tout comme angular 1). Par exemple, lorsqu'un frère modifie un modèle, l'autre frère qui a des liaisons avec le même modèle est automatiquement mis à jour.

  2. Événements de composant : les composants enfants peuvent émettre un événement vers le composant parent à l'aide des liaisons @Output (). Le composant parent peut gérer l'événement et manipuler le modèle d'application ou son propre modèle de vue. Les modifications apportées au modèle d'application sont automatiquement propagées à tous les composants qui se lient directement ou indirectement au même modèle.

  3. Événements de service : les composants peuvent s'abonner aux événements de service. Par exemple, deux composants frères peuvent s'abonner au même événement de service et répondre en modifiant leurs modèles respectifs. Plus d'informations ci-dessous.

Comment puis-je communiquer entre un composant racine et un composant imbriqué à plusieurs niveaux?

  1. Modèle d'application partagé : le modèle d'application peut être transmis du composant racine aux sous-composants profondément imbriqués via les liaisons @Input (). Les modifications apportées à un modèle à partir de n'importe quel composant se propagent automatiquement à tous les composants qui partagent le même modèle.
  2. Événements de service : vous pouvez également déplacer EventEmitter vers un service partagé, ce qui permet à n'importe quel composant d'injecter le service et de s'abonner à l'événement. De cette façon, un composant racine peut appeler une méthode de service (généralement une mutation du modèle), qui à son tour émet un événement. Plusieurs couches vers le bas, un composant petit-enfant qui a également injecté le service et s'est abonné au même événement, peut le gérer. Tout gestionnaire d'événements qui modifie un modèle d'application partagé se propage automatiquement à tous les composants qui en dépendent. C'est probablement l'équivalent le plus proche $scope.broadcast()de Angular 1. La section suivante décrit cette idée plus en détail.

Exemple d'un service observable qui utilise des événements de service pour propager les modifications

Voici un exemple de service observable qui utilise des événements de service pour propager les modifications. Lorsqu'un TodoItem est ajouté, le service émet un événement notifiant ses abonnés aux composants.

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

Voici comment un composant racine souscrirait à l'événement:

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

Un composant enfant imbriqué à plusieurs niveaux souscrirait à l'événement de la même manière:

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

Voici le composant qui appelle le service pour déclencher l'événement (il peut résider n'importe où dans l'arborescence des composants):

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

Référence: Change Detection in Angular

pixelbits
la source
27
Je l' ai vu le $ arrière dans quelques postes maintenant une observable ou EventEmitter - par exemple itemAdded$. Est-ce une convention RxJS ou quelque chose? D'où est-ce que ça vient?
Mark Rajcok
1
Bonne réponse. Vous avez déclaré: "Les modifications apportées au modèle d'application sont automatiquement propagées à tous les composants qui se lient directement ou indirectement au même modèle." J'ai le pressentiment que cela ne fonctionne pas tout à fait de cette façon (mais je ne suis pas sûr). L'autre article de blog de Savkin donne un exemple d'un composant modifiant la streetpropriété du modèle d'application, mais comme Angular 2 implémente la détection des modifications par identité / référence, aucune modification n'est propagée ( onChangesn'est pas appelée), car la référence du modèle d'application n'a pas changé ( cont ...)
Mark Rajcok
10
Vous souhaiterez peut-être mettre à jour votre réponse pour utiliser un Observable au lieu d'un EventEmitter dans le service. Voir stackoverflow.com/a/35568924/215945 et stackoverflow.com/questions/36076700
Mark Rajcok
2
Oui, le suffixe $ est une convention RxJS popularisée par Cycle.js. cycle.js.org/…
jody tate
4
Vous ne devez pas vous abonner manuellement à un émetteur d'événements. Ce n'est peut-être pas observable dans la version finale! Voir ceci: bennadel.com/blog/…
NetProvoke
49

Le code suivant comme exemple de remplacement de $ scope.emit () ou $ scope.broadcast () dans Angular 2 à l'aide d'un service partagé pour gérer les événements.

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

    off(name, listener) {
        this.listeners[name] = this.listeners[name].filter(x => x != listener);
    }

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

Exemple d'utilisation:

Diffuser:

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

Auditeur:

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

Il peut prendre en charge plusieurs 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);
});
jim.taylor.1974
la source
Qu'est-ce que cela fait? static get parameters () {return [new Inject (EventsService)]; }
Beanwah
Dans cet exemple, j'utilise Ionic 2 Framework. La méthode des paramètres statiques est appelée lorsque la méthode constructeur est invoquée et est utilisée pour fournir les dépendances au constructeur. Explication ici stackoverflow.com/questions/35919593/…
jim.taylor.1974
1
Bien fait. Simple et fournit un système de notification facilement adaptable pour l'ensemble de l'application, pas seulement une seule.
Mike M du
Je viens de créer un service similaire avec le support des caractères génériques. J'espère que ça aide. github.com/govorov/ng-radio
Stanislav E. Govorov
2
Génial, l'a utilisé mais a ajouté une fonction off si plus est intéressé: off(name, listener) { this.listeners[name] = this.listeners[name].filter(x => x != listener); }
LVDM
16

J'utilise un service de messagerie qui encapsule un rxjs Subject(TypeScript)

Exemple Plunker: 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);
  }
}

Les composants peuvent s'abonner et diffuser des événements (expéditeur):

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

(destinataire)

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

La subscribeméthode de MessageServiceretourne un Subscriptionobjet rxjs , qui peut être désabonné comme ceci:

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

Voir également cette réponse: https://stackoverflow.com/a/36782616/1861779

Exemple Plunker: Message Service

t.888
la source
2
très précieux. Merci d'avoir répondu. Je viens de découvrir que vous ne pouvez pas communiquer avec deux composants dans deux modules différents en utilisant cette méthode. Pour atteindre l'objectif, j'ai dû enregistrer MessageService au niveau app.module en y ajoutant des fournisseurs. Quoi qu'il en soit, c'est vraiment cool.
Rukshan Dangalla
tout cela est terriblement obsolète. en particulier le plongeur qui ne charge aucune ressource avec succès. ce sont tous 500 codes d'erreur.
tatsu
Je reçoisProperty 'filter' does not exist on type 'Subject<EventMessage>'.
Drew
@Drew, sur les nouvelles versions de RxJS this.handler.pipe(filter(...)). Voir les opérateurs louables .
t.888
1
@ t.888 merci, je l'ai compris. La fonction de souscription mise à jour ressemble àreturn this.handler.pipe( filter(message => message.type === type), map(message => message.payload) ).subscribe(callback);
Drew
12

N'UTILISEZ PAS EventEmitter pour votre communication de service.

Vous devez utiliser l'un des types observables. J'aime personnellement BehaviorSubject.

Exemple simple:

Vous pouvez passer l'état initial, ici je passe nul

let subject = new BehaviorSubject (null);

Lorsque vous souhaitez mettre à jour le sujet

subject.next (myObject)

Observez depuis n'importe quel service ou composant et agissez lorsqu'il reçoit de nouvelles mises à jour.

subject.subscribe (this.YOURMETHOD);

Voici plus d'informations. .

Danial Kalbasi
la source
1
pourriez-vous nous expliquer les raisons de cette décision de conception?
mtraut
@mtraut ce lien a également une explication complète.
Danial Kalbasi
pour des explications plus détaillées sur l'utilisation de BehaviourSubject, veuillez lire cet article blog.cloudboost.io/…
rafalkasa
C'est exactement ce dont j'avais besoin. Agréable et simple :)
Low
2

Ma façon préférée de faire est d'utiliser le sujet du comportement ou l'émetteur d'événement (presque le même) dans mon service pour contrôler tous mes sous-composants.

À l'aide de angular cli, exécutez ng gs pour créer un nouveau service, puis utilisez un BehaviorSubject ou 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);
   }
}

Lorsque vous effectuez cette opération, chaque composant utilisant votre service en tant que fournisseur sera informé du changement. Abonnez-vous simplement au résultat comme vous le faites avec 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
  }
}
Andrea Martines
la source
1

J'ai créé un échantillon pub-sub ici:

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

L'idée est d'utiliser des sujets RxJ pour câbler un observateur et des observables comme solution générique pour émettre et s'abonner à des événements personnalisés. Dans mon exemple, j'utilise un objet client à des fins de démonstration

this.pubSubService.Stream.emit(customer);

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

Voici également une démonstration en direct: http://www.syntaxsuccess.com/angular-2-samples/#/demo/pub-sub

TGH
la source
1

Voici ma 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});
    }
}

utilisation:

export class <YOURCOMPONENT> implements IEventListener{

  constructor(private eventManager: EventManagerService) {


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


  }

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

}

émettre:

 this.eventManager.emit("EVENT_NAME");
zakrzu
la source
0

Nous avons implémenté une directive observable ngModelChange qui envoie toutes les modifications de modèle via un émetteur d'événements que vous instanciez dans votre propre composant. Vous devez simplement lier votre émetteur d'événements à la directive.

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

En html, liez votre émetteur d'événement (countryChanged dans cet exemple):

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

Dans votre composant tapuscrit, effectuez quelques opérations asynchrones sur 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)
                })
            })
    }
}
Peter Rigole
la source
0

Événements de service: les composants peuvent s'abonner aux événements de service. Par exemple, deux composants frères peuvent s'abonner au même événement de service et répondre en modifiant leurs modèles respectifs. Plus d'informations ci-dessous.

Mais assurez-vous de vous désabonner à la destruction du composant parent.

BVS Bharat Kumar
la source