When an Angular template writes [label]="x" or label="New" on a component element, where does the value go, and what changed for data- attributes in v22?
answer
- inputs take precedence
- static attributes feed inputs too
- strings only from markup
- data- used to be stripped
- now a plain attribute
basics
~20 s[label]="x" sets every matching input on the element and skips the DOM property when an input matched; a static label="New" initialises the input with the string. Since Angular 22 a data-label attribute no longer binds the input; it stays a plain attribute.
solid answer
~40 sFor a property binding on a component or directive host, Angular first sets **all inputs** with that public name (including aliases) on the element's component and directives; only when **no** input matches does it fall back to the DOM property. A **static attribute** such as `label="New"` also initialises a matching input, but always with a **string**, so a numeric or boolean input needs a transform or a `[label]` binding. `[attr.label]` never reaches the input - it only writes the HTML attribute. Before v22 the template compiler stripped a leading `data-` from attribute names before matching, so `data-label="New"` could set the `label` input; **since v22** `data-`-prefixed attributes no longer bind inputs or outputs and remain ordinary attributes, useful for CSS hooks and test selectors.
code
ts · 24 linesimport { Component, input, numberAttribute } from '@angular/core';
@Component({
selector: 'app-badge',
template: `<span>{{ label() }} ({{ count() }})</span>`,
})
export class Badge {
readonly label = input('');
readonly count = input(0, { transform: numberAttribute });
}
@Component({
selector: 'app-inbox',
imports: [Badge],
template: `
<app-badge label="New" count="3" /> <!-- inputs get 'New' and 3 -->
<app-badge [label]="title" /> <!-- input, not the DOM property -->
<app-badge data-label="New" /> <!-- v22: plain attribute only -->
<app-badge [attr.label]="title" /> <!-- attribute only -->
`,
})
export class Inbox {
protected readonly title = 'Inbox';
}go deeper
Remember that [label] on a component sets its label input, and a static label="New" passes a string to it.
Explain the routing order (inputs first, DOM property only as fallback), why attr. never reaches inputs, and the v22 data- change.
Audit templates during a v22 upgrade for data- attributes that were silently feeding inputs, and fix tests that relied on them.
Keep a clear convention separating component inputs from test and styling hooks, so markup attributes are never ambiguous.
## One syntax, several possible targets On a plain element, `[title]="x"` has one target: the DOM property. On an element that hosts a **component** or carries **directives**, the same name may also be an **input**. Angular resolves this deterministically. ## How a property binding is routed For `[label]="expr"` on a component element: 1. The compiler collects the inputs of every component and directive matched on the element whose public name is `label` (an alias counts as the public name). 2. At runtime, when the value changes, Angular sets **each** matching input. If at least one matched, it **stops**: the DOM property of the same name is not written. (For a component host, the component is also marked for a check.) 3. Only if nothing matched does the value go to the DOM property, subject to the compile-time schema check that reports `NG8002` for unknown names. So `<app-badge [title]="t">` where `AppBadge` declares a `title` input sets the input and leaves the host element's native `title` property alone. If you want the tooltip too, the component must render it (for example with a host binding). ## Static attributes and inputs A plain attribute in markup, `<app-badge label="New">`, is **also** matched against inputs. Angular passes the attribute's text as the input's initial value: - the value is always a **string** (`count="3"` delivers `"3"`), so declare a transform such as `numberAttribute` or `booleanAttribute`, or use a property binding `[count]="3"`; - the attribute stays on the element as well, which matters for CSS selectors. ## What attr. does on a component element `[attr.label]="x"` targets the **attribute** only. It never sets an input, even when the component has one called `label`. This is sometimes used on purpose - to keep an attribute for styling or accessibility tooling - but it is a common source of "my input is never set" bugs. ## The v22 change for data- attributes Older Angular normalised attribute names by stripping a leading `data-` before interpreting them. As a result, markup such as `<app-badge data-label="New">` could set the `label` input, which surprised people who used `data-` attributes purely as hooks for CSS or end-to-end tests. Angular 22 removed that behaviour. The breaking-change note reads that `data-`-prefixed attributes no longer bind inputs nor outputs. Now: | Markup | Before v22 | v22 and later | |---|---|---| | `<app-badge data-label="New">` | could initialise the `label` input | plain `data-label` attribute only | | `<app-badge label="New">` | initialises `label` | initialises `label` | | `<app-badge [label]="text">` | binds `label` | binds `label` | | `<tr [attr.data-row-id]="id">` | attribute | attribute | **Migrating:** search templates for `data-` attributes on components and directives. Where the intent was to set an input, drop the prefix (`label="New"`) or use a binding (`[label]`). Where the intent was a test or CSS hook, nothing changes except that the component no longer receives the value. ## Debugging an input that never receives its value - Confirm the component or directive is actually **matched**: it must be in the host component's `imports` (or the NgModule's declarations) and its selector must match the element. - Check the **public name**: with an alias, the template must use the alias, not the class field name. - Look for an **`attr.`** prefix, which targets the attribute and bypasses the input. - Look for a **`data-`** prefix left over from pre-v22 markup. - Check the **type**: a static attribute delivers a string, which a strictly typed input may reject at compile time or misinterpret at runtime. ## Why it matters in interviews The question probes whether a candidate understands that Angular's templates are **compiled against known inputs and a DOM schema**, not interpreted loosely: the same attribute name can mean an input, a DOM property or a plain attribute depending on what is matched on the element and which syntax is used.
- Why does count="3" on a component deliver a string, and how do you get a number?Static attributes come from markup, so Angular passes their text unchanged to the matching input. Declare the input with a transform such as `numberAttribute`, which converts the string, or write `[count]="3"` so the expression evaluates to a number.
- After upgrading to v22, an e2e selector still finds <app-badge data-label="New">, but the badge renders empty; why?The `data-label` attribute is still in the DOM, so the selector matches, but since v22 Angular no longer strips the `data-` prefix to set the `label` input. The input keeps its default. Write `label="New"` for the input and keep a separate `data-` hook if the test needs one.
saying these in an interview costs you the question
- A property binding sets both the input and the DOM property
- [attr.label] reaches the component's label input
- Static attributes deliver numbers when the input is typed number
- In v22, data-label still initialises a label input
- Angular picks the DOM property first and inputs second