Eventos Globais em Angular


224

Não há equivalente a $scope.emit()ou $scope.broadcast()em Angular?

Conheço a EventEmitterfuncionalidade, mas, pelo que entendi, isso apenas emitirá um evento para o elemento HTML pai.

E se eu precisar me comunicar entre fx. irmãos ou entre um componente na raiz do DOM e um elemento aninhado em vários níveis de profundidade?


2
Eu tive uma pergunta semelhante relacionada à criação de um componente de diálogo que pode ser acessado a partir de qualquer ponto no dom: stackoverflow.com/questions/34572539/… Basicamente, uma solução é colocar um emissor de evento em um serviço
brando

1
Aqui está minha implementação de um serviço desse tipo usando o RXJS que permite obter o enésimo último valor após a assinatura. stackoverflow.com/questions/46027693/…
Codewarrior 3/17

Respostas:


385

Não há equivalente para $scope.emit()ou $scope.broadcast()do AngularJS. O EventEmitter dentro de um componente se aproxima, mas, como você mencionou, ele emitirá apenas um evento para o componente pai imediato.

No Angular, existem outras alternativas que tentarei explicar abaixo.

As ligações @Input () permitem que o modelo de aplicativo seja conectado em um gráfico de objeto direcionado (raiz às folhas). O comportamento padrão da estratégia do detector de alterações de um componente é propagar todas as alterações em um modelo de aplicativo para todas as ligações de qualquer componente conectado.

Lado: Existem dois tipos de modelos: View Models e Application Models. Um modelo de aplicativo é conectado através de ligações @Input (). Um modelo de exibição é apenas uma propriedade de componente (não decorada com @Input ()) que é vinculada ao modelo do componente.

Para responder suas perguntas:

E se eu precisar me comunicar entre os componentes irmãos?

  1. Modelo de aplicativo compartilhado : os irmãos podem se comunicar por meio de um modelo de aplicativo compartilhado (assim como o angular 1). Por exemplo, quando um irmão faz uma alteração em um modelo, o outro irmão que tem ligações ao mesmo modelo é atualizado automaticamente.

  2. Eventos do componente : os componentes filhos podem emitir um evento para o componente pai usando as ligações @Output (). O componente pai pode manipular o evento e manipular o modelo de aplicativo ou seu próprio modelo de visualização. As alterações no modelo de aplicativo são propagadas automaticamente para todos os componentes que se ligam direta ou indiretamente ao mesmo modelo.

  3. Eventos de serviço : os componentes podem se inscrever em eventos de serviço. Por exemplo, dois componentes irmãos podem se inscrever no mesmo evento de serviço e responder modificando seus respectivos modelos. Mais sobre isso abaixo.

Como posso me comunicar entre um componente raiz e um componente aninhado em vários níveis?

  1. Modelo de Aplicativo Compartilhado : O modelo de aplicativo pode ser passado do componente Raiz para subcomponentes profundamente aninhados através de ligações @Input (). As alterações em um modelo de qualquer componente serão propagadas automaticamente para todos os componentes que compartilham o mesmo modelo.
  2. Eventos de serviço : Você também pode mover o EventEmitter para um serviço compartilhado, o que permite que qualquer componente injete o serviço e se inscreva no evento. Dessa forma, um componente Raiz pode chamar um método de serviço (normalmente modificando o modelo), que por sua vez emite um evento. Várias camadas abaixo, um componente neto que também injetou o serviço e se inscreveu no mesmo evento, pode lidar com isso. Qualquer manipulador de eventos que altere um Modelo de Aplicativo compartilhado será propagado automaticamente para todos os componentes que dependem dele. Este é provavelmente o equivalente mais próximo $scope.broadcast()do Angular 1. A próxima seção descreve essa idéia com mais detalhes.

Exemplo de um serviço observável que usa eventos de serviço para propagar alterações

Aqui está um exemplo de um serviço observável que usa eventos de serviço para propagar alterações. Quando um TodoItem é adicionado, o serviço emite um evento notificando seus assinantes de componentes.

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

Aqui está como um componente raiz se inscreveria no evento:

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

Um componente filho aninhado em vários níveis se inscreveria no evento da mesma maneira:

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

Aqui está o componente que chama o serviço para acionar o evento (ele pode residir em qualquer lugar da árvore de componentes):

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

Referência: detecção de alterações em angular


27
Eu já vi os últimos $ em algumas postagens agora para um observável ou EventEmitter - por exemplo itemAdded$,. Isso é uma convenção RxJS ou algo assim? De onde vem isto?
precisa saber é o seguinte

1
Boa resposta. Você declarou: "As alterações no modelo de aplicativo são propagadas automaticamente para todos os componentes que direta ou indiretamente se ligam ao mesmo modelo". Eu tenho um palpite de que não funciona dessa maneira (mas não tenho certeza). A outra postagem no blog de Savkin fornece um exemplo de componente que altera a streetpropriedade do modelo de aplicativo, mas como o Angular 2 implementa a detecção de alterações por identidade / referência, nenhuma alteração é propagada ( onChangesnão é chamada), porque a referência do modelo de aplicativo não foi alterada ( (...)
Mark Rajcok

10
Convém atualizar sua resposta para usar um Observable em vez de um EventEmitter no serviço. Consulte stackoverflow.com/a/35568924/215945 e stackoverflow.com/questions/36076700
Mark Rajcok 23/16

2
Sim, o $ com sufixo é uma convenção RxJS popularizada pelo Cycle.js. cycle.js.org/…
jody tate

4
Você não deve se inscrever manualmente em um emissor de eventos. Pode não ser um observável na versão final! Veja isto: bennadel.com/blog/…
NetProvoke

49

O código a seguir como exemplo de substituição de $ scope.emit () ou $ scope.broadcast () no Angular 2 usando um serviço compartilhado para manipular eventos.

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

Exemplo de uso:

Transmissão:

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

Ouvinte:

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

Ele pode suportar vários argumentos:

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

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

O que isso faz? static get parameters () {return [new Inject (EventsService)]; }
Beanwah

Neste exemplo, estou usando o Ionic 2 Framework. O método de parâmetros estáticos é chamado quando o método do construtor é chamado e é usado para fornecer as dependências ao construtor. Explicação aqui stackoverflow.com/questions/35919593/…
jim.taylor.1974

1
Bem feito. Simples e fornece um sistema de notificação facilmente adaptável para todo o aplicativo, não apenas um caso único.
Mike M

Acabei de criar um serviço semelhante com suporte a caracteres curinga. Espero que ajude. github.com/govorov/ng-radio
Stanislav E. Govorov

2
Awesome, usei, mas adicionei uma função off se mais estiver interessado: off(name, listener) { this.listeners[name] = this.listeners[name].filter(x => x != listener); }
LVDM

16

Estou usando um serviço de mensagens que envolve um rxjs Subject(TypeScript)

Exemplo de Plunker: Serviço de Mensagens

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

Os componentes podem se inscrever e transmitir eventos (remetente):

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

(receptor)

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

O subscribemétodo de MessageServiceretorna um Subscriptionobjeto rxjs , que pode ser cancelado da seguinte maneira:

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

Consulte também esta resposta: https://stackoverflow.com/a/36782616/1861779

Exemplo de Plunker: Serviço de Mensagens


2
muito valioso. Obrigado pela resposta. Acabei de descobrir que você não pode se comunicar com dois componentes em dois módulos diferentes dessa maneira. Para atingir o objetivo, tive que registrar o MessageService no nível app.module adicionando provedores lá. De qualquer forma, essa é uma maneira muito legal.
Rukshan Dangalla

tudo isso está desatualizado. especialmente o desentupidor que não carrega nenhum recurso com sucesso. são todos os 500 códigos de erro.
Tatsu

Eu receboProperty 'filter' does not exist on type 'Subject<EventMessage>'.
Drew

@Drew, nas versões mais recentes do uso do RxJS this.handler.pipe(filter(...)). Veja operadores locáveis .
t.888 18/11/19

1
@ t.888 obrigado, eu descobri. A função de inscrição atualizada parece #return this.handler.pipe( filter(message => message.type === type), map(message => message.payload) ).subscribe(callback);
Drew

12

NÃO use EventEmitter para sua comunicação de serviço.

Você deve usar um dos tipos observáveis. Eu, pessoalmente, gosto de BehaviorSubject.

Exemplo simples:

Você pode passar estado inicial, aqui eu passando nulo

deixe subject = new BehaviorSubject (null);

Quando você deseja atualizar o assunto

subject.next (myObject)

Observe de qualquer serviço ou componente e aja quando receber novas atualizações.

subject.subscribe (this.YOURMETHOD);

Aqui está mais informações. .


1
você poderia elaborar os motivos dessa decisão de design?
Mtrut 12/08

@mtraut esse link também tem uma explicação abrangente.
Danial Kalbasi

para explicação mais detalhada como usar BehaviourSubject leia este artigo blog.cloudboost.io/...
rafalkasa

Precisamente o que eu precisava. Agradável e simples :) #
1616


2

Minha maneira favorita de fazer é usar o assunto de comportamento ou emissor de evento (quase o mesmo) em meu serviço para controlar todo o meu subcomponente.

Usando cli angular, execute ng gs para criar um novo serviço e use um 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);
   }
}

Quando você faz isso, todos os componentes que usam seu serviço como provedor estarão cientes da mudança. Simplesmente assine o resultado como você faz com 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

Eu criei uma amostra de pub-sub aqui:

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

A idéia é usar os assuntos RxJs para conectar um observador e um observável como uma solução genérica para emitir e assinar eventos personalizados. Na minha amostra, eu uso um objeto de cliente para fins de demonstração

this.pubSubService.Stream.emit(customer);

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

Aqui também está uma demonstração ao vivo: http://www.syntaxsuccess.com/angular-2-samples/#/demo/pub-sub


1

Esta é a minha versão:

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

usar:

export class <YOURCOMPONENT> implements IEventListener{

  constructor(private eventManager: EventManagerService) {


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


  }

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

}

emitir:

 this.eventManager.emit("EVENT_NAME");

0

Implementamos uma diretiva observável ngModelChange que envia todas as alterações de modelo por meio de um emissor de eventos que você instancia em seu próprio componente. Você simplesmente precisa vincular seu emissor de evento à diretiva.

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

No html, vincule o emissor do evento (countryChanged neste exemplo):

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

No seu componente de texto datilografado, faça algumas operações assíncronas no 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)
                })
            })
    }
}

0

Eventos de serviço: os componentes podem se inscrever em eventos de serviço. Por exemplo, dois componentes irmãos podem se inscrever no mesmo evento de serviço e responder modificando seus respectivos modelos. Mais sobre isso abaixo.

Mas não se esqueça de cancelar a assinatura ao destruir o componente pai.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.