Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Eric Betts
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Eric Betts

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

By
Published in Comments (5)

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:

Structural directive syntax is very flexible in Angular 2 Beta 14.

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.

Want to use code from this post? Check out the license.

Reader Comments

2 Comments

Is this documented somewhere. The ngFor does not use # as the start of the expression, whereas i can only get the *-magic working with a #-prefix

tx

2 Comments

Just realised `#` has been replaced by `let`. So it seems this *-magic only works when using `#` or `let`. I cannot find such a statement however in the documentation.

15,902 Comments

@Raphael,

Correct, the syntax was changing in one of the betas I think. Now, I believe that "#" / "var-" is only uses for Element/Component references and "let" is used for local template variables. So, I believe the syntax would now look like:

let i from 1 to 11 step 2 let first = first let last = last
let i from:2 to:12 step:2 let first=first let last=last
let i from 3 ; to 13 ; step 2 ; let first = first ; let last = last ;
let i ; from:4 ; to:14 ; step:2 ; let first = first ; let last = last ;
let i , from:5 , to:15 , step:2 , let first = first , let last = last

Its hard when the keep changing stuff, these posts, unfortunately, get out-of-date too quickly :(

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel