Я использую 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).