Understanding DOM manipulation in Angular

Lokesh Sardana
7 min readApr 12, 2019

--

If you are coming from the background of working with angularjs, it was quite straight forward to access and manipulate the DOM there. You had access to the DOM node through element injected in the link function of the directive.

function link(scope, element, attrs) {
}

Or through angular.element which was an AngularJS’s built in subset of jQuery. But this approach had its drawbacks. It made your code tightly coupled with Browser’s API.

The new Angular (2 onwards) works on multiple platforms: mobile, web workers etc. So, they have introduced a number of APIs to work as an abstraction layer between your code and platform APIs. These APIs come in the form of different reference types likeElementRef, TemplateRef, ViewRef, ComponentRef and ViewContainerRef.

In this blog, we will see some examples of how these reference types can be used to manipulate DOM in angular. But before that let’s look at the ways to access these reference types within a Component/Directive.

DOM Queries

Angular has provided two ways to query/access various reference types within a Component/Directive. These are

  • ViewChild/ViewChildren
  • ContentChild/ContentChildren

ViewChild/ViewChildren

These are decorators which can be used within a Component/Directive as @ViewChild (returns a single reference) or @ViewChildren (returns a list of references in the form of a QueryList). These will assign the values of reference types from template to the component fields they are applied to. The basic usage is as follow:

@ViewChild(selector, {read: ReferenceType}) fieldName;

A selector can be a string representing a template reference variable, or a Component/Directive class, or a TemplateRef or a provider defined in the child component tree.

@ViewChild("myElem") template: ElementRef;

The second parameter is optional and is only required to query some reference types which can’t be inferred easily by Angular like ViewContainerRef.

@ViewChild("myContainer", {read: ViewContainerRef}) container: ViewContainerRef;

ContentChild/ContentChildren

The usage is pretty much similar to that of ViewChild/ViewChildren. The only difference is that it queries within the <ng-content> projected elements of the component while the @ViewChild queries within the template of the component. This will be explained better in the examples of upcoming sections.

DOM access via ElementRef

ElementRef is a very basic abstraction layer on a DOM element in Angular. It’s an angular wrapper around the native element.

You can get hold of ElementRef in a Component or Directive in following ways:

Dependency Injection

Host element of a Component or Directive can be accessed via direct DI in the constructor.

@Component({
selector: 'app-test',
template: '<div>I am a test component</div>'
})
export class TestComponent implements OnInit {

constructor(private element: ElementRef) { }

ngOnInit() {
console.log(this.element.nativeElement);
}

}
/*
* Output:
* <app-test>
* <div>I am a test component</div>
* </app-test>
* */

Using ViewChild and Template Reference Variables

@Component({
selector: 'app-test',
template: `
<div #child1>First Child</div>
<div>Second Child</div>
`
})
export class TestComponent implements OnInit {

@ViewChild("child1") firstChild: ElementRef;

constructor() { }

ngOnInit() {
console.log(this.firstChild.nativeElement);
}

}

/*
* Output: <div>First Child</div>
* */

Using ContentChild

Works in a similar manner as that of @ViewChild, but for <ng-content> projected elements.

// Child Component
@Component({
selector: "component-a",
template: `<ng-content></ng-content>`
})
export class ComponentA {
@ContentChild("contentChild") contentChild: ElementRef;

ngOnInit() {
console.log(this.contentChild.nativeElement);
}
}
// Parent Component
@Component({
selector: 'app-test',
template: `
<component-a>
<div #contentChild>Content Child 1</div>
<div>Content Child 2</div>
</component-a>
`
})
export class TestComponent implements OnInit {}
/*
* Output: <div>Content Child 1</div>
* */

It looks pretty straight forward that you can easily access a DOM element via ElementRef and then manipulate the DOM by accessing the nativeElement. Something like this:

@Component({
selector: 'app-test-component',
template: `
<div class="header">I am a header</div>
<div class="body" #body>
</div>
<div class="footer">I am a footer</div>
`
})
export class TestComponent implements AfterContentInit {
@ViewChild("body") bodyElem: ElementRef;

ngAfterContentInit(): void {
this.bodyElem.nativeElement.innerHTML = `<div>Hi, I am child added by directly calling the native APIs.</div>`;
}


}

However, the direct usage of ElementRef is discouraged by Angular Team because it directly provides the access to DOM which can make your application vulnerable to XSS attacks. It also creates tight coupling between your application and rendering layers which makes is difficult to run an app on multiple platforms.

https://angular.io/api/core/ElementRef#properties

So, how should we deal with it? The answer is: Views.

Everything is a ‘View’ in Angular

A view is the smallest building block of an angular app’s UI. Every component has its own view. You can consider it as a group of elements which can be created and destroyed together.

A view can be classified into two types:

  • Embedded Views — created from templates
  • Host Views — created from components

Displaying a view in UI can be broken down into two steps:

  1. Creating a view from template or component
  2. Rendering a view into a view container

Embedded Views

Embedded views are created from templates defined using <ng-template> element.

Creating an embedded view

First a template needs to be accessed within a component as TemplateRefusing @ViewChild and template reference variable. Then, an embedded view can be created from a TemplateRef by passing a data-binding context.

const viewRef = this.template.createEmbeddedView({
name: "View 1"
});

This context is being consumed by the template in<ng-template>.

<ng-template #template let-viewName="name">
<div>Hi, My name is {{viewName}}. I am a view created from a template</div>
</ng-template>

You can also use the $implicit property in the context if you have only a single property to bind.

const viewRef = this.template.createEmbeddedView({
$implicit: "View 1"
});

In this case, you can skip assigning values to template variables.

<ng-template #template let-viewName>
<div>Hi, My name is {{viewName}}. I am a view created from a template</div>
</ng-template>

Rendering an embedded view

Till now, we have created only an instance of ViewRef. This view is still not visible in the UI. In order to see it in the UI, we need a placeholder (a view container) to render it. This placeholder is being provided by ViewContainerRef.

Any element can serve as a view container, however <ng-container> is a better candidate as it is rendered as a comment and doesn’t leave any redundant element in the html DOM.

@Component({
selector: 'app-test-component',
template: `
<div class="header">I am a header</div>
<div class="body">
<ng-container #container></ng-container>
</div>
<div class="footer">I am a footer</div>

<ng-template #template let-viewName="name">
<div>Hi, My name is {{viewName}}. I am a view created from a template</div>
</ng-template>
`,
})
export class TestComponent implements AfterContentInit {

@ViewChild("template") template: TemplateRef;
@ViewChild("container", {read: ViewContainerRef}) container: ViewContainerRef;

constructor() { }

ngAfterContentInit(): void {
const viewRef = this.template.createEmbeddedView({
name: "View 1"
});
this.container.insert(viewRef);
}
}

Both <ng-container> and <ng-template> elements will be rendered as comments leaving the html DOM neat and clean.

The above 2 steps process of creating a view and adding it into a container can further be reduced by using the createEmbeddedView method available in the ViewContainerRef itself.

this.container.createEmbeddedView(this.template, {
name: "View 1"
});

This can be further simplified by moving the whole view creation logic from component class to the template using ngTemplateOutlet and ngTemplateOutletContext.

@Component({
selector: 'app-test-component',
template: `
<div class="header">I am a header</div>
<div class="body">
<ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{name: 'View 1'}"></ng-container>
</div>
<div class="footer">I am a footer</div>

<ng-template #template let-viewName="name">
<div>Hi, My name is {{viewName}}. I am a view created from a template</div>
</ng-template>
`
})
export class TestComponent {}

Host Views

Host Views are quite similar to Embedded View. The only difference is that the Host Views are created from components instead of templates.

Creating a host view

In order to create a host view, first you need to create a ComponentFactory of the component you want to render using ComponentFactoryResolver.

constructor(
private componentFactoryResolver: ComponentFactoryResolver
) {
this.someComponentFactory = this.componentFactoryResolver.resolveComponentFactory(SomeComponent);
}

Then, a dynamic instance of the component is created by passing an Injector instance to the factory. Every component should be bound to an instance of Injector. You can use the injector of the parent component for the dynamically created components.

const componentRef = this.someComponentFactory.create(this.injector);
const viewRef = componentRef.hostView;

Rendering a host view

Rendering a host view is almost similar to rendering an embedded view. You can directly insert it into a view container.

@Component({
selector: 'app-test-component',
template: `
<div class="header">I am a header</div>
<div class="body">
<ng-container #container></ng-container>
</div>
<div class="footer">I am a footer</div>
`,
})
export class TestComponentComponent implements AfterContentInit {

@ViewChild("container", {read: ViewContainerRef}) container: ViewContainerRef;

private someComponentFactory: ComponentFactory<SomeComponent>;

constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector
) {
this.someComponentFactory = this.componentFactoryResolver.resolveComponentFactory(SomeComponent);
}

ngAfterContentInit(): void {
const componentRef = this.someComponentFactory.create(this.injector);
const viewRef = componentRef.hostView;
this.container.insert(viewRef);
}
}

Or by directly calling the createComponent method of ViewContainerRef and passing the component factory instance.

this.container.createComponent(this.someComponentFactory);

Now, similar to embedded view, we can also shift the whole logic of host view creation in template itself using ngComponentOutlet.

@Component({
selector: 'app-test-component',
template: `
<div class="header">I am a header</div>
<div class="body">
<ng-container [ngComponentOutlet]="comp"></ng-container>
</div>
<div class="footer">I am a footer</div>
`
})
export class TestComponent {
comp = SomeComponent;
}

Don’t forget to store the reference of the component class in parent component’s field. The template has access only to the fields of the components.

Summary

Here we come to an end. Let’s conclude what we have understood till now.

  • We can access the DOM in Angular using different reference types likeElementRef, TemplateRef, ViewRef, ComponentRef and ViewContainerRef.
  • These reference types can be queried from templates using @ViewChild and @ContentChild.
  • Browser’s native DOM element can be accessed via ElementRef. However, manipulating this element directly is discouraged because of security reasons.
  • Concept of Views.
  • How to create and render an Embedded View.
  • How to create and render a Component View.

So, that’s it for today about understanding DOM manipulation in Angular. This was my first blog. Please do leave me feedback in the comments.

--

--