Advanced Reactive Forms

Back

Loading concept...

Advanced Reactive Forms in Angular

The Orchestra Conductor Story

Imagine you’re a conductor leading a big orchestra. Each musician (form control) plays their part. But what if you need a whole section of violins that can grow or shrink? What if you want musicians to follow your exact rules? That’s what Advanced Reactive Forms do—they give you super powers to control complex forms!


FormArray: Your Growing Team of Musicians

Think of FormArray as a row of chairs where musicians can sit. You can add more chairs when new musicians join, or remove chairs when they leave.

What Is FormArray?

A FormArray holds a list of form controls—like having many textboxes that you can add or remove dynamically.

import { FormArray, FormControl } from '@angular/forms';

// Create a FormArray with 2 phone numbers
const phones = new FormArray([
  new FormControl('555-1234'),
  new FormControl('555-5678')
]);

When Do You Need It?

  • Multiple phone numbers (user adds as many as needed)
  • List of skills (add/remove skills)
  • Shopping cart items (dynamic list)

FormArray Methods: Adding & Removing Chairs

Your orchestra needs flexibility! Here are the magic methods:

push() – Add a New Chair

// Add a new phone number
phones.push(new FormControl('555-9999'));

removeAt() – Remove a Specific Chair

// Remove the first phone number (index 0)
phones.removeAt(0);

at() – Get a Specific Chair

// Get the second phone number
const secondPhone = phones.at(1);

insert() – Squeeze in a Chair

// Insert at position 1
phones.insert(1, new FormControl('555-0000'));

clear() – Remove All Chairs

// Remove everyone!
phones.clear();

length – Count the Chairs

console.log(phones.length); // 3
graph TD A["FormArray"] --> B["push - Add at end"] A --> C["removeAt - Remove by index"] A --> D["at - Get by index"] A --> E["insert - Add at position"] A --> F["clear - Remove all"]

FormGroup Methods: Conducting Sections

A FormGroup is like a section of the orchestra—violins, cellos, flutes grouped together. Each group has its own controls.

addControl() – Add a New Instrument

const form = new FormGroup({
  name: new FormControl('')
});

// Add email later
form.addControl('email', new FormControl(''));

removeControl() – Remove an Instrument

form.removeControl('email');

contains() – Check If Instrument Exists

if (form.contains('email')) {
  console.log('Email field exists!');
}

setControl() – Replace an Instrument

// Replace the name control entirely
form.setControl('name', new FormControl('New Name'));

get() – Grab an Instrument

const nameControl = form.get('name');

Dynamic Forms: Building on the Fly

Sometimes you don’t know what the form looks like until the user decides. Like a magic stage that builds itself!

The Recipe

// Form structure comes from server/config
const formConfig = [
  { name: 'firstName', type: 'text' },
  { name: 'age', type: 'number' },
  { name: 'subscribe', type: 'checkbox' }
];

// Build form dynamically
const form = new FormGroup({});

formConfig.forEach(field => {
  form.addControl(
    field.name,
    new FormControl('')
  );
});

In the Template

<form [formGroup]="form">
  <div *ngFor="let field of formConfig">
    <label>{{ field.name }}</label>
    <input [formControlName]="field.name">
  </div>
</form>
graph TD A["Config/Server Data"] --> B["Loop Through Fields"] B --> C["addControl for Each"] C --> D["FormGroup Ready!"] D --> E["Render with *ngFor"]

Typed Forms: Safety Glasses On!

Before Angular 14, forms were like a mystery box—you never knew what type of data was inside. Typed Forms give you X-ray vision!

The Old Way (Untyped)

const form = new FormGroup({
  name: new FormControl('Alice')
});

// What type is this? Angular says: any
const value = form.value.name; // any 😕

The New Way (Typed)

const form = new FormGroup({
  name: new FormControl('Alice'),
  age: new FormControl(25)
});

// TypeScript knows!
const name = form.value.name; // string | undefined
const age = form.value.age;   // number | undefined

Why “| undefined”?

Because controls can be disabled—and disabled controls don’t appear in .value. Angular is being extra careful!

getRawValue() – Get Everything

// Gets ALL values, even disabled ones
const allData = form.getRawValue();
// { name: 'Alice', age: 25 }

NonNullableFormBuilder: Never Empty!

Some fields should always have a value. Like a name tag that’s never blank. NonNullableFormBuilder makes controls that reset to their initial value, not to null.

Regular FormControl

const name = new FormControl('Alice');
name.reset(); // Value becomes null 😱

NonNullable FormControl

const nnfb = inject(NonNullableFormBuilder);

const form = nnfb.group({
  name: 'Alice',
  age: 25
});

form.reset();
// name is 'Alice' again, not null! 🎉
// age is 25 again!

Why Use It?

  • Forms that must always have defaults
  • No more checking for null everywhere
  • Cleaner, safer code
graph TD A["FormControl reset"] --> B["Value = null"] C["NonNullable reset"] --> D["Value = initial value"] D --> E["Much Safer!"]

Form Methods: Your Toolkit

Every form control comes with built-in tools:

setValue() – Set Everything

form.setValue({
  name: 'Bob',
  age: 30
}); // Must set ALL fields!

patchValue() – Set Some Things

form.patchValue({
  name: 'Bob'
}); // Only updates name, ignores age

reset() – Start Fresh

form.reset(); // Clear everything
form.reset({ name: 'Default' }); // Reset to specific values

enable() / disable()

form.get('age')?.disable(); // Gray out age field
form.get('age')?.enable();  // Bring it back

markAsTouched() / markAsDirty()

// Tell Angular the user interacted
form.markAllAsTouched();

Quick Reference Table

Method What It Does
setValue() Set all controls
patchValue() Set some controls
reset() Reset to initial
disable() Make uneditable
enable() Make editable
markAsTouched() Mark as touched
getRawValue() Get all values

ControlValueAccessor: Building Custom Instruments

What if you want to create your own special instrument for the orchestra? Like a star-rating picker or a color selector? That’s where ControlValueAccessor comes in!

The Four Magic Methods

Your custom component must implement these:

interface ControlValueAccessor {
  writeValue(value: any): void;
  registerOnChange(fn: any): void;
  registerOnTouched(fn: any): void;
  setDisabledState?(disabled: boolean): void;
}

What Each Method Does

Method Purpose
writeValue Angular tells YOUR component what value to show
registerOnChange You tell Angular when the value changes
registerOnTouched You tell Angular when user touched it
setDisabledState Angular tells you to enable/disable

Simple Example: Star Rating

@Component({
  selector: 'star-rating',
  template: `
    <span *ngFor="let star of stars; let i = index"
          (click)="rate(i + 1)">
      {{ i < value ? '★' : '☆' }}
    </span>
  `,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: StarRatingComponent,
    multi: true
  }]
})
export class StarRatingComponent
    implements ControlValueAccessor {

  stars = [1, 2, 3, 4, 5];
  value = 0;
  onChange: any = () => {};
  onTouched: any = () => {};

  writeValue(val: number) {
    this.value = val;
  }

  registerOnChange(fn: any) {
    this.onChange = fn;
  }

  registerOnTouched(fn: any) {
    this.onTouched = fn;
  }

  rate(rating: number) {
    this.value = rating;
    this.onChange(rating);
    this.onTouched();
  }
}

Using Your Custom Control

<form [formGroup]="form">
  <star-rating formControlName="rating"></star-rating>
</form>

Now your star rating works just like a regular input!

graph TD A["Angular Form"] -->|writeValue| B["Your Component"] B -->|onChange| A B -->|onTouched| A A -->|setDisabledState| B

Putting It All Together

Let’s build a dynamic survey form that uses everything:

@Component({...})
export class SurveyComponent {
  private nnfb = inject(NonNullableFormBuilder);

  form = this.nnfb.group({
    name: ['', Validators.required],
    questions: this.nnfb.array([])
  });

  get questions() {
    return this.form.get('questions') as FormArray;
  }

  addQuestion() {
    const questionGroup = this.nnfb.group({
      text: '',
      type: 'text'
    });
    this.questions.push(questionGroup);
  }

  removeQuestion(index: number) {
    this.questions.removeAt(index);
  }

  submit() {
    // Typed! TypeScript knows the shape
    const data = this.form.getRawValue();
    console.log(data.name);
    console.log(data.questions);
  }
}

Your Conductor’s Checklist

You’ve learned to:

  • FormArray – Manage lists of controls
  • FormArray Methods – push, removeAt, at, insert, clear
  • FormGroup Methods – addControl, removeControl, contains, setControl
  • Dynamic Forms – Build forms from configuration
  • Typed Forms – Get type safety with your forms
  • NonNullableFormBuilder – Reset to defaults, not null
  • Form Methods – setValue, patchValue, reset, enable/disable
  • ControlValueAccessor – Create custom form controls

You’re now a Form Conductor Extraordinaire! Go build amazing, dynamic, type-safe forms!

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.