Angular Template Reference Variables beta

Examples of how to use template reference variables to reference DOM elements, components and directives in a template.

Updated Jan 2018, Angular version 5.1

DOM Elements

The hash symbol (#) is used to declare a template reference variable.

We declare a variable called refVar on a textarea element (in green) and then access the properties and methods in the DOM for that element (in yellow).

text-area-one.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-text-area-one',
  template: `
    <p>
      <textarea #refVar
                (keyup)="0"
                (select)="0"
                cols="25"
                rows="3">Change or highlight some of this text</textarea>
    </p>
    <div>textLength: {{refVar.textLength}}</div>
    <div>selectionStart: {{refVar.selectionStart}}</div>
    <div>selectionEnd: {{refVar.selectionEnd}}</div>
    <p>
      <button (click)="refVar.select()">
        Select All
      </button>
      <button (click)="refVar.value = ''"
              [disabled]="refVar.value === ''">
        Clear
      </button>
    </p>
  `
})
export class TextAreaOneComponent {
}

We can read properties

{{refVar.textLength}}

[disabled]="refVar.value === ''"

write to properties

(click)="refVar.value = ''"

and call methods

(click)="refVar.select()"

This is the output from the component. Note that all the functionality is achieved using template reference variables. There is no logic in the component class.

ref- Prefix

This example uses the ref- prefix (in green) as an alternative to the hash symbol. In other words, ref-isReadOnly is equivalent to #isReadOnly.

text-area-two.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-text-area-two',
  template: `
    <div>
      <textarea #textArea
                [readonly]="isReadOnly.checked"
                cols="25"
                rows="3">one two three four</textarea>
    </div>
    <label>
      <input ref-isReadOnly type="checkbox" (change)="0">
      Read only
    </label>
    <p>
      <button (click)="logMessage(textArea, isReadOnly)">Message</button>
    </p>
    <p>{{message}}</p>
  `
})
export class TextAreaTwoComponent {
  message: string;

  logMessage(textArea: HTMLTextAreaElement,
             readOnly: HTMLInputElement): void {

    this.message = `This text "${textArea.value}" is
                    ${readOnly.checked ? 'read only' : 'editable'}`;
  }
}

This is the output.

Variable Scope

Template reference variables are hoisted and can be accessed anywhere in the template. Here, the isReadOnly variable is accessed before and after it is declared.

text-area-two.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-text-area-two',
  template: `
    <div>
      <textarea #textArea
                [readonly]="isReadOnly.checked"
                cols="25"
                rows="3">one two three four</textarea>
    </div>
    <label>
      <input ref-isReadOnly type="checkbox" (change)="0">
      Read only
    </label>
    <p>
      <button (click)="logMessage(textArea, isReadOnly)">Message</button>
    </p>
    <p>{{message}}</p>
  `
})
export class TextAreaTwoComponent {
  message: string;

  logMessage(textArea: HTMLTextAreaElement,
             readOnly: HTMLInputElement): void {

    this.message = `This text "${textArea.value}" is
                    ${readOnly.checked ? 'read only' : 'editable'}`;
  }
}

Structural Directives

Take care when using template reference variables with structural directives (*ngIf, *ngFor etc).

For example, this code

conditional.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-conditional',
  template: `
    <p #para *ngIf="true">
      Some text
    </p>
    <!--#para === undefined: {{para === undefined}}-->
  `,
})
export class ConditionalComponent {
}

produces this output in development mode.

Some text

#para === undefined: true

#para is undefined even though *ngIf evaluates to true. To understand why this happens, see this blog post.

However, during a production build, the Angular compiler will report the following error:

Property 'para' does not exist on type 'ConditionalComponent'.

Object Types

We can pass template reference variable into a method on the component class. The parameter types are HTMLTextAreaElement and HTMLInputElement, and are documented on MDN Web Docs.

text-area-two.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-text-area-two',
  template: `
    <div>
      <textarea #textArea
                [readonly]="isReadOnly.checked"
                cols="25"
                rows="3">one two three four</textarea>
    </div>
    <label>
      <input ref-isReadOnly type="checkbox" (change)="0">
      Read only
    </label>
    <p>
      <button (click)="logMessage(textArea, isReadOnly)">Message</button>
    </p>
    <p>{{message}}</p>
  `
})
export class TextAreaTwoComponent {
  message: string;

  logMessage(textArea: HTMLTextAreaElement,
             readOnly: HTMLInputElement): void {

    this.message = `This text "${textArea.value}" is
                    ${readOnly.checked ? 'read only' : 'editable'}`;
  }
}

Note

This is just to illustrate the object type of the variables. In a real application, a better solution would be to pass in only those values that are needed. For example:

logMessage(textArea.value, isReadOnly.checked)

logMessage(text: string, readOnly: boolean): void {
    this.message = `This text "${text}" is ${readOnly ? 'read only' : 'editable'}`;
}

This will keep details of the DOM element out of the component class, which is considered good practice.

Components

Template reference variables can be used to access public properties and methods of a component class.

Take this timer component which has several public methods.

timer.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-timer',
  template: `
    <h1>
      {{elapsedTime | date:'HH:mm:ss'}}
      <span class="half-size">{{elapsedTime | date:'S'}}</span>
    </h1>
  `
})
export class TimerComponent {
  private totalMillis = 0;
  private intervalId: number = null;

  get elapsedTime(): Date {
    return new Date(this.totalMillis);
  }

  get isRunning(): number {
    return this.intervalId;
  }

  get isTimeSet(): number {
    return this.totalMillis;
  }

  start(): void {
    const startMillis = this.nowMillis;
    this.intervalId = window.setInterval(() => {
      this.totalMillis = this.nowMillis - startMillis;
    }, 100);
  }

  stop(): void {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }

  reset(): void {
    this.stop();
    this.totalMillis = 0;
  }

  private get nowMillis(): number {
    return new Date().getTime();
  }
}

We use a template reference variable on the <app-timer> custom element to execute the methods on the component class.

stopwatch.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-stopwatch',
  template: `
    <app-timer #timer></app-timer>
    <button (click)="timer.start()" [disabled]="timer.isTimeSet">Start</button>
    <button (click)="timer.stop()" [disabled]="!timer.isRunning">Stop</button>
    <button (click)="timer.reset()" [disabled]="!timer.isTimeSet">Reset</button>
  `
})
export class StopwatchComponent {
}

And here it is.



We can also use the same component in this typing test example.

typing-test.component.ts
import { Component } from '@angular/core';
import { TimerComponent } from './timer.component';

@Component({
  selector: 'app-typing-test',
  template: `
    <h2 #text>{{phrase}}</h2>
    <textarea #answer
      (keydown)="timer.isRunning || timer.start()"
      (keyup)="checkAnswer(answer.value, timer)"
      placeholder="Type the phrase here"
      cols="50" rows="5"></textarea>
    <div>
      <button (click)="answer.value=''; next()">Next</button>
    </div>
    <div [hidden]="answer.value !== text.innerText">
      <app-timer #timer></app-timer>
    </div>
  `
})
export class TypingTestComponent {
  private phrases: string[] = [
    'We\'re gonna need a bigger boat.',
    'What doesn\'t kill you makes you stranger.',
    'The first rule of project mayhem is you do not ask questions.',
    'As far back as I can remember, I always wanted to be a gangster.',
    'I want you to deal with your problems by becoming rich!'
  ];

  private position = 0;

  checkAnswer(answer: string, timer: TimerComponent): void {
    if (answer === this.phrase) {
      timer.stop();
    }
  }

  next(): void {
    this.position = this.position === this.phrases.length - 1 ? 0 : this.position + 1;
  }

  get phrase(): string {
    return this.phrases[this.position];
  }
}