An Angular template-driven sign-up form moves its address inputs into a child component, and they stop affecting the parent form's value and validity. Why, and how do you fix it?
answer
- @Host() stops at the component boundary
- the child's controls act standalone
- viewProviders with ControlContainer
- useExisting NgForm
- a dev-mode warning since 22.1.3
basics
~20 sNgModel looks for its parent ControlContainer with @Host(), which stops at the child component's boundary, so the child's inputs run as standalone controls. Add viewProviders: [{provide: ControlContainer, useExisting: NgForm}] to the child, or make it a proper custom form control.
solid answer
~40 s`NgModel` injects its parent `ControlContainer` (the `NgForm` or an `ngModelGroup`) with `@Optional() @Host()`. `@Host()` limits the search to the current component's template, so an `ngModel` inside `<app-address>` cannot see the `<form>` in the parent's template. Finding no parent, it behaves as a standalone control: no registration, no contribution to `f.value` or `f.valid`, and no error. Since 22.1.3 development builds log warning `NG01354` for this case. The usual fix is `viewProviders: [{provide: ControlContainer, useExisting: NgForm}]` on the child, which re-exposes the parent form inside the child's view so its `ngModel`s register there (use `NgModelGroup` if the child sits inside a group). Alternatives are passing data through inputs and outputs, or turning the child into a custom form control.
go deeper
Know that template-driven fields in a child component do not automatically join the parent's form.
Explain the DI reason: NgModel injects ControlContainer with @Host(), which stops at the child's boundary, so it falls back to standalone.
Pick the fix deliberately: a viewProviders bridge for form fragments, inputs and outputs for reusable sections, or a custom control for one composite value.
Set conventions for splitting large forms so teams do not scatter implicit registrations across components; past a certain size, an explicit model is easier to own.
## The setup that breaks A sign-up form grows, so the address fields move into their own component: ```html <!-- parent template --> <form #f="ngForm"> <input name="email" ngModel required /> <app-address /> </form> ``` Inside `AddressFields`, the inputs still use `ngModel` with names. They render and accept typing, but `f.value` has no address and `f.valid` is `true` even when they are empty. Nothing throws. ## Why the child cannot see the form `NgModel` receives its parent container through dependency injection: it asks for **`ControlContainer`**, the abstract class that `NgForm`, `NgModelGroup` and the reactive container directives provide. It asks with **`@Optional() @Host()`**. - **`@Host()`** restricts resolution to the injectors of the current component's template, stopping at the host element of that component. The `<form>` lives in the **parent's** template, beyond that boundary. - **`@Optional()`** turns "not found" into `null` instead of an error. With a `null` parent, `NgModel` concludes it is **standalone**: it manages its own `FormControl`, never calls `addControl`, and does not even require a `name`. The child's fields are live but invisible to the form. ## The dev-mode warning Since Angular **22.1.3**, a development build detects this case and logs warning **`NG01354`**: an `ngModel` in a child component cannot register with the parent's `NgForm` because `@Host()` stops injection at the component boundary. The message suggests the two fixes below. Earlier versions failed silently, which is why this bug has a long history in real projects. ## Fix 1: re-expose the container with viewProviders ```ts @Component({ selector: 'app-address', imports: [FormsModule], viewProviders: [{provide: ControlContainer, useExisting: NgForm}], template: ` <input name="city" ngModel required /> <input name="zip" ngModel required /> `, }) export class AddressFields {} ``` - **`viewProviders`** scopes the token to the child's **own template**, which is exactly where `@Host()` lets `NgModel` look; it is the placement Angular's error guide uses, and content projected into the child is not affected. - **`useExisting: NgForm`** resolves `NgForm` from the element injector chain **outside** the child, where the parent's `<form>` provides it, and aliases it as the `ControlContainer`. - If the child is placed inside an `ngModelGroup`, alias **`NgModelGroup`** instead so the fields land in that group. For a reactive parent the equivalent is `FormGroupDirective`. The child is now coupled to being used inside a form; that is the trade-off. ## Fix 2 and 3: other designs | Option | How | Best when | |---|---|---| | Bridge with `viewProviders` | alias the parent container | the child is a template fragment of this form | | Mark fields standalone | `[ngModelOptions]="{standalone: true}"` | the child's fields are UI state, not form data | | Inputs and outputs | parent binds values down and events up | the child is a reusable, presentational section | | Custom form control | implement `ControlValueAccessor` and use `ngModel` on the child element | the child represents one value such as an address object | The custom-control route is the most reusable, since the parent then treats the whole address as one control, but it is more code. The mechanics of writing one belong to the value-accessor topic. ## Testing the split form A component test should render the **parent** with the real child, fill the child's inputs, and assert on the parent's `NgForm`: `f.value` should contain the child's names and `f.valid` should turn `false` when a required child field is empty. A test of the child alone passes even when the bridge is missing, which is how this bug usually slips through. ## Checklist when fields go missing from a template-driven form 1. Is the `ngModel` in the same component template as the `<form>`? If not, bridge or redesign. 2. Is it inside `ngModelOptions` `standalone: true` by accident? 3. Does it have a unique `name`? 4. Is the dev console showing `NG01354`?
- How does the viewProviders bridge actually reach the parent's form?`NgModel` resolves `ControlContainer` with `@Host()`, which searches the child's own view up to its host element, so a token in the child's `viewProviders` is found. That entry is only an alias: `useExisting: NgForm` then resolves `NgForm` from the element injectors outside the child, where the parent's `<form>` provides it, so both sides share one form instance.
- The child component sits inside <fieldset ngModelGroup="address"> in the parent. What changes in the bridge?Alias the group instead of the form: `{provide: ControlContainer, useExisting: NgModelGroup}`. The child's controls then register into the `address` group, so the value comes out as `{address: {city, zip}}`. Aliasing `NgForm` would put them at the top level instead.
saying these in an interview costs you the question
- The child's ngModel inputs throw a missing-parent error
- Adding FormsModule to the child fixes the registration
- Wrapping the child's inputs in their own <form> joins them to the parent form
- The parent form automatically includes controls from child components
- @Host() lets NgModel search all ancestor components