EventEmitter Is An RxJS Observable Stream In Angular 2 Beta 6
Yesterday, as I was looking through the Angular 2 Beta 6 source code, I noticed that the EventEmitter class - which powers component and directive outputs - extends the RxJS Subject class. Subject then, in turn, extends both the RxJS Observer and Observable classes. This means that the EventEmitter class is essentially an RxJS observable stream. Which, of course, means that we should be able to operate on it and subscribe to it just like we would any other RxJS stream. And, since I am still learning RxJS streams, I figured this would be a good subject (no pun intended) for a quick demo.
Run this demo in my JavaScript Demos project on GitHub.
Honestly, I couldn't really think of a good use-case for this off the top of my head. So, I came up with something that is a little contrived, but I think demonstrates the possibilities. In the following code, I'm using a host binding to pipe local event coordinates into the .next() method of an EventEmitter instance. I'm then transforming and subscribing to the EventEmitter such that I can translate the mouse coordinates into CSS style properties for an element within the view.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
EventEmitter Is An RxJS Observable Stream In Angular 2 Beta 6
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
EventEmitter Is An RxJS Observable Stream In Angular 2 Beta 6
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/angular2-all.umd.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/almond.js"></script>
<script type="text/javascript">
// Defer bootstrapping until all of the components have been declared.
// --
// NOTE: Not all components have to be required here since they will be
// implicitly required by other components.
requirejs(
[ /* Using require() for better readability. */ ],
function run() {
var App = require( "App" );
ng.platform.browser.bootstrap( App );
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide the root App component.
define(
"App",
function registerApp() {
// Configure the App component definition.
ng.core
.Component({
selector: "my-app",
// Instead of invoking a callback in our host binding, we're
// actually just piping the local event coordinates directly
// into the mouse stream (which we are subscribing to within
// the component controller).
host: {
"(mousemove)": "mouseStream.next({ x: $event.pageX, y: $event.pageY });"
},
template:
`
<div
class="dot"
[style.left.px]="position.left"
[style.top.px]="position.top">
</div>
`
})
.Class({
constructor: AppController
})
;
return( AppController );
// I control the App component.
function AppController() {
var vm = this;
// I hold the position of the dot within the bounds of the component.
vm.position = {
left: -50,
top: -50
};
// As the user moves the mouse, we are going to pipe those (x,y)
// coordinates into this EventEmitter instance.
// --
// NOTE: This is populated with .next() values by the host-binding
// on the app component host element.
vm.mouseStream = new ng.core.EventEmitter();
// Since EventEmitter inherits from RxJS.Subject, which in turn
// inherits from RxJS.Observable, it means that the EventEmitter, in
// Angular 2, is actually an Observable sequence. Which, of course,
// means that we can operate on it and subscribe to it. In this
// stream, we're going to transform the raw mouse coordinates into
// coordinates that can be used to update the view.
// --
// NOTE: This stream is far more complicated than it needs to be - I
// am simply seizing this as an opportunity to experiment with stream
// mechanics and operators.
vm.mouseStream
// Offset the coordinates away from the mouse.
.map(
function offsetFromCursor( coordinate ) {
return({
x: ( coordinate.x - 25 ),
y: ( coordinate.y - 20 )
});
}
)
// Snap the coordinates to a 25x25 grid system.
.map(
function snapToGrid( coordinate ) {
var gridSize = 25;
return({
x: ( coordinate.x - ( coordinate.x % gridSize ) ),
y: ( coordinate.y - ( coordinate.y % gridSize ) )
});
}
)
// Don't pass on the next coordinate value until it has moved
// to another box in the grid system (this one really isn't
// necessary, I'm just experimenting with streams).
.distinctUntilChanged(
function comparator( a, b ) {
return( ( a.x === b.x ) && ( a.y === b.y ) );
}
)
// Log out the value that is being passed onto the subscriber.
// This does not affect the outcome of the value chain.
.do(
function asideNext( value ) {
console.log( "Passing through (%s,%s).", value.x, value.y );
}
)
// Here, we are actually subscribing to the stream of
// coordinates which we are using to update the position of the
// Dot in the view.
// --
// CAUTION: If this were a real-world scenario, we'd have to
// capture this subscription and then unsubscribe to it in the
// ngOnDestroy() life-cycle event handler.
.subscribe(
function handleNext( coordinate ) {
vm.position = {
left: coordinate.x,
top: coordinate.y
};
}
)
;
}
}
);
</script>
</body>
</html>
As you can see, I'm taking the EventEmitter instance and (by way of chaining) I'm calling the following methods on it:
- .map()
- .map()
- .distinctUntilChanged()
- .do()
- .subscribe() -> Returns a subscription which we are ignoring.
Basically, I'm treating it like it's an RxJS observable stream because, well, it is. And, when we run this code and move the mouse around, we get the following output:
The EventEmitter class powers the directive output bindings. So, I have no doubt that this "EventEmitter as RxJS stream" concept is actually powering a lot of functionality behind the scenes, below the surface of the framework. But, now that we know that it can be leveraged explicitly, it will be fun to see where else event-powered streams can make sense in our Angular 2 applications.
Want to use code from this post? Check out the license.
Reader Comments
It would be interesting to see how performant this type of app is when the application is run entirely in a webworker. See https://github.com/angular/angular/blob/master/modules/angular2/docs/web_workers/web_workers.md
Like how would you prevent default events etc
I have a feeling that webworker apps might become an anti-pattern for Angular2 apps, unless it is easy enough to balance where initial event processing takes place.
In any case given that apps can live on webworkers, it makes sense for events to be observables.
@Steve,
That's a super interesting topic. To be honest, I don't really have my head wrapped around the idea of running the application in different environments, including the Server and Web Workers. I see that Angular 2 is doing a lot to keep the environment-specific properties encapsulated though things like the ElementRef and the Renderer. But, even in my little bit of experimentation so far, it seems near impossible to NOT depend on the browser whether its for the window.location or something else that doesn't seem to be readily available as an encapsulated area.
So, I have no idea :P
Do NOT count on EventEmitter continuing to be an Observable!
Do NOT count on those Observable operators being there in the future!
These will be deprecated soon and probably removed before release.
Use EventEmitter only for event binding between a child and parent component. Do not subscribe to it. Do not call any of those methods. Only call `eve.emit()`
@Ward,
Ah, very good to know! I had no idea.
@All,
For what it's worth, I took Ward's advice to heart and revisited the idea of logging in order to experiment with using RxJS Subject in lieu of the EventEmitter:
www.bennadel.com/blog/3045-ward-bell-do-not-expect-eventemitter-to-be-observable-in-angular-2.htm
The demo is overly complex - but I wanted something slightly non-trivial as a test harness.
Now that Angular 2 is released, what is the official stance on using EventEmitter as an Observable?
@Brian,
If you look at the Angular source code, the EventEmitter still appears to extend Subject:
export class EventEmitter<T> extends Subject<T> { ... }
But, I haven't really thought much about it. I've been using Subjects directly whenever I need to create a Subject and have been using EventEmitters only for component "outputs". Basically, I've just gone with what Ward said.