The Flexible Syntax Of Structural Directive Expressions In Angular 2 Beta 14

Yesterday, I started digging into structural directives in Angular 2. As I was building my index-loop directive, I noticed that the use of the ";" delimiter in my directive expression seemed to be optional. After a little more trial and error (and perusal of the source code), I realized that most of the operators and separators in the directive expression are optional and are there only for enhanced readability and developer preference.

Run this demo in my JavaScript Demos project on GitHub.

When it comes to structural directive expressions (using the "*" syntax sugar) there are two sets of bindings that we are configuring:

Input property bindings.

View-local variable bindings.

There seem to be a few hard-and-fast rules regarding these bindings:

When it comes to the input property bindings, we cannot use the "=" assignment operator.

When it comes to the view-local variable bindings, we have to use the "=" assignment operator (except for the implicit binding).

The implicit view-local variable binding (ie, #i) has to be the first sub-expression.

Other than that, the use of the ":" operator and the use of separators (such as ";" and ",") are completely up to you.

To demonstrate, I'm going to take the bnLoop index-loop structural directive from yesterday and remove just about everything but a console.log() statement. Then, we'll create five instances of the directive that are functionally equivalent but have completely different syntax:

<!doctype html> <html> <head> <meta charset="utf-8" /> <title> The Flexible Syntax Of Structural Directive Expressions In Angular 2 Beta 14 </title> <link rel="stylesheet" type="text/css" href="./demo.css"></lin> </head> <body> <h1> The Flexible Syntax Of Structural Directive Expressions 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( "Test" ) ], // The "*" syntactic sugar for structural directives in Angular 2 // affords a lot of optional syntax for enhanced readability (and // personal preference). All five of the following *bnTest // expressions are functionally equivalent. Notice the swappable // and optional use of " " and ";" and "," as statement delimiters // and the optional use of ":" as the assignment operator. // -- // NOTE: View-local variables (ex, #first) are different than // input bindings and have to use the "=" assignment operator. // And, when expressions have an implicit view-local variable // (such as #i in this case), it always has to go first. template: ` <section *bnTest="#i from 1 to 11 step 2 #first = first #last = last"> {{ i }} - {{ first }} - {{ last }} </section> <section *bnTest="#i from:2 to:12 step:2 #first=first #last=last"> {{ i }} - {{ first }} - {{ last }} </section> <section *bnTest="#i from 3 ; to 13 ; step 2 ; #first = first ; #last = last ;"> {{ i }} - {{ first }} - {{ last }} </section> <section *bnTest="#i ; from:4 ; to:14 ; step:2 ; #first = first ; #last = last ;"> {{ i }} - {{ first }} - {{ last }} </section> <section *bnTest="#i , from:5 , to:15 , step:2 , #first = first , #last = last"> {{ i }} - {{ first }} - {{ last }} </section> ` }) .Class({ constructor: AppController }) ; return( AppController ); // I control the App component. function AppController() { // Nothing to do here... } } ); // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // I provide a test structure directive that logs out its input bindings just // to demonstrate that they were bound properly. define( "Test", function registerTest() { ng.core .Directive({ selector: "[bnTest]", // Because we are using the "*" template syntax, our expression // is parsed into a number of individual inputs that are all // prefixed with the directive name. So, given the expression: // -- // #i from 1 to 10 step 1 // -- // ... we are given the following inputs: // -- // bnTestFrom ( parsed "from" sub-attribute ) // bnTestTo ( parsed "to" sub-attribute ) // bnTestStep ( parsed "step" sub-attribute ) // -- // NOTE: The "#i" portion is a view-local variable that we have // to setup on each instance of the template that we clone. inputs: [ "from: bnTestFrom", "to: bnTestTo", "step: bnTestStep" ] }) .Class({ constructor: LoopController, // Define the ngOnChanges: function noop() {} }) ; LoopController.parameters = [ new ng.core.Inject( ng.core.ViewContainerRef ), new ng.core.Inject( ng.core.TemplateRef ) ]; return( LoopController ); // I control the Loop directive. function LoopController( viewContainerRef, templateRef ) { var vm = this; // Expose the public methods. vm.ngOnChanges = ngOnChanges; // --- // PUBLIC METHODS. // --- // I get called whenever any one of the bound input values change. function ngOnChanges( changes ) { var viewRef = viewContainerRef.createEmbeddedView( templateRef ); console.log( "ngOnChanges( from: %s, to: %s, step: %s )", vm.from, vm.to, vm.step ); // Set up all the local variable bindings. viewRef.setLocal( "$implicit", vm.from ); viewRef.setLocal( "first", true ); viewRef.setLocal( "last", false ); } } } ); </script> </body> </html>

As you can see, we are implementing the *bnLoop structural directive with the following five expressions:

#i from 1 to 11 step 2 #first = first #last = last

#i from:2 to:12 step:2 #first=first #last=last

#i from 3 ; to 13 ; step 2 ; #first = first ; #last = last ;

#i ; from:4 ; to:14 ; step:2 ; #first = first ; #last = last ;

#i , from:5 , to:15 , step:2 , #first = first , #last = last

And, when we run the above code, we get the following output:

As you can see, they all work perfectly well. Functionally, they are all equivalent. Just about all of the separators and input assignment operators are optional in Angular 2 structural directive expressions. It's up to you, as the developer, to choose a combination of tokens that lends well to readability and maintainability. With great power comes great responsibility.

Tweet This Groovy post by @BenNadel - The Flexible Syntax Of Structural Directive Expressions In Angular 2 Beta 14 Woot woot — you rock the party that rocks the body!







