Getting Element references in Angular 9 templates

0
777
Angular 9

In this post you’re going to learn how to Element references in Angular 9 template.

This is typically known as a “Template Reference” variable, in React they’re called “refs” too, so the concept is much the same.

Angular 9 template reference variable

Element references inside the template

First, let’s create an <input> and give it an ngModel directive:

<input ngModel>

Notice here how I’m not using the typical [(ngModel)] syntax, as here we just want to allow Angular to set a local model and update on change. This allows us to then log out the model value in a different way momentarily!

To get a direct reference to the DOM element, we’ll introduce Angular’s ref syntax – the hash # symbol. Adding it to our <input> would allow us to do something interesting:

<input ngModel #username>
<p>{{ username.value }}</p>

We’ve now used #username (you can call these template ref variables anything you like) and because it’s a reference to our <input ngModel>, it’s actually re-binding the value under-the-hood each time we type.

As #username is now a reference variable bound to the Angular component, we can access it anywhere – which is exactly what I’m doing with {{ username.value }} and logging out the value!

Element references inside a Component class

So we know how to access our #username inside the template, but what about the component?

Angular 9 template reference variable

For this, we need to introduce ViewChild, which can be used as a decorator on a property:

@Component({...})
export class AppComponent {
  @ViewChild('username') input;
}

Now anywhere inside the class we can reference this.input and get the element ref! But, what type is this?

Related: Angular 9 Lazy Loading

ViewChild can return a few different things, one being an ElementRef – an element reference! This looks like:

class ElementRef<T> {
  constructor(nativeElement: T)
  nativeElement: T
}

This <T> is a generic in TypeScript, which means we should supply further type information for it to infer down to the constructor and also the nativeElement property. Here’s how we can do that:

@Component({...})
export class AppComponent {
  @ViewChild('username') input: ElementRef<HTMLInputElement>;
}

So surely now we can just go ahead and access this.input?

@Component({...})
export class AppComponent {
  @ViewChild('username') input: ElementRef<HTMLInputElement>;

  constructor() {
    // undefined
    console.log(this.input);
  }
}

Inside the constructor our this.input reports as undefined, and moving this console.log to ngOnInit solves the issue as the component template is ready then:

So surely now we can just go ahead and access this.input?

@Component({...})
export class AppComponent implements OnInit {
  @ViewChild('username') input: ElementRef<HTMLInputElement>;

  ngOnInit() {
    // ElementRef { nativeElement: <input> }
    console.log(this.input);
  }
}

We can totally use ngOnInit, but there’s actually a lifecycle hook built exclusively for when our ViewChild will be ready – and that’s the AfterViewInit hook. Let’s refactor:

@Component({...})
export class AppComponent implements AfterViewInit {
  @ViewChild('username') input: ElementRef<HTMLInputElement>;

  ngAfterViewInit() {
    // ElementRef { nativeElement: <input> }
    console.log(this.input);
  }
}

So to use the native <input> we’ll need to reference this.input.nativeElement each time.

⚠ WARNING! IT’S ADVISED NOT TO USE PROPERTIES ON THIS.INPUT.NATIVEELEMENT DIRECTLY HOWEVER – USE THE RENDERER2 CLASS!

The docs for ElementRef suggest “Relying on direct DOM access creates tight coupling between your application and rendering layers which will make it impossible to separate the two and deploy your application into a web worker.”

I agree, so let’s do it properly.

The Renderer2 is considered the preferred option. Putting everything together, our final solution ends up as follows:

import { Component, ViewChild, ElementRef, AfterViewInit, Renderer2 } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
  <div>
    <input ngModel #username>
    <p>{{ username.value }}</p>
  </div>
  `
})
export class AppComponent implements AfterViewInit {
  @ViewChild('username') input: ElementRef<HTMLInputElement>;

  constructor(private renderer: Renderer2) {}
  
  ngAfterViewInit() {
    // ElementRef: { nativeElement: <input> }
    console.log(this.input);

    // Access the input object or DOM node
    console.dir(this.input.nativeElement);

    // Manipulate via Renderer2
    this.renderer.setStyle(this.input.nativeElement, 'background', '#d515a0');
  }
}

Related: Ionic Google Maps

Hope! you are know about Element references in Angular 9 template.