skip to content

In Angular, how do the @defer viewport, interaction and hover triggers decide which element to watch, and what rules apply to it?

level: middleimportance: should knowfreq 38%

answer

  1. placeholder or a template reference
  2. exactly one root element
  3. same template, reachable view
  4. viewport options object, minus root

basics

~20 s

Angular's element-based @defer triggers watch the @placeholder's single root element by default, or an element named by a template reference variable in the same template, reachable from the block. Missing or unreachable targets are compile errors.

solid answer

~40 s

`viewport`, `interaction` and `hover` need a DOM element. Written bare — `on viewport` — they watch the root element of the `@placeholder` block, which must have **exactly one** root element; a bare trigger without any placeholder does not compile. Alternatively you pass a template reference: `on interaction(openBtn)` with `<button #openBtn>` elsewhere in the same component template. The compiler resolves that name in the placeholder, then in the view holding the `@defer` and its parent views; a reference inside the deferred content itself, or in another component, is an error. Interaction listens for `click` and `keydown`, hover for pointer entry and `focusin`, and viewport uses a shared IntersectionObserver. Since v21 `on viewport({trigger: ref, rootMargin: '200px'})` passes observer options, all except `root`.

code

ts · 17 lines
ts
import { Component } from '@angular/core';
import { Reviews } from './reviews';

@Component({
  selector: 'app-product-page',
  imports: [Reviews],
  template: `
    <h2 #reviewsAnchor>Reviews</h2>

    @defer (on viewport({ trigger: reviewsAnchor, rootMargin: '300px' })) {
      <app-reviews />
    } @placeholder {
      <p>Reviews load as you scroll.</p>
    }
  `,
})
export class ProductPage {}

go deeper

for a junior

Know that a bare element trigger watches the placeholder, and that you can name another element with a template reference.

for a middle

Explain the one-root-element rule, where the compiler looks for a reference, and what events each trigger listens for.

for a senior

Use a sentinel and rootMargin to start loading before content scrolls in, and pair interaction with hover prefetch on the same element.

for a principal

Standardise trigger targets in shared layout components so teams do not fight compile errors from nested or cross-component references.

## Why some triggers need an element Three of the Angular **`@defer`** `on` triggers react to something happening to a DOM element: | Trigger | Listens for | Mechanism | |---|---|---| | `viewport` | the element intersecting the viewport | an `IntersectionObserver` shared by blocks with identical options | | `interaction` | `click` or `keydown` on the element | DOM event listeners | | `hover` | the pointer entering the element, or `focusin` | DOM event listeners | `hover` includes `focusin`, so keyboard users who tab to the element trigger it too. Every element trigger is removed as soon as the block triggers. ## Default target: the placeholder Without an argument, the trigger watches the **`@placeholder` block's root element**: ```html @defer (on viewport) { <app-reviews /> } @placeholder { <div class="reviews-skeleton"></div> } ``` Two compile-time rules apply: - A target-less `viewport`, `interaction` or `hover` needs a **`@placeholder` block**; otherwise the compiler reports that a trigger with no target can only be used on a `@defer` that has one. - That placeholder must have **exactly one root element node**. Two sibling roots or a text-only placeholder are rejected. ## Explicit target: a template reference variable Pass the name of a **template reference variable** to watch any element you choose: ```html <button #reviewsBtn type="button">Show reviews</button> @defer (on interaction(reviewsBtn)) { <app-reviews /> } ``` The compiler resolves the name in a specific order: 1. The root view of the `@placeholder` block, if there is one. 2. The view that contains the `@defer` block. 3. Each enclosing embedded view up to the component's root template (for example, a `@defer` inside an `@if` can reference an element outside that `@if`). If none has it, compilation fails with "Trigger cannot find reference". The reference must be: - in the **same component template** — not in a child component's template, not in a parent component; - **not inside the deferred main content**, which does not exist until the block triggers; - a reference to an **element**, not to a directive instance exported with `#x="dirName"`. ## Viewport options (v21) Since **v21**, `viewport` accepts an object literal with `IntersectionObserver` options. With options, the target moves into a **`trigger`** property; omit it to keep watching the placeholder: ```html @defer (on viewport({trigger: reviewsAnchor, rootMargin: '300px'})) { <app-reviews /> } ``` - All `IntersectionObserverInit` properties are allowed **except `root`**; the viewport is always the root. - `rootMargin` is the practical one: it starts loading before the element actually scrolls into view. - Blocks with identical options share one observer; different options get separate observers. ## Using the same element for prefetch The same reference can drive prefetch on a different event: ```html @defer (on interaction(reviewsBtn); prefetch on hover(reviewsBtn)) { <app-reviews /> } ``` Using the **same trigger and the same element** for both (`on viewport; prefetch on viewport`) is pointless; the `deferTriggerMisconfiguration` extended diagnostic (NG8021) flags it. ## Common mistakes - Wrapping the placeholder in two sibling elements, then wondering why `on viewport` does not compile. - Referencing an element inside a child component's template: references do not cross component boundaries. - Putting a collapsed, zero-height placeholder under `on viewport`: everything below it moves up, so the trigger point is not where the real content will sit, and the page jumps when the content arrives.

  • Can a @defer trigger reference an element inside a child component's template?
    No. Template references are scoped to one component template. The compiler looks in the placeholder, the view containing the `@defer` block and its enclosing views of the same template. To trigger on something inside a child, put the reference on the child's host element instead, or drive the block with a `when` condition fed by an output.
  • Why does on hover also fire for keyboard users?
    Angular's hover trigger listens for `focusin` as well as pointer events, so tabbing to the watched element counts as hovering it. That keeps a `prefetch on hover(btn)` useful for keyboard users, who would otherwise never trigger the prefetch before pressing Enter.

saying these in an interview costs you the question

  • on viewport watches the deferred content entering the viewport.
  • A placeholder with several root elements works; Angular watches the first one.
  • A trigger can reference an element inside a child component's template.
  • The viewport options object accepts root to observe a scroll container.
  • on interaction reacts to hover as well as clicks.