Getting started

Playground

The form below is running ngwr on this page — signal forms, validation copy and all. One click opens the same demo as a complete Angular 22 workspace on StackBlitz, and every live demo on this site opens the same way.

Signal Forms, live

Nineteen ngwr value controls implement FormValueControl (or FormCheckboxControl) themselves, so [formField] writes the component's own value / checked model. There is no ControlValueAccessor in the library — not a thin one, none. Submit the form below with a field empty: the copy comes from <wr-form-field> through the ngwr/i18n validation.* catalog, so nothing here spells out an error message.

We'll never share it.
import { JsonPipe } from '@angular/common';
import { Component, signal } from '@angular/core';
import { FormField, email, form, required, submit } from '@angular/forms/signals';

import { WrButton } from 'ngwr/button';
import { WrCheckbox } from 'ngwr/checkbox';
import { WrFormField } from 'ngwr/form';
import { WrInput } from 'ngwr/input';
import { WrOption, WrSelect } from 'ngwr/select';

interface Signup {
  email: string;
  team: string;
  agree: boolean;
}

@Component({
  selector: 'demo-root',
  imports: [JsonPipe, FormField, WrButton, WrCheckbox, WrFormField, WrInput, WrOption, WrSelect],
  template: `
    <form (submit)="reserve($event)" style="display: flex; flex-direction: column; gap: 1rem; max-width: 26rem">
      <wr-form-field label="Work email" hint="We'll never share it." required>
        <input wrInput type="email" placeholder="[email protected]" [formField]="signup.email" />
      </wr-form-field>

      <wr-form-field label="Team size" required>
        <wr-select ariaLabel="Team size" placeholder="Pick one" [formField]="signup.team">
          <wr-option value="solo">Just me</wr-option>
          <wr-option value="small">2–10 people</wr-option>
          <wr-option value="large">More than 10</wr-option>
        </wr-select>
      </wr-form-field>

      <wr-form-field>
        <wr-checkbox [formField]="signup.agree">I agree to the terms</wr-checkbox>
      </wr-form-field>

      <button wr-btn type="submit" color="primary" style="align-self: flex-start">Reserve your spot</button>

      @if (submitted(); as value) {
        <pre>{{ value | json }}</pre>
      }
    </form>
  `,
})
export class Demo {
  private readonly model = signal<Signup>({ email: '', team: '', agree: false });

  // Two bindings and nothing in between: `[formField]` writes the component's
  // own `value` / `checked` model, because each of these ngwr controls implements
  // `FormValueControl` itself. There is no ControlValueAccessor in the library.
  protected readonly signup = form(this.model, path => {
    required(path.email);
    email(path.email);
    required(path.team);
    required(path.agree);
  });

  protected readonly submitted = signal<Signup | null>(null);

  protected reserve(event: Event): void {
    event.preventDefault();
    void submit(this.signup, () => {
      this.submitted.set(this.model());
      // undefined means "the server found nothing wrong" — a real action would
      // POST here and map a 4xx onto the field it belongs to.
      return Promise.resolve(undefined);
    });
  }
}

Give it about two and a half minutes

The button hands StackBlitz a real workspace, and a real workspace has to install before it can serve — around 430 packages, cold, then roughly half a minute of Angular build. There is no progress bar for the install; the terminal is the progress bar. If you want it running in ten seconds instead, the three commands below do it locally.

What the sandbox contains

A @angular/build:application workspace whose dependency ranges are read from the Angular and ngwr versions this site was built with — carets, so the install tracks the current patch rather than freezing on the day it was written.

  • ngwr and @angular/cdk installed, with @use 'ngwr'; in src/styles.scss — the whole token layer and every component's styles from one import.
  • A zoneless bootstrap: provideZonelessChangeDetection(), plus provideWrOverlay() — ngwr's own CDK overlay container, so it can never collide with another library's — and provideWrDateAdapter(), which every calendar and date picker injects. They go in whether or not the demo uses them: each costs an import line, and a missing one is a blank page.
  • The demo as src/app/demo.ts, character for character what the page shows. Snippets that are only a template fragment get a component generated around them, with the imports resolved from the library's own selector map.
  • ng serve as npm start. StackBlitz installs the workspace from npm before it can serve it — around 430 packages, cold — so the first open takes a while, and how long is its container's call rather than ours.
import { provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withDisabledInitialNavigation } from '@angular/router';

import { provideWrDateAdapter } from 'ngwr/date';
import { provideWrOverlay } from 'ngwr/overlay';

import { Demo } from './app/demo';

bootstrapApplication(Demo, {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideZonelessChangeDetection(),
    provideRouter([], withDisabledInitialNavigation()),
    provideWrDateAdapter(),
    provideWrOverlay(),
  ],
}).catch(error => console.error(error));
// src/styles.scss — one import for the whole library.
@use 'ngwr';

The same thing locally

ng add ngwr writes exactly this: it installs the peers, appends the stylesheet import, and prints the bootstrap for the options you pick.

# The same workspace on your machine — prompts for styles, date adapter,
# density and theme, then prints the bootstrap snippet you picked.
ng new my-app --style=scss
cd my-app
ng add ngwr