Photo by Aritra Roy on Unsplash

Part 1: Stop using @ViewChild

rajesh kumar
3 min readMay 26, 2024

Accessing native DOM elements is a fundamental task in web development. Modern frameworks, however, minimize the need for direct DOM access by utilizing various strategies. For example, Angular uses directives like *ngIf to conditionally add or remove elements and *ngFor to repeat elements. These techniques abstract away direct DOM manipulation, making it less of a concern when developing applications in Angular or similar frameworks.

Despite these abstractions, there are scenarios where direct DOM access remains necessary. Some third-party libraries, such as Google Maps, require direct DOM elements to function. Google Maps needs a div element to initialize and display the map. Typically, developers use the @ViewChild decorator to access the div element and then provide it to the Map API for initialization.

/// <reference types="@types/google.maps" />
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';

@Component({
selector: 'app-map-component',
template: `<div class="map-container" #googleMap></div>`,
styles: ['.map-container {height: 500px;}'],
})
export class MapComponentComponent implements AfterViewInit {
@ViewChild('googleMap', { read: ElementRef }) googleMapElement!: ElementRef<HTMLDivElement>;
map?: google.maps.Map;

async ngAfterViewInit() {
const { Map } = await google.maps.importLibrary("maps") as google.maps.MapsLibrary;
// Pass the nativeElement to google map library
this.map = new Map(this.googleMapElement.nativeElement, {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
}
}

While this approach works, it introduces three significant problems:

  1. If the div element is removed from the template, the code breaks, making the map functionality non-reusable.
  2. The reference to the native element is only available after the ngAfterViewInit lifecycle hook.
  3. It violates the Single Responsibility Principle of SOLID design principles. The component using @ViewChild becomes responsible for fetching data for the map, initializing it, setting it up, and using it.

There is a better approach: using an attribute directive.

Let’s refactor the previous component and create a map attribute directive.

/// <reference types="@types/google.maps" />
import { Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
selector: 'div[appGoogleMap]'
})
export class GoogleMapDirective implements OnInit {
map?: google.maps.Map;
constructor(private elementRef: ElementRef<HTMLDivElement>) { }

async ngOnInit() {
const { Map } = await google.maps.importLibrary("maps") as google.maps.MapsLibrary;
this.map = new Map(this.elementRef.nativeElement, {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
}
}

// Use attribute directive
<div class="map-container" appGoogleMap></div>

Let’s consider another use case: drawing shapes or images on a canvas element. An easy approach is to use @ViewChild to get the drawing context from the native element.

A better alternative is to use an attribute directive and pass the drawing information via an @Input property.

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

@Component({
selector: 'app-canvas-component',
template: `<canvas #myCanvas></canvas>`,
})
export class CanvasComponentComponent {
// No way to check if myCanvas on build time
@ViewChild('myCanvas', { read: ElementRef }) canvasRef!: ElementRef<HTMLCanvasElement>;

ngAfterViewInit() {
const canvas = this.canvasRef.nativeElement;
const canvasContext = canvas.getContext('2d');
// Draw using cavas context
canvasContext?.beginPath();
canvasContext?.arc(75, 75, 50, 0, 2 * Math.PI);
canvasContext?.stroke();
}
}

// Vs
import { Directive, ElementRef } from '@angular/core';

@Directive({
// this directive can be used with Cavas element only
selector: 'canvas[appCanvas]'
})
export class CanvasDirective {
constructor(private elementRef: ElementRef<HTMLCanvasElement>) {
this.draw();
}

draw() {
const canvas = this.elementRef.nativeElement;
const canvasContext = canvas.getContext('2d');
// Draw using cavas context
canvasContext?.beginPath();
canvasContext?.arc(75, 75, 50, 0, 2 * Math.PI);
canvasContext?.stroke();
}
}
// Use attribute directive
<canvas appCanvas></canvas>

Looks good, right? But wait, there’s more.

We’ve addressed our first two issues, but our attribute directive is still handling multiple responsibilities. Can we break it down further? Let’s explore that in the next blog.

--

--