Can't Bind to ngModel: Angular FormsModule Fix
Import FormsModule in the owning component or module.
20+ years shipping production backend systems. Lessons pulled from things that broke in production.
- ✓Basic Angular templates and binding
- ✓Simple TypeScript class properties
- ✓An Angular CLI project to edit
- [(ngModel)] is a directive from FormsModule, not native HTML, so Angular rejects the binding until that module is imported
- Standalone components add FormsModule to their own imports array; NgModule apps add it to the owning module's imports
- Lazy feature modules need their own FormsModule import because they never inherit the root module's imports
- Inputs inside a
Picture a universal remote with a Netflix button that does nothing until you program it. ngModel is that button: the syntax sits on your input, but the behavior lives in FormsModule, which you must import to program it. Standalone components each own their own remote, so every form component needs its own programming step. One import line, and the button starts working everywhere in that template.
You add [(ngModel)]="username" to an input, save, and Angular answers with Can't bind to 'ngModel' since it isn't a known property of 'input'. The syntax looks exactly like the tutorial. The property exists on your class. Yet the compiler insists ngModel is a stranger.
The confusion is natural because ngModel looks like a native HTML attribute, but it isn't one. It's a directive shipped in FormsModule, and Angular only recognizes directives that are imported into the component or module owning your template. No import means no directive, and no directive means the binding matches nothing the compiler knows.
Standalone components made this both simpler and easier to miss. There's no shared AppModule carrying FormsModule for everyone anymore; each standalone component lists its own imports. A form that worked in a module app breaks the moment you convert it without carrying FormsModule along.
This guide fixes it fast and permanently. You'll learn which import each app style needs, why lazy modules need their own copy, what the follow-up name-attribute error means, and when a custom component genuinely needs a ControlValueAccessor bridge versus a plain @Input().
Why ngModel Needs FormsModule to Exist
The [(ngModel)] syntax is shorthand for a property binding plus an event binding, and both halves are delivered by the NgModel directive declared in FormsModule. Native inputs expose value and input events; NgModel wraps them into the two-way contract your template expects. When FormsModule isn't imported, the compiler sees an attribute it can't match to any directive or native property, and NG8002 is its honest report.
Standalone components declare their directive dependencies in the imports array, which replaced the old NgModule declarations-plus-imports dance. Adding FormsModule there scopes the directive to that component's template only. Sibling components don't inherit it, which surprises developers coming from a shared-module world where one import covered dozens of templates. The scoping is deliberate: templates declare exactly what they use, and unused directives never ship.
Verify the fix by checking the error disappears on save without touching the binding itself. If it persists, you edited the wrong owner: the component whose template holds the input is the only imports array that matters. A parent's imports never flow downward. This single rule, owner imports win, resolves the majority of ngModel reports you'll ever see.
FormsModule in NgModule vs Standalone Apps
NgModule apps and standalone apps answer the same question, where do directives come from, in different places. In a module app, the declaring module's imports array supplies directives to every template it declares. In a standalone app, each component's imports array supplies directives to its own template. Both are per-owner registrations; neither inherits across boundaries you didn't wire.
Lazy loading draws the sharpest boundary. A lazy feature module gets its own injector and its own compilation scope, so AppModule's FormsModule import is invisible inside it. The symptom is maddening: ngModel works on the home page and fails on the settings page with identical syntax. The fix is a second FormsModule import in the lazy module, which feels redundant until you see modules as sealed scopes rather than a global pool.
Shared modules are the sanctioned way to stop repeating yourself: create a SharedModule that imports and exports FormsModule, then import SharedModule wherever forms appear. Don't over-share, though. Importing every forms API into components that render static text bloats bundles and hides which templates actually bind. Import at the owner, share deliberately, and the error stays away.
The name Attribute Follow-Up Error Inside Forms
Fixing the import often reveals a second error: ngModel inside a form tag needs a name attribute or the standalone option. Template-driven forms build an implicit FormGroup from named inputs, and an unnamed input can't join it. The name attribute is the input's identity in that group; without it, Angular can't track validity or aggregate values on submit.
The standalone option is the escape hatch for inputs that live inside a form tag but shouldn't join the form: search hints, UI-only toggles, draft fields. Marking them standalone keeps their two-way binding working while excluding them from validation and submission values. Use it sparingly. A form where half the inputs are standalone confuses every developer who reads the submit handler, because the model and the form silently disagree.
Expect this error ordering and don't fight it. The compiler reports the unknown property first because without the directive it can't evaluate anything else. Import FormsModule, then add names, then handle custom components. Each fix unlocks the next check. Teams that chase the name error before the import waste time editing templates the compiler hasn't actually validated yet. Patience with the ordering pays off on every future form you'll build.
Custom Components and the ControlValueAccessor Bridge
Custom components hit the same NG8002 error for a different reason: even with FormsModule imported, ngModel has no idea how to read or write your component's value. Native inputs expose a standard value contract; your star-rating widget doesn't. ControlValueAccessor is the bridge interface that teaches ngModel your component's language through writeValue, registerOnChange, and registerOnTouched, plus an NG_VALUE_ACCESSOR registration.
Only build this bridge for genuinely reusable inputs: date pickers, rich selects, rating widgets used across features. For one-off components, plain @Input() and @Output() bindings are simpler, need no forms imports, and keep the data flow visible. Teams burn days writing accessors for components that appear on exactly one page, then maintain that bridge forever for zero reuse benefit.
The most common accessor bug is forgetting to call the registered onChange callback when the internal value changes. The UI updates but ngModel never hears about it, so the parent model goes stale while the widget looks alive. Always call onChange and onTouched at every user interaction point. A quick test that writes a value through ngModel and reads it back catches this before users do.
ReactiveFormsModule Won't Fix an ngModel Error
ReactiveFormsModule is the wrong fix for an ngModel error, and reaching for it is the most common misstep after the first import fails. It registers formControlName, formGroup, and the reactive model classes, but not the NgModel directive. The compiler error survives, developers add more imports at random, and the template accumulates modules it never uses.
Each forms style owns distinct syntax. Template-driven uses [(ngModel)] with FormsModule and keeps logic in the HTML. Reactive uses formControlName with ReactiveFormsModule and builds the model in TypeScript. You may import both modules when different inputs use different styles, but never combine both syntaxes on a single input: ngModel with formControlName is deprecated and produces double-registration warnings with values that fight.
Choose per form, not per input. Simple forms with light validation stay template-driven and readable. Dynamic forms with nested groups, conditional validators, or heavy unit testing go reactive, where the FormGroup model is trivially testable without rendering. Write the choice in the component's top comment so the next developer doesn't blend styles out of habit. A visible style decision keeps every future input consistent and every template review short.
When the Error Survives the Obvious Fix
When the error persists after what looks like the right fix, interrogate three suspects in order. First, ownership: confirm the file you edited actually owns the template holding the binding, since parent imports never flow down and lazy boundaries seal scopes. Second, spelling: ngModel is case-sensitive, and ngmodel or ng-model in lowercase matches nothing. Third, element kind: a custom component needs the accessor bridge, while a native input never does.
A fast diagnostic loop beats guessing. Run grep for FormsModule across the feature to map who imports what, open the owning component's imports array, and compare against the old module's imports if this code was recently converted. Most stubborn cases resolve to editing a parent's imports while the child template starves, or converting to standalone without carrying FormsModule along.
Document the outcome where the next person will look. A one-line comment above the imports array, forms inputs below need FormsModule, saves the next developer the same twenty minutes. Better still, add a spec that renders the real template: mocked-template specs never compile bindings, so they sail past exactly the errors users hit. Real templates in specs turn this runtime surprise into a compile-time pin.
The Standalone Conversion That Took Down Login for 52 Minutes
- Standalone conversion must redistribute every old module import to the new components; deleting the module file deletes the directives its templates relied on.
- Mocked component specs can't catch template errors, so at least one spec per form must compile the real template with real imports.
- Fix compiler errors in reported order: the missing import hides the name-attribute error, and chasing the second error first wastes the whole incident.
| File | Command / Code | Purpose |
|---|---|---|
| src | @Component({ | Why ngModel Needs FormsModule to Exist |
| src | @NgModule({ | FormsModule in NgModule vs Standalone Apps |
| src | @Component({ | The name Attribute Follow-Up Error Inside Forms |
| src | ControlValueAccessor, | Custom Components and the ControlValueAccessor Bridge |
| src | @Component({ | ReactiveFormsModule Won't Fix an ngModel Error |
Key takeaways
Common mistakes to avoid
5 patternsImporting FormsModule in AppModule but using ngModel in a lazy feature
Using ngModel inside a form tag without a name attribute
Mixing [(ngModel)] with formControlName on the same input
Implementing ControlValueAccessor to fix a plain input
Importing ReactiveFormsModule and expecting ngModel to work
Interview Questions on This Topic
Why does [(ngModel)] need FormsModule imported?
Frequently Asked Questions
20+ years shipping production backend systems. Lessons pulled from things that broke in production.
That's Angular. Mark it forged?
5 min read · try the examples if you haven't