Глобальные события в Angular


224

Нет ли эквивалента $scope.emit()или$scope.broadcast() в Angular?

Я знаю EventEmitterфункциональность, но насколько я понимаю, это будет просто отправлять событие родительскому элементу HTML.

Что делать, если мне нужно общаться между FX. братья или сестры или между компонентом в корне DOM и элементом, вложенным в несколько уровней?


2
У меня был похожий вопрос, связанный с созданием компонента диалога, к которому можно было бы получить доступ из любой точки домена: stackoverflow.com/questions/34572539/… По сути, одно из решений состоит в том, чтобы поместить источник событий в сервис
brando

1
Вот моя реализация такого сервиса, использующего RXJS, который позволяет получить n-е последние значения при подписке. stackoverflow.com/questions/46027693/…
Codewarrior

Ответы:


385

Нет эквивалента $scope.emit()или$scope.broadcast() AngularJS от него. EventEmitter внутри компонента подходит близко, но, как вы упомянули, он отправляет событие только непосредственному родительскому компоненту.

В Angular есть другие альтернативы, которые я постараюсь объяснить ниже.

Привязки @Input () позволяют связать модель приложения в графе ориентированных объектов (от корня до листьев). Поведение по умолчанию стратегии детектора изменений компонента заключается в распространении всех изменений в модели приложения для всех привязок из любого подключенного компонента.

Помимо: есть два типа моделей: модели просмотра и прикладные модели. Модель приложения связана через привязки @Input (). Модель представления - это просто свойство компонента (не декорированное @Input ()), которое связано в шаблоне компонента.

Чтобы ответить на ваши вопросы:

Что делать, если мне нужно общаться между родственными компонентами?

  1. Модель общего приложения : братья и сестры могут общаться через модель общего приложения (как в угловом 1). Например, когда один из братьев или сестер вносит изменения в модель, другой брат, имеющий привязки к той же модели, автоматически обновляется.

  2. События компонента . Дочерние компоненты могут отправлять событие родительскому компоненту, используя привязки @Output (). Родительский компонент может обрабатывать событие и манипулировать моделью приложения или его собственной моделью представления. Изменения в модели приложения автоматически распространяются на все компоненты, которые прямо или косвенно связаны с одной и той же моделью.

  3. Сервисные события : Компоненты могут подписаться на сервисные события. Например, два дочерних компонента могут подписаться на одно и то же сервисное событие и ответить, изменив свои соответствующие модели. Подробнее об этом ниже.

Как я могу общаться между корневым компонентом и компонентом, вложенным в несколько уровней?

  1. Модель общего приложения : модель приложения может быть передана из корневого компонента в глубоко вложенные подкомпоненты через привязки @Input (). Изменения в модели из любого компонента будут автоматически распространяться на все компоненты, которые используют одну и ту же модель.
  2. События службы : вы также можете переместить EventEmitter в общую службу, которая позволяет любому компоненту внедрить службу и подписаться на событие. Таким образом, корневой компонент может вызывать метод службы (обычно изменяющий модель), который, в свою очередь, генерирует событие. Несколько уровней вниз, дочерний компонент, который также внедрил службу и подписался на то же событие, может обработать его. Любой обработчик событий, который изменяет общую модель приложения, будет автоматически распространяться на все компоненты, которые зависят от нее. Это, вероятно, самый близкий эквивалент $scope.broadcast()из Angular 1. Следующий раздел описывает эту идею более подробно.

Пример наблюдаемой службы, которая использует события службы для распространения изменений

Вот пример наблюдаемой службы, которая использует события службы для распространения изменений. Когда добавляется TodoItem, служба генерирует событие, уведомляющее подписчиков компонента.

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

Вот как корневой компонент будет подписываться на событие:

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

Дочерний компонент, вложенный в несколько уровней, подписался бы на событие таким же образом:

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

Вот компонент, который вызывает сервис для запуска события (он может находиться в любом месте дерева компонентов):

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

Ссылка: Обнаружение изменений в угловых


27
Я видел косой $ в нескольких постах теперь для наблюдаемого или EventEmitter - например, itemAdded$. Это соглашение RxJS или что-то? Откуда это?
Марк Райкок

1
Хороший ответ. Вы заявили: «Изменения в модели приложения автоматически распространяются на все компоненты, которые прямо или косвенно связаны с одной и той же моделью». У меня есть догадка, что это не совсем так (но я не уверен). В другом сообщении Савкина в блоге приведен пример того, как компонент изменяет streetсвойство модели приложения, но поскольку Angular 2 реализует обнаружение изменений по идентификатору / ссылке, изменения не распространяются ( onChangesне вызывается), поскольку ссылка на модель приложения не изменилась ( продолжение ...)
Марк Райкок

10
Возможно, вы захотите обновить свой ответ, чтобы использовать Observable вместо EventEmitter в службе. См. Stackoverflow.com/a/35568924/215945 и stackoverflow.com/questions/36076700
Марк Райкок,

2
Да, суффикс $ - это соглашение RxJS, популяризированное Cycle.js. cycle.js.org/…
Джоди Тейт

4
Вы не должны вручную подписываться на Eventemitter. Это не может быть заметным в финальной версии! Смотрите это: bennadel.com/blog/...
NetProvoke

49

Следующий код в качестве примера замены $ scope.emit () или $ scope.broadcast () в Angular 2 с использованием общего сервиса для обработки событий.

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

Пример использования:

Broadcast:

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

Слушатель:

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

Он может поддерживать несколько аргументов:

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

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

Что это делает? статические параметры получения () {return [new Inject (EventsService)]; }
Beanwah

В этом примере я использую Ionic 2 Framework. Метод статических параметров вызывается при вызове метода конструктора и используется для предоставления зависимостей конструктору. Объяснение здесь stackoverflow.com/questions/35919593/…
jim.taylor.1974

1
Красиво сделано. Простая и легко адаптируемая система уведомлений для всего приложения, а не только для одноразового использования.
Майк М

Я только что создал подобный сервис с поддержкой подстановочных знаков. Надеюсь, поможет. github.com/govorov/ng-radio
Говоров Станислав Евгеньевич

2
Круто, использовал его, но добавил функцию отключения, если больше заинтересован: off(name, listener) { this.listeners[name] = this.listeners[name].filter(x => x != listener); }
LVDM

16

Я использую службу сообщений, которая упаковывает rxjs Subject (TypeScript)

Пример Plunker: служба сообщений

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

Компоненты могут подписываться и транслировать события (отправитель):

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

(приемник)

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

subscribeМетод MessageServiceвозвращает rxjs Subscriptionобъект, который может быть отписался от примерно так:

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

Также см. Этот ответ: https://stackoverflow.com/a/36782616/1861779

Пример Plunker: служба сообщений


2
Очень ценно. Спасибо за ответ. Я только что узнал, что таким способом нельзя общаться с двумя компонентами в двух разных модулях . Для достижения цели мне пришлось зарегистрировать MessageService на уровне app.module, добавив туда провайдеров. В любом случае это действительно крутой способ.
Рукшан Дангалла

все это ужасно устарело. особенно плункер, который не загружает ресурсы успешно. они все 500 кодов ошибок.
Tatsu

Я получаюProperty 'filter' does not exist on type 'Subject<EventMessage>'.
Дрю

@Drew, на более новых версиях RxJS this.handler.pipe(filter(...)). Смотрите операторы lettable .
t.888

1
@ t.888 спасибо, разобрался. Обновленная функция подписки выглядит такreturn this.handler.pipe( filter(message => message.type === type), map(message => message.payload) ).subscribe(callback);
Дрю

12

НЕ используйте EventEmitter для служебного общения.

Вы должны использовать один из наблюдаемых типов. Мне лично нравится BehaviorSubject.

Простой пример:

Вы можете пройти начальное состояние, здесь я пропустил нуль

let subject = new BehaviorSubject (null);

Когда вы хотите обновить тему

subject.next (MyObject)

Наблюдайте от любого сервиса или компонента и действуйте, когда он получает новые обновления.

subject.subscribe (this.YOURMETHOD);

Здесь больше информации. ,


1
не могли бы вы уточнить причины такого дизайнерского решения?
mtraut

@mtraut эта ссылка также имеет исчерпывающее объяснение.
Даниал Калбаси

для более подробного объяснения, как использовать BehaviourSubject, пожалуйста, прочитайте эту статью blog.cloudboost.io/…
rafalkasa

Точно то, что мне было нужно.
Низкий

8

Вы можете использовать EventEmitter или observables для создания службы Eventbus, которую вы регистрируете в DI. Каждый компонент, который хочет участвовать, просто запрашивает службу в качестве параметра конструктора и отправляет и / или подписывается на события.

Смотрите также


2

Мой любимый способ сделать это - использовать объект поведения или источник событий (почти такой же) в моем сервисе для контроля всего моего подкомпонента.

Используя angular cli, запустите ng gs, чтобы создать новый сервис, затем используйте BehaviorSubject или 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);
   }
}

Когда вы сделаете это, каждый компонент, использующий вашу службу в качестве поставщика, будет знать об этом изменении. Просто подпишитесь на результат, как вы делаете с 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

Я создал образец pub-sub здесь:

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

Идея состоит в том, чтобы использовать объекты RxJs для подключения Observer и Observables в качестве общего решения для создания и подписки на пользовательские события. В моем примере я использую объект клиента для демонстрационных целей

this.pubSubService.Stream.emit(customer);

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

Вот также живая демонстрация: http://www.syntaxsuccess.com/angular-2-samples/#/demo/pub-sub


1

Это моя версия:

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

использовать:

export class <YOURCOMPONENT> implements IEventListener{

  constructor(private eventManager: EventManagerService) {


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


  }

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

}

испускают:

 this.eventManager.emit("EVENT_NAME");

0

Мы реализовали наблюдаемую директиву ngModelChange, которая отправляет все изменения модели через генератор событий, который вы создаете в своем собственном компоненте. Вы просто должны привязать ваш источник событий к директиве.

Видеть: https://github.com/atomicbits/angular2-modelchangeobservable

В html свяжите ваш источник событий (countryChanged в этом примере):

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

В вашем компоненте машинописи выполните некоторые асинхронные операции над 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

Сервисные события: Компоненты могут подписаться на сервисные события. Например, два дочерних компонента могут подписаться на одно и то же сервисное событие и ответить, изменив свои соответствующие модели. Подробнее об этом ниже.

Но обязательно отмените подписку на это при уничтожении родительского компонента.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.