Currently incorporating Angular 2.0.0-rc.4 alongside RxJS 5.0.0-beta.6.
In the midst of exploring various methods for generating observable streams from events, I find myself inundated with choices and would like to gather opinions. Recognizing that there is no universal solution and different techniques suit different scenarios, there may be other methods I have yet to discover or consider.
The interaction cookbook in Angular 2 offers multiple approaches for a parent component to interact with events in a child component. Among these, only the example demonstrating how parents and children communicate via a service utilizes observables, which may appear excessive for many cases.
The specific situation involves an element in the template emitting numerous events, prompting the need to periodically ascertain the most recent value.
I employ the sampleTime
method of Observable
set at a 1000ms interval to monitor mouse movements on an HTML <p>
element.
1) This method entails using the ElementRef
provided through the constructor to access the nativeElement
property and query 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})`);
});
}
}
This approach tends to draw criticism as Angular 2 generally provides adequate abstraction over the DOM, reducing the necessity of direct interactions. Nevertheless, the fromEvent
factory method of Observable
remains alluring and easily accessible, often being the first technique considered.
2) Involves utilizing an EventEmitter
, essentially 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);
}
}
While avoiding direct DOM querying, using event emitters primarily facilitates communication from child to parent and such events aren’t necessarily designed for output purposes.
Evidence suggests that assuming event emitters will function as observables in the final release is precarious, hinting at potential instability in relying on this feature.
3) This strategy employs 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 particular method appears well-rounded, striking a balance between effectiveness and simplicity. It affords the use of a ReplaySubject
to retrieve either a history of published values upon subscription or just the latest one if available.
4) Involves using a template reference combined 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})`);
});
}
}
Although functional, this method raises some concerns. Template references are mainly intended for component interactions within a template, while it also necessitates direct DOM access through nativeElement
, string-based event and template reference naming conventions, and relies on the AfterViewInit
lifecycle hook.
5) An extension involving a custom component managing a Subject
and periodically emitting an event.
@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);
}
}
Integration within the parent component would resemble:
@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})`);
}
}
This methodology presents a user-friendly approach with the custom component encapsulating the desired behavior, though communications are restricted solely within the component hierarchy without horizontal notifications.
6) Contrasted with a straightforward forwarding of events 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);
}
}
Implemented in the parent using the @ViewChild
decorator either as:
@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 as:
@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);
}
}
employing an observable Subject
, mirroring the previously discussed technique.
8) Lastly, in scenarios requiring broadcast notifications throughout the component tree, employing a shared service is deemed advantageous.
@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);
}
}
This design encapsulates the functionality within the service. The child component simply needs to inject the LocationService
in the constructor and invoke moveTo
within 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 required level within the component hierarchy for broadcasting purposes.
@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();
}
}
Remember to unsubscribe when done. While offering flexibility, this solution introduces added complexity.
To summarize, I opt for an internal Subject within the component if inter-component communication isn't necessary (3). When communication up the component tree is required, I prefer encapsulating a Subject in the child component and applying stream operators within the component itself (5). For maximum flexibility across the entire component tree, utilizing a service to wrap a stream proves most effective (8).