skip to content

In Angular templates, why can a #ref declared inside an @if or @for block not be read from outside that block?

level: middleimportance: should knowfreq 40%

answer

  1. blocks create embedded views
  2. lookup walks upward only
  3. each @for row has its own
  4. inner can read outer
  5. lift it or use a query

basics

~20 s

Angular's @if, @for, @defer and ng-template bodies are separate embedded views; a #ref declared inside one is visible there and in views nested deeper, but name lookup only walks outward, so the parent template cannot see it.

solid answer

~50 s

Angular resolves template names by scope, and every construct that may create or destroy content, such as `@if`, `@for`, `@switch`, `@defer`, `<ng-template>` and structural directives, has its own embedded view and so its own scope. Name lookup starts in the current view and walks up to parents, never down. A `#ref` declared outside a block is readable inside it; one declared inside is invisible to the parent, which is also correct at runtime because the element may not exist, or may exist many times. Inside `@for` each row gets its own instance of the reference, pointing at that row's element. Plain elements such as `<div>` do not create a view, so a ref on a nested `<div>` is visible to the whole view. To use an inner element from outside, move the reference out, or use a query in the class.

code

html · 10 lines
html
<input #query placeholder="Filter" />

@if (showAdvanced()) {
  <input #minPrice type="number" />
  <!-- valid: outer ref read in an inner view -->
  <button (click)="apply(query.value, minPrice.value)">Apply</button>
}

<!-- invalid: minPrice is declared inside the @if view -->
<button (click)="reset(minPrice)">Reset</button>

go deeper

for a junior

Remember that refs declared inside @if or @for stay inside, while refs from outside can be used inside.

for a middle

Explain that blocks and ng-template create embedded views, that lookup walks outward only, and that each @for row has its own reference instance.

for a senior

Choose between moving a ref outward, a view query and passing values into signals, and handle the absent case when content is conditional.

for a principal

Encourage templates where state flows through signals rather than cross-block element access, so conditional structure can change without breaking references.

## Views are the unit of scope An Angular template compiles into **views**. The component's template is one view, and every place where content may appear, disappear or repeat becomes a separate **embedded view** nested inside it: - `@if` / `@else` branches and `@switch` cases; - each row of a `@for`, plus its `@empty` block; - `@defer` and its placeholder, loading and error blocks; - `<ng-template>` and any structural directive (`*ngIf`, `*ngFor`, custom ones). Plain HTML elements do **not** create views. A `<div>` or `<section>` is just a node inside the current view. Template names (`#ref` references, `@let` declarations, `let-x` variables, the `@for` item and its contextual variables) belong to the view that declares them. Resolving a name works like lexical scope in JavaScript: look in the current view, then its parent, then the grandparent, up to the component's template, and finally the component class. ## What that means for #ref | Where the ref is declared | Readable from | Not readable from | |---|---|---| | top level of the template | everywhere in the template, including nested blocks | (nothing inside the template is excluded) | | inside a `<div>` at top level | the whole top-level view, since elements create no view | (same as top level) | | inside an `@if` branch | that branch and blocks nested in it | the parent template, the `@else` branch | | inside a `@for` row | that row, one instance per row | the parent template, other rows | Two consequences follow from the table: 1. **Outer to inner works.** `<input #query>` at top level can be used in a `@for` row's handler: `(click)="pick(item, query.value)"`. 2. **Inner to outer does not.** A `<input #code>` inside `@if (showCode())` cannot be read by a button outside the `@if`. With template type checking on, the name falls through to the component and is reported as a property the component does not have; the runtime could not honour it anyway, because when the condition is false the input does not exist. ## Why this is the right rule The scope follows the **lifetime** of the content: - an `@if` body can be created and destroyed on every check, so a parent holding a reference into it would point at a destroyed node; - a `@for` body exists once per item, so there is no single element a parent reference could mean; - a `<ng-template>` might be rendered zero, one or many times in other places. Letting inner names escape would make the parent's expressions depend on views that may not exist. ## Each @for row gets its own reference Inside a loop, a reference is declared once in source but instantiated per row: ```html @for (field of fields(); track field.id) { <label> {{ field.label }} <input #box [value]="field.value" /> <button type="button" (click)="box.select()">Select</button> </label> } ``` Each button selects the input in **its own row**: `box` in row 3 is row 3's input. There is no way to address "the `box` of row 5" from outside the loop by name. ## Getting at an inner element from outside When the outside really needs an inner element: - **Move the reference outward** if the element can live outside the block. - **Use a view query** in the class, such as `viewChild('code')` or `viewChildren('box')`. Queries follow the view tree and update as blocks are created and destroyed, returning `undefined` or an empty list when the content is absent. - **Pass the value, not the element**: a handler inside the block can call a component method with the value, which then lives in a signal. ## Scope for other template names The same view scoping applies to `@let`, with one addition: `@let` also cannot be read **before** its declaration in the same view, while a `#ref` can, because references are resolved for the whole view.

  • In Angular, is a #ref on an element nested inside a plain <div> visible to the rest of the template?
    Yes. Plain elements create no embedded view, so a reference on an element inside a `<div>` belongs to the same view as the `<div>` and is visible throughout that view, including in blocks nested inside it. Only views, such as `@if`, `@for`, `@defer` or `<ng-template>`, start a new scope.
  • How does an Angular component class reach an element that only exists inside an @if block?
    Use a view query such as `viewChild('code')`. Queries track the view tree, so the result is the element while the branch is rendered and `undefined` while it is not. Code reading it must handle the absent case, which is exactly why the parent template is not allowed to name it directly.

saying these in an interview costs you the question

  • A #ref anywhere in the template is global to the whole template
  • Wrapping an element in a <div> hides its #ref from siblings
  • Inside @for, a #ref always points at the first row's element
  • An inner view cannot read refs declared in the parent template
  • Adding ? like ref?.value makes an inner ref readable from outside