ng serve or ng build stops with:
✘ [ERROR] NG8002: Can't bind to 'ngModel' since it isn't a known property of 'input'. [plugin angular-compiler]
src/app/profile-form.ts:9:11:
9 │ <input [(ngModel)]="name" />
╵ ~~~~~~~~~~~~~~~~~~The template compiler found a property binding, [ngModel] (which [(ngModel)] expands to), and could not match it to any directive input on that element or to any native property of <input>. In almost every case the directive exists but is not in the component’s compilation scope: FormsModule was never imported where this template is compiled. That output is from Angular 20; Angular 19 prints the same message.
Quick fix checklist
- Standalone component (the default since Angular 19): add
FormsModuleto that component’simportsarray. - Component declared in an NgModule (
standalone: false): addFormsModuleto theimportsof the module that declares it, not justAppModule. - Error mentions
formControl,formGrouporformControlName: importReactiveFormsModuleinstead. - Error names your own component (
... of 'app-avatar'): check the component is imported, then check the input’s real name, including any alias. - Native attribute such as
colspan: bind with[attr.colspan]. - In unit tests of NgModule components the same problem appears at runtime as NG0303; add the module to the test’s
imports.
Before you start
You need to know which compilation scope your component lives in. Open the component file: if @Component has no standalone: false, it is standalone and its template sees only what its own imports array lists. If it says standalone: false, find the @NgModule whose declarations include it. The standalone imports note explains this scope in more detail.
You should also know which forms API you meant to use. [(ngModel)] is template-driven forms (FormsModule); [formControl] and [formGroup] are reactive forms (ReactiveFormsModule). If you are unsure which to pick, see template-driven vs reactive forms.
Why it happens
At build time, the Angular compiler resolves each [name]="expr" binding in this order:
- Is there a directive or component on this element, from the template’s scope, with an input called
name? If so, bind to that input. - Otherwise, is
namea known DOM property of this element according to Angular’s DOM schema? If so, set that property. - Otherwise, report NG8002.
ngModel is not a DOM property. It is an input of the NgModel directive, whose selector is [ngModel]:not([formControlName]):not([formControl]). That directive is exported by FormsModule. If FormsModule is not in scope, step 1 finds nothing, step 2 finds nothing, and you get NG8002. The same logic explains the variants:
[formControl]on<input>meansReactiveFormsModuleis missing (verified:Can't bind to 'formControl' since it isn't a known property of 'input').[formGroup]on<form>is the same module.[foo]on<div>means there is no such property and no directive: a plain typo.[colspan]on<td>fails because the DOM property iscolSpan; bind the attribute with[attr.colspan]. Likewise[aria-label]fails on Angular 19 (Angular 20 accepts ARIA bindings directly), and[attr.aria-label]works on both.
When the element is one of your own components, the message adds numbered hints because Angular cannot tell whether you forgot the import or mistyped the input:
✘ [ERROR] NG8002: Can't bind to 'userName' since it isn't a known property of 'app-avatar'.
1. If 'app-avatar' is an Angular component and it has 'userName' input, then verify that it is included in the '@Component.imports' of this component.
2. If 'app-avatar' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message.
3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@Component.schemas' of this component.Step-by-step walkthrough
Step 1: Locate the binding and the element
The error gives the file, line and column, and underlines the binding. Note two things: the property name (ngModel) and the element (input). The element tells you whether it is a native element (look for a missing directive module) or a custom element (look at that component’s inputs).
Step 2: Find the owning scope
Open the component class for that template. For a standalone component:
import { Component } from '@angular/core';
@Component({
selector: 'app-profile-form',
imports: [], // nothing here provides ngModel
template: `<input [(ngModel)]="name" />`,
})
export class ProfileForm {
name = 'Ada';
}For an NgModule component, find the module with this class in declarations. A very common trap: FormsModule is imported in AppModule, but the component is declared in SharedModule or a lazy feature module. Module imports are not inherited; each declaring module needs its own.
Step 3: Import the right module
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-profile-form',
imports: [FormsModule],
template: `<input [(ngModel)]="name" />`,
})
export class ProfileForm {
name = 'Ada';
}NgModule equivalent:
@NgModule({
declarations: [ProfileForm],
imports: [CommonModule, FormsModule],
exports: [ProfileForm],
})
export class ProfileModule {}For reactive forms, import ReactiveFormsModule and bind a real control:
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-search-box',
imports: [ReactiveFormsModule],
template: `<input [formControl]="query" />`,
})
export class SearchBox {
readonly query = new FormControl('', { nonNullable: true });
}Step 4: Check custom component inputs
If the element is your component and it is already imported, the input name is wrong. With an alias, the template must use the alias, not the class property:
import { Component, Input, input } from '@angular/core';
@Component({ selector: 'app-avatar', template: `{{ size() }} {{ userName }}` })
export class Avatar {
readonly size = input(32); // bind as [size]
@Input('user') userName = ''; // bind as [user], NOT [userName]
}Tested in Angular 19 and 20: <app-avatar [user]="'a'" /> compiles, <app-avatar [userName]="'a'" /> fails with NG8002 even though Avatar is imported. A plain field without @Input() or input() also fails, since only declared inputs are bindable.
Worked scenario
A team migrates a sign-up page to standalone with the schematic, then adds a newsletter checkbox by hand.
@Component({
selector: 'app-signup',
imports: [RouterLink],
template: `
<form (ngSubmit)="save()">
<input [(ngModel)]="email" />
<label><input type="checkbox" [(ngModel)]="newsletter" /> Newsletter</label>
<button>Sign up</button>
</form>
`,
})
export class Signup {
email = '';
newsletter = false;
save() {}
}The build reports NG8002 twice, one for each ngModel. Diagnosis: the component used to be declared in AuthModule, which imported FormsModule, so ngModel worked through the module’s scope. When it was converted to standalone by hand, only RouterLink was copied into imports. A standalone component sees nothing from its old module.
Adding FormsModule fixes the build, and then the browser shows the next error:
NG01352: If ngModel is used within a form tag, either the name attribute must be set or the form
control must be defined as 'standalone' in ngModelOptions.Inside a <form>, NgForm registers every ngModel by name. The final version:
@Component({
selector: 'app-signup',
imports: [RouterLink, FormsModule],
template: `
<form (ngSubmit)="save()">
<input name="email" [(ngModel)]="email" />
<label><input type="checkbox" name="newsletter" [(ngModel)]="newsletter" /> Newsletter</label>
<button>Sign up</button>
</form>
`,
})
export class Signup {
email = '';
newsletter = false;
save() {}
}Common mistake
Adding NO_ERRORS_SCHEMA or CUSTOM_ELEMENTS_SCHEMA. NO_ERRORS_SCHEMA makes the compiler accept any property on any element, so [ngModel] compiles into a DOM property write that does nothing: no two-way binding, no validation, no error. CUSTOM_ELEMENTS_SCHEMA only relaxes elements with a dash in the name, so it does not even silence the <input> case. Both hide real typos everywhere else in the template.
Importing FormsModule through importProvidersFrom(FormsModule) in app.config.ts. That registers providers, but template directives are resolved per component scope. Each standalone component that uses ngModel must list FormsModule in its own imports.
Importing FormsModule when you meant reactive forms. [formControl] needs ReactiveFormsModule. Importing both is legal, but mixing ngModel and formControl on the same input is deprecated and confusing.
Verify the behavior
First, ng build should complete without NG8002. Then prove the two-way binding actually works, because a schema hack would also make the build pass:
import { TestBed } from '@angular/core/testing';
describe('ProfileForm', () => {
it('writes typed text back to the component', async () => {
const fixture = TestBed.createComponent(ProfileForm);
fixture.detectChanges();
await fixture.whenStable();
const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
expect(input.value).toBe('Ada');
input.value = 'Grace';
input.dispatchEvent(new Event('input'));
expect(fixture.componentInstance.name).toBe('Grace');
});
});whenStable() matters because NgModel writes its initial value asynchronously. The value assertions matter even more, because tests are JIT-compiled by default and report this problem at runtime as NG0303 rather than failing the build. With FormsModule removed from this standalone component, Angular 20 under ng test only logs it:
ERROR: 'NG0303: Can't bind to 'ngModel' since it isn't a known property of 'input' (used in the 'ProfileForm' component template).and the test fails solely on Expected '' to be 'Ada'. Components declared through TestBed declarations throw the same NG0303 instead when the setup enables errorOnUnknownProperties (CLI-generated test setups do; older hand-written test.ts files usually do not). Either way, an assertion on real behavior is what catches it reliably.
Interview exercise
“Why does Angular need a module import for [(ngModel)], when [value] on the same input works without any import?”
Answer and reasoning
[value] binds to a real DOM property of HTMLInputElement, which Angular’s DOM schema knows about, so the compiler can emit a direct property write. ngModel is not a DOM property; it is an input of the NgModel directive. The compiler only matches directives that are in the component’s compilation scope (its imports, or the declaring NgModule’s imports and exports), so the directive must be imported before the selector [ngModel] can match.
A strong answer adds why Angular works this way: explicit scopes let the compiler type-check bindings and tree-shake unused directives, and they make each standalone component self-describing. It also explains what the directive does: [(ngModel)] is sugar for [ngModel]="x" (ngModelChange)="x = $event", and NgModel uses a ControlValueAccessor to read and write the input, which is why it also works on custom form controls.