You have just migrated an existing component to "Standalone" mode, or perhaps you are spinning up a fresh component in Angular 17+. You add a simple input field, bind it with [(ngModel)], and your application crashes.
The console throws the infamous error:
Can't bind to 'ngModel' since it isn't a known property of 'input'.
This error is the most common friction point for developers adopting the Standalone Component architecture. It occurs because the mental model for dependency management has shifted from implicit (Module-based) to explicit (Component-based).
Here is the immediate technical solution, followed by the architectural root cause and edge cases regarding ReactiveFormsModule and CommonModule.
The Immediate Fix
To resolve this error, you must explicitly import FormsModule into the imports array of your standalone component's metadata.
In the legacy NgModule architecture, you likely imported FormsModule once in app.module.ts, making it available globally. Standalone components do not look at app.module.ts. They are self-contained contexts.
The Broken Code
This component throws the error because it attempts to use the ngModel directive without declaring where that directive comes from.
import { Component } from '@angular/core';
@Component({
selector: 'app-user-search',
standalone: true,
// MISSING IMPORTS
template: `
<div class="search-container">
<input type="text" [(ngModel)]="searchQuery" placeholder="Search users..." />
<p>Searching for: {{ searchQuery }}</p>
</div>
`
})
export class UserSearchComponent {
searchQuery = '';
}
The Working Solution
Import FormsModule from @angular/forms and add it to the imports array inside the @Component decorator.
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms'; // 1. Import the module
@Component({
selector: 'app-user-search',
standalone: true,
imports: [FormsModule], // 2. Register it here
template: `
<div class="search-container">
<input type="text" [(ngModel)]="searchQuery" placeholder="Search users..." />
<p>Searching for: {{ searchQuery }}</p>
</div>
`
})
export class UserSearchComponent {
searchQuery = '';
}
Root Cause Analysis: Directives and Scope
To master Angular, one must understand why this happens. The error message "Can't bind to ngModel..." is technically precise.
Angular's compiler scans your HTML template. When it sees [(ngModel)], it looks for a directive selector matching ngModel.
The Shift from NgModule to Standalone
In the classic architecture (NgModule), dependencies were often "bucketed." If you imported FormsModule in a Shared Module, every component declared in that module inherited access to ngModel.
Standalone Components invert this model.
A standalone component is a sandbox. It knows nothing about the rest of your application unless explicitly told. This design pattern, known as the SCAM pattern (Single Component Angular Module) before it was official, facilitates:
- Tree Shaking: Build tools can eliminate unused code more effectively because dependencies are strictly mapped.
- Lazy Loading: Components can be loaded on demand without complex module scaffolding.
- Unit Testing: Tests become faster and less flaky because you test the component in isolation without dragging in a massive parent module.
Because your component is now a sandbox, the compiler does not recognize the ngModel selector until FormsModule (which exports the NgModel directive) is bridged into the component via the imports array.
Scenario 2: Reactive Forms
A common variation of this error occurs when developers switch to [formControl] or formGroup but apply the wrong fix.
If you are using Model-Driven forms (Reactive Forms), importing FormsModule will not solve your problem. You must import ReactiveFormsModule.
The Reactive Solution
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormControl } from '@angular/forms';
@Component({
selector: 'app-newsletter-signup',
standalone: true,
imports: [ReactiveFormsModule], // Required for [formControl]
template: `
<input [formControl]="emailControl" placeholder="Enter email" />
`
})
export class NewsletterSignupComponent {
emailControl = new FormControl('');
}
If you use both Template-Driven features (simple ngModel) and Reactive features in the same component, you must import both modules.
Scenario 3: The CommonModule Trap
While ngModel is the most frequent stumbling block, CommonModule is the second.
In older Angular versions, if you forgot CommonModule, structural directives like *ngIf and *ngFor would fail.
Modern Angular Control Flow
If you are on Angular 17+, you should be using the new built-in control flow syntax (@if, @for). These are built into the template engine and do not require CommonModule.
However, you still need CommonModule (or specific pipe imports) if you use pipes like async, json, or date.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; // Required for AsyncPipe
@Component({
selector: 'app-data-display',
standalone: true,
imports: [CommonModule],
template: `
@if (data$ | async; as data) {
<!-- @if works natively, but | async needs CommonModule -->
<div>{{ data.title }}</div>
}
`
})
export class DataDisplayComponent {
// ... logic
}
To optimize bundle size further, you can import specific pipes instead of the entire CommonModule:
import { AsyncPipe, DatePipe } from '@angular/common';
@Component({
// ...
imports: [AsyncPipe, DatePipe] // Highly tree-shakable
})
Unit Testing Standalone Components
Migrating to standalone components also changes how you configure your tests. When testing a component that uses ngModel, you no longer declare the component; you import it.
Because the component is standalone, it carries its own dependencies (imports: [FormsModule]) with it. This simplifies the TestBed setup significantly.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserSearchComponent } from './user-search.component';
describe('UserSearchComponent', () => {
let component: UserSearchComponent;
let fixture: ComponentFixture<UserSearchComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
// We IMPORT the component instead of declaring it.
// Because the component imports FormsModule internally,
// we do not need to import FormsModule here manually.
imports: [UserSearchComponent]
}).compileComponents();
fixture = TestBed.createComponent(UserSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Summary
The "Can't bind to ngModel" error is a safeguard, not a bug. It enforces the standalone architecture's rule that dependencies must be explicit.
- Template-Driven Forms: Import
FormsModule. - Reactive Forms: Import
ReactiveFormsModule. - Pipes: Import
CommonModuleor the specific pipe (e.g.,AsyncPipe).
By explicitly defining your imports, you create components that are modular, easier to test, and future-proof against the evolving Angular ecosystem.