Why does <input [(ngModel)]="name"> in an Angular standalone component fail with "Can't bind to 'ngModel'", and what does the binding expand to?
answer
- ngModel is a directive, not a DOM property
- the component's imports array
- @angular/forms
- ngModel plus ngModelChange
basics
~20 sngModel is an input of the NgModel directive from FormsModule, not a property of <input>. Without FormsModule in the component's imports nothing claims it, so the compiler reports NG8002. With it, the binding is [ngModel] plus (ngModelChange).
solid answer
~40 s`[(ngModel)]` is ordinary two-way sugar: it expands to `[ngModel]="name"` plus `(ngModelChange)="name = $event"`. Neither name exists on a native `<input>`; both belong to the `NgModel` directive, whose selector matches `[ngModel]` and which is exported by `FormsModule` from `@angular/forms`. In a standalone component, a directive only matches if it is in that component's `imports`, so without `FormsModule` the compiler finds no element property or directive input called `ngModel` and fails with NG8002, *Can't bind to 'ngModel' since it isn't a known property of 'input'*. Add `FormsModule` to `imports` (or to the declaring NgModule's imports in older code). `NgModel` then writes the value into the control and emits `ngModelChange` on user input. The bound target can be a plain field or a `WritableSignal`.
code
ts · 16 linesimport { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-station-search',
imports: [FormsModule],
template: `
<input name="q" [(ngModel)]="query" />
<!-- longhand of the same binding -->
<input name="q2" [ngModel]="query()" (ngModelChange)="query.set($event)" />
`,
})
export class StationSearch {
query = signal('');
}go deeper
Recall that ngModel comes from FormsModule and must be in the component's imports, and name the two halves.
Explain directive matching per compilation scope and why the error names input as the element.
Show how splitting the binding into [ngModel] and (ngModelChange) solves transformation and ordering problems.
Place [(ngModel)] against other form approaches when setting conventions for a codebase, without mixing binding styles on one control.
## Why the error appears An Angular template can bind to two kinds of names on an element: - a **native DOM property**, such as `value` or `disabled` on `<input>`, or - an **input of a component or directive** that matched the element. `[(ngModel)]` expands to `[ngModel]="name"` and `(ngModelChange)="name = $event"`. There is no DOM property called `ngModel`, so the property half only works if a directive with an `ngModel` input matches the element. That directive is **`NgModel`**, and its selector includes `[ngModel]`. Directives are matched **per compilation scope**: | Code style | Where `FormsModule` must be listed | |---|---| | Standalone component (the default since v19) | the component's own `imports` array | | Component declared in an NgModule | that NgModule's `imports` | If `FormsModule` is missing, no directive claims `ngModel`, and the compiler reports **NG8002**: *Can't bind to 'ngModel' since it isn't a known property of 'input'.* The same error appears for any misspelt or unimported input, which is why the fix is to check the imports first. ## The fix ```ts import { Component, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; @Component({ selector: 'app-profile-name', imports: [FormsModule], template: ` <label>Name <input name="name" [(ngModel)]="name" /></label> <p>Hello, {{ name() }}</p> `, }) export class ProfileName { name = signal('Ada'); } ``` `FormsModule` is the usual import because it brings `NgModel` together with the value-accessor directives that connect it to native inputs, checkboxes, selects and radio buttons. ## What the expansion does at runtime 1. The **property half** `[ngModel]` passes the current value into `NgModel`'s `ngModel` input. `NgModel` writes it into the native control through a value accessor. 2. When the user edits the field, the value accessor tells `NgModel`, which emits its **`ngModelChange`** output with the new value. 3. The **event half** assigns that value back: `name = $event` for a plain field, or `name.set($event)` when `name` is a `WritableSignal`. So `[(ngModel)]` follows exactly the same `x` / `xChange` convention as a component's own two-way binding. Nothing about it is special apart from the directive that implements it. Two consequences follow from the expansion: - You can split it: `[ngModel]="name()" (ngModelChange)="onName($event)"` is a common way to transform or validate a value before storing it. - Any extra `(ngModelChange)` handler on the same element runs in **template order** relative to the two-way listener, so a handler written before `[(ngModel)]` sees the old value of the bound field. ## When to split the binding The shorthand is right when the component simply stores what the user typed. Split it into its halves when something has to happen between the control and the state: ```html <input name="code" [ngModel]="code()" (ngModelChange)="code.set($event.toUpperCase())" /> ``` Here the stored value is normalised on the way in, and because the property half pushes the stored value back on the next pass, the control usually shows the normalised text too. The exception is worth knowing: if normalising produces the same value that was pushed last time (trimming a trailing space, for example), the property binding sees no change, and the control keeps the text exactly as typed. The same pattern covers parsing a number, clamping a range or ignoring a value while a request is pending. The split form is also the easiest way to see, in a code review, exactly what the event half does, which is why some teams prefer it whenever the handler is more than an assignment. ## Signals and plain fields Since v17.2 the bound expression may be a `WritableSignal`: the template passes the signal itself (`[(ngModel)]="name"`, not `name()`), the property half reads it, and the event half calls `set()`. A plain field (`name = 'Ada'`) also works. Which of these to prefer, and how `NgModel` fits into template-driven forms with validation and `NgForm`, belongs to forms rather than to the binding syntax. ## Checklist when the error appears - `FormsModule` is in the standalone component's `imports` (not only in a parent's). - The attribute is spelt `[(ngModel)]`, with a capital M and the banana inside the box. - The element really is a form control or a custom control with a value accessor. - In NgModule code, the component is declared in the module that imports `FormsModule`.
- In an Angular component, how do you trim a value before storing it while still using ngModel?Split the two-way binding into its halves: `[ngModel]="name()" (ngModelChange)="name.set($event.trim())"`. The property half still keeps the control in sync, and the event half now runs your statement instead of a plain assignment.
- Why does adding FormsModule to the parent component not fix NG8002 in a standalone child in Angular?Standalone components compile their templates against their own `imports` only. A directive imported by the parent is not visible inside the child's template, so the child must list `FormsModule` itself.
saying these in an interview costs you the question
- ngModel is a built-in property of HTML input elements
- Importing FormsModule once in the root component makes it available everywhere
- [(ngModel)] uses a different mechanism from component two-way binding
- [(ngModel)] requires the bound value to be a plain field, not a signal
- The fix for NG8002 is adding CUSTOM_ELEMENTS_SCHEMA