skip to content

In an Angular template-driven form, what do #f="ngForm" and ngModelGroup give you, and what shape does f.value take?

level: middleimportance: should knowfreq 48%

answer

  1. exportAs gives a template reference
  2. NgForm wraps a FormGroup
  3. ngSubmit, submitted, resetForm
  4. ngModelGroup nests a FormGroup
  5. value mirrors the name hierarchy

basics

~10 s

#f="ngForm" exposes the NgForm directive: its FormGroup, value, validity, submitted flag, ngSubmit event and resetForm(). ngModelGroup="address" adds a nested FormGroup, so f.value becomes an object keyed by names, with {address: {...}} for grouped fields.

solid answer

~30 s

`NgForm` is exported as `ngForm`, so `#f="ngForm"` gives the template the directive instance rather than the `<form>` element. Through it you read `f.value`, `f.valid`, `f.controls`, `f.form` (the underlying `FormGroup`) and `f.submitted`, handle `(ngSubmit)`, and call `f.resetForm()`, which resets values and the submitted flag. `ngModelGroup="address"` on a wrapper element creates a nested `FormGroup` that the inner `ngModel`s register into, and it is exported as `ngModelGroup` so `#addr="ngModelGroup"` gives that section's validity. The value mirrors the names: `{email: '...', address: {street: '...', city: '...'}}`. Note that `ngSubmit` fires even when the form is invalid; checking validity is your job.

code

ts · 25 lines
ts
import {Component} from '@angular/core';
import {FormsModule, NgForm} from '@angular/forms';

@Component({
  selector: 'app-signup-with-address',
  imports: [FormsModule],
  template: `
    <form #f="ngForm" (ngSubmit)="save(f)">
      <input name="email" ngModel required email />
      <fieldset ngModelGroup="address" #addr="ngModelGroup">
        <input name="city" ngModel required />
        <input name="zip" ngModel required />
      </fieldset>
      <p>Address complete: {{ addr.valid }}</p>
      <button type="submit">Save</button>
    </form>
  `,
})
export class SignupWithAddress {
  save(f: NgForm): void {
    if (f.invalid) return; // ngSubmit fires even when invalid
    console.log(f.value); // {email: '...', address: {city: '...', zip: '...'}}
    f.resetForm();
  }
}

go deeper

for a junior

Know that #f="ngForm" gives you the form's value and validity in the template and that (ngSubmit) is how you handle submission.

for a middle

Explain exportAs, the members you use on NgForm, and how ngModelGroup nests a FormGroup so the value becomes a nested object.

for a senior

Call out the traps: ngSubmit ignores validity, reset() keeps submitted, and the value shape is untyped and driven by template names.

for a principal

Judge when the implicit, untyped shape becomes a liability for the API contract and an explicit model should replace it.

## Template references and exportAs A template reference variable such as `#f` normally points at the DOM element. Writing `#f="ngForm"` asks for the **directive whose `exportAs` is `ngForm`** on that element instead. `NgForm` is attached automatically to every `<form>` once `FormsModule` is imported, so this is how the template, and anything you pass `f` to, reaches the form model. The same pattern works for fields (`#email="ngModel"`) and sections (`#addr="ngModelGroup"`). ## What NgForm exposes | Member | What it gives you | |---|---| | `form` | the underlying `FormGroup` | | `value`, `valid`, `invalid`, `errors` | aggregated state of all registered controls | | `controls` | the registered controls keyed by `name` | | `submitted` | `true` after a submit, `false` again after `resetForm()` | | `ngSubmit` | output that fires on the native `submit` event | | `resetForm(value?)` | resets values and state and clears `submitted` | | `setValue(value)` | sets every registered control at once | Details worth knowing: - **`ngSubmit` fires regardless of validity.** The directive marks the form submitted and emits; it does not check `valid`. Guard your handler, or disable the button with `[disabled]="f.invalid"`. - **The page does not reload.** `NgForm` handles the native `submit` event and prevents the default navigation, except for forms with `method="dialog"`. - **`resetForm()` versus `reset()`**: `resetForm()` also clears `submitted`, which matters if error messages are shown only after a submit attempt; the inherited `reset()` does not touch it. A native `<button type="reset">` calls `resetForm()` for you. ## ngModelGroup: nested sections Put `ngModelGroup="address"` on a wrapper element and every `ngModel` inside registers into a **nested `FormGroup`** named `address` instead of the top-level one: ```html <form #f="ngForm"> <input name="email" ngModel required /> <fieldset ngModelGroup="address" #addr="ngModelGroup"> <input name="street" ngModel required /> <input name="city" ngModel required /> </fieldset> </form> ``` Now `f.value` is `{email: '', address: {street: '', city: ''}}` and `addr.valid` tells you whether the address section alone is complete. Groups can nest inside groups, and names only need to be unique **within their group**, so `billing.city` and `shipping.city` can coexist. ## What the value shape follows 1. Each `ngModel` contributes one key, its `name`. 2. Each `ngModelGroup` contributes one key holding an object of its children. 3. Controls marked `[ngModelOptions]="{standalone: true}"` contribute nothing. 4. Disabled controls are left out of `value`, as in any `FormGroup`. Because the shape comes from the template, it is **untyped**: `f.value` is `any`, and renaming a `name` silently changes the object your submit handler posts. ## Rules and limits - `ngModelGroup` must sit inside a `<form>` with `NgForm` or inside another `ngModelGroup`; placing it inside a `[formGroup]` throws, because reactive containers expect `formGroupName`. - The parent is found through the element injector **within the same component template**, so a group or field in a child component does not register with the parent's form unless you bridge it. - Group-level validity is the cheapest way to drive per-section UI, such as a step indicator that turns green when the address is complete. ## Reaching the form from the class Passing `f` into the submit handler, as in `(ngSubmit)="save(f)"`, is the simplest route. When the class needs the form outside a handler, a view query such as `viewChild.required(NgForm)` returns the same directive. Remember that the form's controls are registered a microtask after the first render, so code that reads `controls` too early sees an empty group. ## Interview framing Say that `#f="ngForm"` is the handle to the implicit model, list the handful of members you actually use (`value`, `valid`, `submitted`, `ngSubmit`, `resetForm`), and show how `ngModelGroup` turns flat names into a nested object. Mention the `ngSubmit`-fires-when-invalid trap; interviewers like it.

  • Your error messages show only when f.submitted is true, and they stay visible after the user clears the form with f.reset(). Why?
    `reset()` is inherited from the base control directive and only resets the `FormGroup`'s values and states; `submitted` belongs to `NgForm` and stays `true`. `resetForm()` resets the group and sets `submitted` back to `false`, so use it, or a native reset button, which calls it.
  • Can two ngModelGroups in one form both contain a field named city?
    Yes. Names must be unique within their own group, not across the whole form. `ngModelGroup="billing"` and `ngModelGroup="shipping"` each create a nested `FormGroup`, so the value becomes `{billing: {city}, shipping: {city}}` with two independent controls.

saying these in an interview costs you the question

  • #f="ngForm" gives you the native HTMLFormElement
  • ngSubmit only fires when the form is valid
  • reset() and resetForm() both clear the submitted flag
  • ngModelGroup is only a visual grouping with no effect on the value
  • Field names must be unique across the whole form, groups included