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.
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
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.
ngwrand@angular/cdkinstalled, with@use 'ngwr';insrc/styles.scss— the whole token layer and every component's styles from one import.- A zoneless bootstrap:
provideZonelessChangeDetection(), plusprovideWrOverlay()— ngwr's own CDK overlay container, so it can never collide with another library's — andprovideWrDateAdapter(), 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 theimportsresolved from the library's own selector map. ng serveasnpm 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