I am using Angular 2.0.0-rc.4 with RxJS 5.0.0-beta.6.
I am experimenting with the different methods of creating observable streams from events, but am overwhelmed by the options and want to canvass opinion. I appreciate there is no one-size-fits-all solution and there are horses for courses. There are probably other techniques I don't know about or haven't considered.
The Angular 2 component interaction cookbook provides several methods of a parent component interacting with a child component's events. However, only the example of parent and children communicate via a service uses observables and it seems like overkill for most scenarios.
The scenario is that a template's element emits a high volume of events and I want to know, periodically, what the most recent value is.
I use Observable
's sampleTime
method with a period of 1000ms to monitor the mouse location on a <p>
HTML element.
1) This technique uses the ElementRef
injected into the component's constructor to access the nativeElement
property and query the child elements by tag name.
@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})`);
});
}
}
Consensus opinion seems to frown on this technique because Angular2 provides a sufficient abstraction over the DOM so that you rarely, if ever, need to interact directly with it. But that fromEvent
factory method of Observable
makes it rather tempting to use and it was the first technique that came to mind.
2) This technique uses an EventEmitter
, which is an 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);
}
}
This solution avoids querying the DOM, but event emitters are used to communicate from child to parent, and this event is not for output.
I read here that it shouldn't be assumed that event emitters will be observables in the final release, so this may not be a stable feature to rely on.
3) This technique uses an observable 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);
}
}
This solution ticks all the boxes in my opinion without adding much complexity. I could use a ReplaySubject
to receive an entire history of published values when I subscribe to it, or just the most recent one, if it exists, with subject = new ReplaySubject<MouseEvent>(1);
instead.
4) This technique uses a template reference in conjunction with the @ViewChild
decorator.
@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})`);
});
}
}
While it works, it smells a bit to me. Template references are primarily for component interaction within a template. It also touches the DOM through the nativeElement
, uses strings to refer to the event name and the template reference, and uses the AfterViewInit
lifecycle hook.
5) I extended the example to use a custom component which manages a Subject
and emits an event periodically.
@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);
}
}
It is used by the parent like this:
@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})`);
}
}
I like this technique; the custom component encapsulates the desired behaviour and makes it easy for the parent to use, but it only communicates up the component tree and can't notify siblings.
6) Contrast that with this technique which simply forwards the event from child to parent.
@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);
}
}
And is wired up in the parent using the @ViewChild
decorator either like this:
@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) Or like this:
@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);
}
}
using an observable Subject
, which is identical to the earlier technique.
8) Finally, if you need to broadcast notifications across the component tree, a shared service seems to be the way to go.
@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);
}
}
The behaviour is encapsulated in the service. All that is required in the child component is the LocationService
injected in the constructor and a call to moveTo
in the event handler.
@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});
}
}
Inject the service at the level of the component tree you need to broadcast from.
@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();
}
}
Not forgetting to unsubscribe when finished with. This solution offers a lot of flexibility at the expense of some complexity.
In conclusion, I would use a Subject internal to the component if there is no need for inter-component communication (3). If I needed to communicate up the component tree I would encapsulate a Subject in the child component and apply the stream operators within the component (5). Otherwise, if I needed maximum flexibility, I would use a service to wrap a stream (8).
In method 6 you can use event binding (scroll down to "Custom Events with EventEmitter") instead of @ViewChild
and ngAfterViewInit
, which simplifies a lot:
@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})`); }
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With