Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Jeff McDowell and Jonathan Dowdle and Joel Hill and Josh Siok and Christian Ready and Steve 'Cutter' Blades and Matt Vickers
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Jeff McDowell@jeff_s_mcdowell ) , Jonathan Dowdle@jdowdle ) , Joel Hill@Jiggidyuo ) , Josh Siok@siok ) , Christian Ready@christianready ) , Steve 'Cutter' Blades@cutterbl ) , and Matt Vickers@envex )

Directive Output Bindings Use "$event" For Dependency-Injection In AngularJS 2 Beta 1

By Ben Nadel on

In AngularJS, when we think about "dependency injection", we often think about providing services to component controllers and other services. But, component template expressions also use a dependency-injection, of sorts, allowing invocation arguments to be pulled out of the component properties, the template-local variables, and the invocation-local variables. When binding to a DOM (Document Object Model) event or a directive output event, AngularJS 2 uses "$event" as the token for the emitted value in the event-binding's dependency-injection context.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

In AngularJS 1.x, when you set up an isolate-scope attribute expression that emitted a value, you had to evaluate that expression using a "locals" object that would map output values to dependency-injection tokens in the calling context. In AngularJS 2 Beta 1, the same mechanics are in play; however, we no longer have to provide a "locals" object when emitting an output value. This is because AngularJS 2 automatically maps the emitted value to the "$event" token in the calling context.

This somewhat reduces the flexibility of directive outputs, in that they can only emit a single value. But, this also means that we now have a very consistent way to emit and bind to output values. And, it makes the vast-majority of use-cases - single-value emission - much easier to reason about and consume.

Because AngularJS is using the $event token as the emitted value's dependency-injection token, it means that your event-handlers can use $event to provide the emitted value at invocation time. And, you can do so in conjunction with any number of other invocation arguments.

To demonstrate this, I've put together a small demo that sets up three bindings to a directive's output event, "beep." In each binding, I'm using a different ordered-set of arguments that the invoker pulls out of the dependency-injection context. Notice that the "$event" token, in each binding, is in a different location in that ordered-set.

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Directive Output Bindings Use "$event" For Dependency-Injection In AngularJS 2 Beta 1
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></lin>
  • </head>
  • <body>
  •  
  • <h1>
  • Directive Output Bindings Use "$event" For Dependency-Injection In AngularJS 2 Beta 1
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/1/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(
  • [ "AppComponent" ],
  • function run( AppComponent ) {
  •  
  • ng.platform.browser.bootstrap( AppComponent );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root application component.
  • define(
  • "AppComponent",
  • [ "Widget" ],
  • function registerAppComponent( Widget ) {
  •  
  • // Configure the App component definition.
  • var AppComponent = ng.core
  • .Component({
  • selector: "my-app",
  • directives: [ Widget ],
  •  
  • // When we define this template, we'll create three instances of
  • // our Widget component, each which has a (beep) binding that is
  • // making use of three different values:
  • // --
  • // * A component value.
  • // * A template value.
  • // * An event value.
  • // --
  • // Each binding, however, identifies these values with a
  • // different argument order in order to demonstrate that the
  • // expression evaluation is using dependency-injection in which
  • // "$event" token represents the event "locals" value.
  • template:
  • `
  • <p #p title="Template local value.">
  • The following widgets produce a custom event.
  • </p>
  •  
  • <widget (beep)="logBeep( $event, message, p.title )">
  •  
  • logBeep( <strong>$event</strong>, message, p.title )
  •  
  • </widget>
  •  
  • <widget (beep)="logBeep( message, $event, p.title )">
  •  
  • logBeep( message, <strong>$event</strong>, p.title )
  •  
  • </widget>
  •  
  • <widget (beep)="logBeep( message, p.title, $event )">
  •  
  • logBeep( message, p.title, <strong>$event</strong> )
  •  
  • </widget>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppComponent );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // I am the component property that is being referenced in the
  • // template as part of the (beep) event bindings.
  • vm.message = "Component property value.";
  •  
  • // Expose the public methods.
  • vm.logBeep = logBeep;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I log the beep event to the console, logging-out the event-
  • // handler arguments in the same order in which they were provided
  • // by the method invoker / dependency-injector.
  • function logBeep( a, b, c ) {
  •  
  • console.log( "- - - - - - - - - - - - - - -" );
  • console.log( "1:", a, decorateLog( a ) );
  • console.log( "2:", b, decorateLog( b ) );
  • console.log( "3:", c, decorateLog( c ) );
  • console.log( "- - - - - - - - - - - - - - -" );
  •  
  •  
  • // I add a logging message that helps identify which event-
  • // handler argument is the one from the EventEmitter (ie, the
  • // output value).
  • function decorateLog( value ) {
  •  
  • if ( value.indexOf( "EventEmitter" ) === 0 ) {
  •  
  • return( "<-- $event value!" );
  •  
  • } else {
  •  
  • return( "" );
  •  
  • }
  •  
  • }
  •  
  • };
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a component that produces a "beep" event (output) when clicked.
  • define(
  • "Widget",
  • function registerWidget() {
  •  
  • // Configure the Widget component definition.
  • var WidgetComponent = ng.core
  • .Component({
  • selector: "widget",
  • outputs: [ "beep" ],
  • host: {
  • "(click)": "triggerBeep()"
  • },
  • template:
  • `
  • <ng-content></ng-content>
  • `
  • })
  • .Class({
  • constructor: WidgetController
  • })
  • ;
  •  
  • return( WidgetComponent );
  •  
  •  
  • // I control the Widget component.
  • function WidgetController() {
  •  
  • var vm = this;
  •  
  • // I am the output event stream for the "beep" event. The event
  • // stream has to be a public property so that the calling context
  • // can bind to it.
  • vm.beep = new ng.core.EventEmitter();
  •  
  • // Expose the public methods.
  • vm.triggerBeep = triggerBeep;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I trigger the beep event on the output stream.
  • function triggerBeep() {
  •  
  • // When we emit() a value, the value is made available in the
  • // template binding's dependency-injection as the token "$event".
  • vm.beep.emit( "EventEmitter output value." );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, I have three different output bindings, each of which provides $event in a different argument slot:

  • logBeep( $event, message, p.title )
  • logBeep( message, $event, p.title )
  • logBeep( message, p.title, $event )

When you first start dealing with directive output event streams, you may think that the $event always has to be the first argument; or maybe even that $event has to be the only argument. But, when we run the above code and click on each of the widgets (triggering each of the output events), we get the following output:


 
 
 

 
 Directive output bindings use $event as the token for dependency-injection in AngularJS 2 Beta 1. 
 
 
 

As you can see, the use of the "$event" token properly mapped the directive's output event value to the appropriate event-handler argument.

It's possible that you've never thought about this. And, this may seem like a really small, insignificant feature. But the fact that template expressions use dependency-injection for method invocation is really powerful and actually simplifies the logic required to render a view. If you've ever looked at ReactJS, you'll see that everything has to be a React class; or, that you have use .bind() all over the place to associate iteration values with event handlers. This is because React doesn't evaluate template expressions for you - you have to manage all the wiring explicitly. With AngularJS, the number of templates and the number of components will always be smaller thanks to the magic of dependency-injection.




Reader Comments

You will know by now that such events don't bubble - you need a more generic "message bus" to communicate between arbitrary components.

Reply to this Comment

> template expressions use dependency-injection for method invocation
Good way of looking at $event.

> directive outputs, in that they can only emit a single value.
Custom events follow the same model as DOM events. Is this limiting?

The difference:
DOM events bubble all the way. Custom events only go up one level to the parent. But, the Angular team has left the door open to bubble the Custom events all the way up too if there are any good use-cases for it.

Reply to this Comment

@Esfand,

As far as only emitting a single value, I only intended to show that it was different than AngularJS 1.x. In 1.x, you could emit an event like:

scope.onSelect({ item: selectedItem, index: selectedIndex })

... which would make both "item" and "index" available in the "locals" object used to invoke the callback method. In AngularJS 2.x, though, this would simply be a single object, $event, and you could still use $event.item and $event.index, if you wanted to.

So, no I don't think it's a limit. In fact, forcing people to use "$event" actually makes the mental model simpler; so, I think it is a benefit.

>> Angular team has left the door open to bubble the Custom events all the way up

... can you explain that a bit more? I was not aware this was an option.

Reply to this Comment

@Fergus,

Good call on the message but. In AngularJS 1.x, I used to use $rootScope as the message bus :D But, no more scopes :( But, it looks like the Rx Observables can make this easy (thought I haven't experimented yet).

Reply to this Comment

@Fergus,
Thanks for the sample. Yes. That is the right (and perhaps the only) approach to implement a custom event.

Reply to this Comment

Right now, I've only used the EventEmitter for the component "outputs". But, it looks like it based on the whole RxJS library that comes with Angular 2. Hopefully once I get a change to dig into that, I'll be able to see the possibilities.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.