Angular builds the DOM for you, but sometimes you must touch it yourself: set an attribute, add a listener, or work with a library that needs a real element. This lesson shows the tools, and the decorators you will meet in older code.
What you will learn
ElementRefand why direct DOM access is riskyRenderer2for safe DOM changes@HostBindingand@HostListener, and their modernhostreplacement@Input,@OutputandEventEmitter
ElementRef
ElementRef wraps the host element of a component or directive:
// src/app/dom/underline.ts
import { Directive, ElementRef, inject } from '@angular/core';
@Directive({ selector: '[appUnderline]' })
export class Underline {
private readonly el = inject<ElementRef<HTMLElement>>(ElementRef);
constructor() {
this.el.nativeElement.style.textDecoration = 'underline';
}
}
nativeElement is the real DOM node. Direct access works in the browser, but it has risks: code that touches document or nativeElement breaks during server-side rendering, and writing innerHTML can open XSS holes. Prefer template bindings, and use ElementRef only for reading or when no binding exists.
Renderer2
Renderer2 is an abstraction that changes the DOM in a way that also works on the server and with other platforms:
// src/app/dom/highlight-renderer.ts
import { Directive, ElementRef, Renderer2, inject } from '@angular/core';
@Directive({
selector: '[appMark]',
host: {
'(mouseenter)': 'on()',
'(mouseleave)': 'off()',
},
})
export class Mark {
private readonly el = inject(ElementRef);
private readonly renderer = inject(Renderer2);
protected on(): void {
this.renderer.addClass(this.el.nativeElement, 'marked');
this.renderer.setStyle(this.el.nativeElement, 'background', 'khaki');
this.renderer.setAttribute(this.el.nativeElement, 'title', 'Marked');
}
protected off(): void {
this.renderer.removeClass(this.el.nativeElement, 'marked');
this.renderer.removeStyle(this.el.nativeElement, 'background');
this.renderer.removeAttribute(this.el.nativeElement, 'title');
}
}
Useful methods: addClass, removeClass, setStyle, removeStyle, setAttribute, removeAttribute, createElement, appendChild, removeChild, setProperty and listen (add an event listener that Angular removes for you). In most directives, the host property makes even Renderer2 unnecessary.
HostBinding and HostListener
The classic decorators bind to the host element:
// src/app/dom/legacy-host.ts
import { Directive, HostBinding, HostListener } from '@angular/core';
@Directive({ selector: '[appHover]' })
export class LegacyHover {
@HostBinding('class.hovered') hovered = false;
@HostBinding('style.cursor') cursor = 'pointer';
@HostListener('mouseenter') onEnter(): void {
this.hovered = true;
}
@HostListener('mouseleave') onLeave(): void {
this.hovered = false;
}
@HostListener('window:resize', ['$event']) onResize(event: UIEvent): void {
console.log('window resized', (event.target as Window).innerWidth);
}
}
The modern version puts everything in the host object, in one place, and works with signals:
// src/app/dom/modern-host.ts
import { Directive, signal } from '@angular/core';
@Directive({
selector: '[appHoverModern]',
host: {
'[class.hovered]': 'hovered()',
'[style.cursor]': '"pointer"',
'(mouseenter)': 'hovered.set(true)',
'(mouseleave)': 'hovered.set(false)',
'(window:resize)': 'onResize($event)',
},
})
export class ModernHover {
protected readonly hovered = signal(false);
protected onResize(event: UIEvent): void {
console.log('window resized', (event.target as Window).innerWidth);
}
}
Angular removes these listeners when the element is destroyed.
@Input, @Output and EventEmitter
Before signals, inputs and outputs were decorators:
// src/app/dom/legacy-child.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-legacy-child',
template: `
<p>Hello, {{ name }}!</p>
<button type="button" (click)="clicked.emit(name)">Say it</button>
`,
})
export class LegacyChild {
@Input() name = 'friend';
@Input({ required: true }) id!: number;
@Input({ transform: (v: string) => v.trim() }) label = '';
@Output() clicked = new EventEmitter<string>();
}
The signal equivalents from earlier lessons are shorter and typed better:
- `@Input() name = 'x'`: Signal style:
name = input('x') - `@Input({ required: true }) id!: number`: Signal style:
id = input.required<number>() - `@Output() clicked = new EventEmitter<string>()`: Signal style:
clicked = output<string>() - `this.clicked.emit(v)`: Signal style:
this.clicked.emit(v)(same) - `@Input() + @Output() valueChange`: Signal style:
value = model('')
The migrations ng generate @angular/core:signal-input-migration and output-migration convert code for you. Both styles work side by side in one project. Decorators are the older generation, not removed.
Why Angular decorators matter: a decorator is a function that adds metadata to a class or property. @Component, @Directive, @Injectable and @Pipe describe the class. @Input, @Output, @HostBinding, @HostListener, @ViewChild and @ContentChild describe members. In new code many of these became functions (input, output, viewChild) or configuration (host).
Put it together
// src/app/dom/dom-page.ts
import { Component } from '@angular/core';
import { LegacyChild } from './legacy-child';
import { LegacyHover } from './legacy-host';
import { Mark } from './highlight-renderer';
import { ModernHover } from './modern-host';
import { Underline } from './underline';
@Component({
selector: 'app-dom-page',
imports: [LegacyChild, LegacyHover, Mark, ModernHover, Underline],
template: `
<p appUnderline>Underlined by ElementRef</p>
<p appMark>Hover me (Renderer2)</p>
<p appHover>Hover me (legacy decorators)</p>
<p appHoverModern>Hover me (host property)</p>
<app-legacy-child name="Ada" [id]="1" label=" x " (clicked)="last = $event" />
<p>Last clicked: {{ last }}</p>
`,
styles: `.hovered { background: lightblue; }`,
})
export class DomPage {
protected last = '';
}
// src/app/app.ts
import { Component } from '@angular/core';
import { DomPage } from './dom/dom-page';
@Component({
imports: [DomPage],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
<app-dom-page />
</main>
Rules of thumb
- Binding first. Use
[class],[style],[attr]andhostbefore touching the DOM. - Renderer2 over direct DOM changes when you must change something.
- Never build HTML strings from user input.
- Guard browser-only code (
afterNextRender) so server rendering works.
Try it yourself
- Write a directive
appBackgroundthat sets a background colour withRenderer2from an input. - Convert
LegacyChildto signal inputs and outputs. - Add a
(keydown.escape)listener to a directive using thehostproperty.
Quick check
- Why prefer
Renderer2over changingnativeElementdirectly? - What replaces
@HostBindingand@HostListenerin modern code? - What is the signal version of
@Output()withEventEmitter?
Next, continue with Angular Injection Tokens and Providers: Configuration and Testing.