Cordova vs. Zone.js or "Why is Angular's document event listener not in a zone?"

Use Angular template reference variables anywhere in the template... not!

Did you ever use template reference variables in Angular? Maybe you don't remember exactly what they are or represent. Quick and easy they allow you to access either a DOM element or an instance of a component or directive in your current template. They are defined by writing a hash sign (#) together with a name as an additional attribute of the DOM element. If this DOM element is just a plain one the variable contains a reference to it. In that case you can access any property of that DOM element through this variable:

<input type="text" (change)="true" #variable>
{{ variable.value }}

In this example the entered value will be written out when you blur the input (the event listener for change is just for having the change detection run).

If you have a component living on the DOM element the variable will contain the instance of that component (and not the DOM element):

<my-component #variable [input]="'hello'"></my-component>
{{ variable.input }}

And if you want to access the instance of a directive you need to specify which one, because there could be more than one directive existing on that DOM element. The exportAs property of the directive's decorator defines the attribute's value to access that particular instance. For example, the FormsModule's directive for the form DOM element (NgForm) is exporting itself as ngForm. This enables you to e.g. disable a button until the form is valid:

<form #variable="ngForm">
  <!-- some more HTML ... -->
  <button type="submit" [disabled]="variable.form.invalid">Submit!</button>
</form>

Most of the time this works without any problems. But there is a snag in it. First let's take a look what the Angular documentation writes about these variables:

"You can refer to a template reference variable anywhere in the template."

Sounds good. And later the documentation states:

"The scope of a reference variable is the entire template."

Yep, got it. But take a look at the following component's template:

<div *ngIf="true">
  <my-component #variable [input]="'Do you see me?'>
</div>
{{ variable.input }}

What do you think is happening now? Will it show the component input's value "Do you see me?" on the screen? No! It won't. Instead you will get an error in the console:

TypeError: Cannot read property 'input' of undefined

Wait! Didn't the documentation suggest multiple times that these variables can be used anywhere in the template? Yes, and that is still true even it does not look like that for now.

The reason for this is the ngIf directive - or any other directive used together with a star (*). These directives are so called structural directives and the star is just a short form for something more complex. Every time you use a structural directive (ngIf, ngFor, ngSwitchCase, ...) Angular's view engine is desugaring (it removes the syntactical sugar) the star within two steps to the following final form (using the previous ngIf as an example):

<ng-template [ngIf]="true">
  ...
</ng-template>

Did you notice? Something extraordinary emerged out of the previous HTML containing the structural directive - the ng-template node. This node is the reason for limiting the scope of the template reference variable to exactly this inner DOM part only - it defines a new template inside. Because of limiting a variable to its defining template any variable defined within the ng-template (and this means within the DOM part of the ngIf) cannot be used outside of it. It cannot cross the borders of a template.

Huh, so the documentation is right? Yes, but maybe it is not clearly written somewhere that structural directives introduce new nested templates with new barriers for those variables which should be available in the whole template file primarily.

Comments

Feed You can follow this conversation by subscribing to the comment feed for this post.

The comments to this entry are closed.