Как создать наблюдаемый поток RxJS из события дочернего элемента шаблона компонента Angular2

Я использую Angular 2.0.0-rc.4 с RxJS 5.0.0-beta.6.

Я экспериментирую с различными методами создания наблюдаемых потоков из событий, но я поражен вариантами и хочу опросить мнение. Я ценю, что нет универсального решения, и есть лошади для курсов. Есть, вероятно, другие методы, о которых я не знаю или не рассматривал.

В Angular двухкомпонентная кулинарная книга по взаимодействию предоставляет несколько методов родительского компонента, взаимодействующего с событиями дочерних компонентов. Тем не менее, только пример родительский и дочерний обмениваются данными через службу использует наблюдаемые данные, и для большинства сценариев это кажется излишним.

Сценарий заключается в том, что элемент шаблона выделяет большой объем событий, и я хочу знать, периодически, что самое последнее значение.

Я использую метод Observable sampleTime с периодом 1000 мс для отслеживания местоположения мыши в элементе <p> HTML.

1) Этот метод использует ElementRef, введенный в конструктор компонента, для доступа к свойству nativeElement и запроса дочерних элементов по имени тега.

@Component({
  selector: 'watch-child-events',
  template: `
    <p>Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];

  constructor(private el:ElementRef) {}

  ngOnInit() {
    let p = this.el.nativeElement.getElementsByTagName('p')[0];
    Observable
      .fromEvent(p, 'mousemove')
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}

Консенсусное мнение, похоже, не одобряет этот метод, потому что Angular2 обеспечивает достаточную абстракцию над DOM, так что вам редко приходится когда-либо взаимодействовать непосредственно с ним. Но метод fromEvent factory Observable делает его довольно соблазнительным, и это был первый метод, который пришел на ум.

2) В этом методе используется EventEmitter, который является Observable.

@Component({
  selector: 'watch-child-events',
  template: `
    <p (mousemove)="handle($event)">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  emitter:EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>();

  ngOnInit() {
    this.emitter
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.emitter.emit(e);
  }
}

Это решение избегает запросов к DOM, но излучатели событий используются для связи от дочернего к родительскому, и это событие не для вывода.

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

3) Этот метод использует наблюдаемый Subject.

@Component({
  selector: 'watch-child-events',
  template: `
    <p (mousemove)="handle($event)">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  subject = new Subject<MouseEvent>();

  ngOnInit() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}

Это решение помечает все ящики, на мой взгляд, без сложностей. Я мог бы использовать ReplaySubject, чтобы получить всю историю опубликованных значений, когда я подписываюсь на нее, или только самый последний, если он существует, вместо subject = new ReplaySubject<MouseEvent>(1);.

4) Этот метод использует ссылку на шаблон в сочетании с декоратором @ViewChild.

@Component({
  selector: 'watch-child-events',
  template: `
    <p #p">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements AfterViewInit {
  messages:string[] = [];
  @ViewChild('p') p:ElementRef;

  ngAfterViewInit() {
    Observable
      .fromEvent(this.p.nativeElement, 'mousemove')
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}

Пока он работает, он немного пахнет мне. Ссылки на шаблоны в основном предназначены для взаимодействия компонентов внутри шаблона. Он также касается DOM через nativeElement, использует строки для ссылки на имя события и ссылку на шаблон, и использует крючок жизненного цикла AfterViewInit.

5) Я расширил этот пример, используя пользовательский компонент, который управляет Subject и периодически отправляет событие.

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  @Output() event = new EventEmitter<MouseEvent>();
  subject = new Subject<MouseEvent>();

  constructor() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.event.emit(e);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}

Он используется родителем следующим образом:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="handle($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent {
  messages:string[] = [];

  handle(e:MouseEvent) {
    this.messages.push(`${e.type} (${e.x}, ${e.y})`);
  }
}

Мне нравится эта техника; пользовательский компонент инкапсулирует желаемое поведение и облегчает использование родителем, но он только связывает дерево компонентов и не может уведомлять своих братьев и сестер.

6) Контрастируйте это с помощью этой техники, которая просто пересылает событие от дочернего к родительскому.

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  @Output() event = new EventEmitter<MouseEvent>();

  handle(e:MouseEvent) {
    this.event.emit(e);
  }
}

И подключается родителем с помощью декоратора @ViewChild либо так:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer>
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent implements AfterViewInit {
  messages:string[] = [];
  @ViewChild(ChildEventProducerComponent) child:ChildEventProducerComponent;

  ngAfterViewInit() {
    Observable
      .from(this.child.event)
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}

7) Или вот так:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="handle($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  subject = new Subject<MouseEvent>();

  ngOnInit() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}

используя наблюдаемый Subject, который идентичен предыдущему методу.

8) Наконец, если вам нужно передавать уведомления по всему дереву компонентов, то, по-видимому, это путь к совместному использованию.

@Injectable()
export class LocationService {
  private source = new ReplaySubject<{x:number;y:number;}>(1);

  stream:Observable<{x:number;y:number;}> = this.source
    .asObservable()
    .sampleTime(1000);

  moveTo(location:{x:number;y:number;}) {
    this.source.next(location);
  }
}

Поведение инкапсулируется в службу. Все, что требуется в дочернем компоненте, это LocationService, введенный в конструктор, и вызов moveTo в обработчике событий.

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  constructor(private svc:LocationService) {}

  handle(e:MouseEvent) {
    this.svc.moveTo({x: e.x, y: e.y});
  }
}

Ввести службу на уровне дерева компонентов, из которого необходимо передать.

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer>
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent],
  providers: [LocationService]
})
export class WatchChildEventsComponent implements OnInit, OnDestroy {
  messages:string[] = [];
  subscription:Subscription;

  constructor(private svc:LocationService) {}

  ngOnInit() {
    this.subscription = this.svc.stream
      .subscribe((e:{x:number;y:number;}) => {
        this.messages.push(`(${e.x}, ${e.y})`);
      });
  }

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

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

В заключение я бы использовал объект Subject внутри компонента, если нет необходимости в межкомпонентной связи (3). Если мне нужно было передать дерево компонентов, я бы инкапсулировал Subject в дочерний компонент и применил операторы потока в компоненте (5). В противном случае, если бы мне нужна была максимальная гибкость, я бы использовал сервис для переноса потока (8).

Ответ 1

В методе 6 вы можете использовать привязку событий (прокрутите вниз до "Пользовательские события с помощью EventEmitter" ) вместо @ViewChild и ngAfterViewInit, что упрощает многое:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="onEvent($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent {
  messages:string[] = [];
  onEvent(e) { this.messages.push(`${e.type} (${e.x}, ${e.y})`); }
}