Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Seb Duggan
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Seb Duggan@sebduggan )

Input And Output Bindings Can Be Namespaced In Angular 2 Beta 14

By Ben Nadel on

As I was playing around with the template syntax in Angular 2 Beta 14, I stumbled upon a rather interesting behavior. It appears as though the input and output binding expressions are evaluated as object-paths on the target component. This means that by adding the property access operator - "." - you can essentially namespace the inputs and outputs for a component.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Normally, your component inputs and outputs are defined as a simple property:

  • [myTarget]="inputBinding"
  • (myTarget)="eventHandler"

But, as it turns out, it's perfectly legal to throw the property access operator in there in order to create a namespace:

  • [myNamespace.myTarget]="inputBinding"
  • (myNamespace.myTarget)="eventHandler"

What this actually does depends on how you implement your inputs and outputs in the target component. By default, this will cause the input and output values to be stored to and read from the this.myNamespace object, respectively. If, however, you alias your input or output internally, the values will be stored into and read from the alias, regardless of the namespacing you used in your calling context.

To see this in action, I've created a Friend component that accepts input bindings namespaced as "virtues" and exposes output bindings namespaced as "behaviors". There are three inputs and three outputs. For each group (inputs and outputs), two of them use the namespace internally; but, the third uses a simple alias.

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Input And Output Bindings Can Be Namespaced In Angular 2 Beta 14
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Input And Output Bindings Can Be Namespaced In Angular 2 Beta 14
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/14/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/14/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/14/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/14/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/14/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • requirejs(
  • [ /* Using require() for better readability. */ ],
  • function run() {
  •  
  • ng.platform.browser.bootstrap( require( "App" ) );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root application component.
  • define(
  • "App",
  • function registerApp() {
  •  
  • // Configure the App component definition.
  • ng.core
  • .Component({
  • selector: "my-app",
  • directives: [ require( "Friend" ) ],
  •  
  • // When we configure out input and output bindings on the Friend
  • // component, notice that they are all being namespaced on
  • // either "virtues." or "behaviors.".
  • template:
  • `
  • <my-friend
  • [virtues.honesty]="hasHonesty"
  • [virtues.compassion]="hasCompassion"
  • [virtues.kindness]="hasKindness"
  • (behaviors.laugh)="handleLaugh( $event )"
  • (behaviors.hug)="handleHug( $event )"
  • (behaviors.cry)="handleCry( $event )">
  • </my-friend>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppController );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // Setup the input binding values.
  • vm.hasHonesty = true;
  • vm.hasCompassion = false;
  • vm.hasKindness = true;
  •  
  • // Expose the public methods.
  • vm.handleCry = handleCry;
  • vm.handleHug = handleHug;
  • vm.handleLaugh = handleLaugh;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the Friend's "cry" event.
  • function handleCry( event ) {
  •  
  • console.log( "(cry):", event );
  •  
  • }
  •  
  •  
  • // I handle the Friend's "hug" event.
  • function handleHug( event ) {
  •  
  • console.log( "(hug):", event );
  •  
  • }
  •  
  •  
  • // I handle the Friend's "laugh" event.
  • function handleLaugh( event ) {
  •  
  • console.log( "(laugh):", event );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a Friend component that has namespaces inputs and outputs.
  • define(
  • "Friend",
  • function registerFriend() {
  •  
  • // Configure the Friend component definition.
  • ng.core
  • .Component({
  • selector: "my-friend",
  • inputs: [
  • "virtues.honesty",
  • "virtues.compassion",
  •  
  • // In this last input, notice that we are aliasing the input
  • // binding. This approach turns the namespace into a literal
  • // string rather than an object path.
  • "kindness: virtues.kindness"
  • ],
  • outputs: [
  • "behaviors.cry",
  • "behaviors.hug",
  •  
  • // In this last output, notice that we are aliasing the output
  • // binding. This approach turns the namespace into a literal
  • // string rather than an object path.
  • "laugh: behaviors.laugh"
  • ],
  • template:
  • `
  • I am your friend :)
  • `
  • })
  • .Class({
  • constructor: FriendController,
  •  
  • // Define the life-cycle event methods on the prototype so that
  • // they'll be picked up at run time.
  • ngOnChanges: function noop() {}
  • })
  • ;
  •  
  • return( FriendController );
  •  
  •  
  • // I control the SubChild component.
  • function FriendController() {
  •  
  • var vm = this;
  •  
  • // Setup the Virtues namespace for inputs.
  • vm.virtues = {
  • honesty: false,
  • compassion: false
  • };
  •  
  • // Setup the aliased input. Since the configuration for this input
  • // used an alias, the namespace became a string literal and is only
  • // used to match template tokens to the component meta-data.
  • vm.kindness = false;
  •  
  • // Setup the Behaviors namespace for outputs.
  • // --
  • // CAUTION: Using (isAsync=false) in order to make the console-logging
  • // a little bit easier to follow with the groups.
  • vm.behaviors = {
  • cry: new ng.core.EventEmitter( false ),
  • hug: new ng.core.EventEmitter( false )
  • };
  •  
  • // Setup the aliased output. Since the configuration for this output
  • // used an alias, the namespace became a string literal and is only
  • // used to match template tokens to the component meta-data.
  • vm.laugh = new ng.core.EventEmitter( false );
  •  
  • // After a delay, trigger some events.
  • setTimeout(
  • function triggerEvents() {
  •  
  • console.group( "Emitted Behaviors" );
  •  
  • // Emit the namespaced events.
  • vm.behaviors.cry.next( "Weep" );
  • vm.behaviors.hug.next( "Squeeze" );
  •  
  • // Emit the aliased event. Again, since this one was aliased,
  • // the bound output is stored in the root of the component,
  • // not in the namespace object.
  • vm.laugh.next( "Ha ha ha, lol." );
  •  
  • console.groupEnd();
  •  
  • },
  • 500
  • );
  •  
  • // Expose the public methods.
  • vm.ngOnChanges = ngOnChanges;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I get called whenever the input bindings change.
  • function ngOnChanges( changes ) {
  •  
  • console.group( "Virtues" );
  •  
  • // Check the two namespaced inputs.
  • console.log( "Honesty:", vm.virtues.honesty );
  • console.log( "Compassion:", vm.virtues.compassion );
  •  
  • // Check the aliased input. Again, since this one was aliased,
  • // the bound input is stored in the root of the component, not
  • // in the namespace object.
  • console.log( "Kindness:", vm.kindness );
  •  
  • console.groupEnd();
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, in order to accomodate the use of namespaces, I have to explicitly create the namespace objects - this.virtues and this.behaviors - in the root of the component. If I don't do this, Angular 2 will complain that objects are undefined. And, when we run the above code, we can see that everything worked perfectly:


 
 
 

 
 Input and Output bindings can be namespaced in Angular 2 beta 14. 
 
 
 

While this is not necessarily a feature that I would use all that often, I can see this as being an elegant way to organize a complex component. Sometimes, you don't want everything right there in the root of your instance. And, having a way to group inputs and outputs can help maintain sanity.




Reader Comments

@All,

One thing I should add, while the inputs can be in the namespace object, the "changes" event object uses the object path as the key. Meaning, in the ngOnChanges() life-cycle event handler, we'll get a "changes" object that looks something like this:

changes[ "virtues.compassion" ] = SimpleChange;
changes[ "virtues.honesty" ] = SimpleChange;
changes[ "kindness" ] = SimpleChange;

... notice that "virtues.compassion" is an object KEY, not a PATH.

Reply to this Comment

Ben, I really gotta ask, what makes you even think to do these mad scientist experiments? They're really interesting, but it would never occur to me to even try this. Cool post!

Reply to this Comment

@Matt,

Ha ha, a lot of this kind of stuff is actually accidental. Like, I'm doing something else and then I see something work in an unexpected way or throw an error with an "interesting" error message and I think, "Hmmm, I gotta dig into that a little more." :D

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.