# Schema Form

> A whole form drawn from a Signal Forms schema. The presentation rides on the same schema as the validation, through the `WR_FIELD` metadata key — so a form is one description, not two.

Source: https://ngwr.dev/reference/components/schema-form  
Kind: Component, Standalone, Signal Forms

## Installation

```angular-ts
import { WR_FIELD } from 'ngwr/form';
import { WrSchemaForm } from 'ngwr/schema-form';

@Component({
  imports: [WrSchemaForm],
})
export class MyComponent {}
```

## Basic usage

Describe each field once, next to its rules. `<wr-schema-form>` walks the field tree, draws the control the `kind` names inside a `<wr-form-field>`, and lets the schema own everything else — the `*` marker, the disabled state, the error copy.

```angular-ts
import { form, metadata, required, schema } from '@angular/forms/signals';
import { WR_FIELD } from 'ngwr/form';

interface Signup {
  fullName: string;
  workEmail: string;
  role: string;
}

const signupSchema = schema<Signup>(path => {
  required(path.fullName);
  metadata(path.fullName, WR_FIELD, () => ({ kind: 'input', placeholder: 'Ada Lovelace' }));

  required(path.workEmail);
  metadata(path.workEmail, WR_FIELD, () => ({
    kind: 'input',
    type: 'email',
    label: 'Work email',
    hint: 'We only use it for the invoice.',
  }));

  metadata(path.role, WR_FIELD, () => ({ kind: 'select', label: 'Plan', options: PLANS }));
});
```

```angular-html
<wr-schema-form [field]="signup" [columns]="2" />
```

## Layout

`columns` sets the grid; a field's `span` widens it and is clamped to the grid it sits in, so a two-column spec dropped into a one-column form still lines up.

```angular-html
<wr-schema-form [field]="signup" [columns]="2" />
```

## A spec is a function, so it can react

`metadata()` takes a `LogicFn`, which means a label, a hint or a set of options may depend on the value of another field — without a branch anywhere in the template.

```typescript
// The spec is a LogicFn, so it can read the rest of the form. An admin
// sees a longer list, and nothing in the template knows about it.
metadata(path.role, WR_FIELD, ctx => ({
  kind: 'select',
  options: ctx.stateOf(path.workEmail).value().endsWith('@acme.com') ? ALL_PLANS : PUBLIC_PLANS,
}));
```

## What the spec deliberately does not carry

`required`, `disabled`, `readonly`, `hidden`, `min` and `max` are absent from `WrFieldSpec`, and their absence is the design.

Every one of them is already an Angular schema function that drives the control through `[formField]`. Repeating them in the spec would be a second source of truth for state the form already owns — and for the bounds it is not even possible: Angular refuses a `[min]` or `[max]` binding on a field-bound control outright (`NG8022`). The one thing this component adds is `hidden()`, which Angular's own docs say a template has to honour itself; a hidden field is drawn by nobody.

```typescript
// State lives in the schema, never in the spec.
required(path.workEmail);          // draws the * beside the label
disabled(path.seats, ({ valueOf }) => valueOf(path.role) === 'starter');
hidden(path.notes, ({ valueOf }) => !valueOf(path.agree));
min(path.seats, 1);
max(path.seats, 500);

// This does not compile — NG8022. The field owns the bound.
// <wr-input-number [formField]="f.seats" [min]="1" />
```

## A field with no spec is skipped

Silently, and on purpose: a schema may describe more than one screen renders, and inferring a control from a field's type would draw a switch for every boolean flag in the model.

That is also the escape hatch. Point `[field]` at a nested object field to draw one section, and write the fields that need real markup — a [colour picker](https://ngwr.dev/reference/components/color-picker), a [transfer list](https://ngwr.dev/reference/components/transfer), anything with a layout of its own — by hand beside it. The two halves bind to the same form.

```angular-html
<!-- Draw most of the form, then write the one field by hand. -->
<wr-schema-form [field]="signup.address" [columns]="2" />

<wr-form-field label="Signature">
  <wr-color-picker [formField]="signup.brandColor" />
</wr-form-field>
```

## A nested group is one form each

The field tree is walked one level deep, so an object-valued field is skipped like any other undescribed field — and its children go with it.

That is the default because a model nests for reasons a screen knows nothing about, and because a group has a presentation of its own — a heading, a fieldset, a column count that need not match its parent's — which the spec does not describe. So a group is drawn by pointing a second `<wr-schema-form>` at it, with whatever markup belongs around it.

```angular-html
<!-- `address` is a group: the parent draws none of it. -->
<wr-schema-form [field]="signup" [columns]="2" />

<h3>Address</h3>
<wr-schema-form [field]="signup.address" [columns]="2" />
```

Describing the children of a group and then getting none of them is never what anyone meant, and it does not look like a mistake — nothing is missing from the page, so a half-drawn form reads as a finished one. That exact shape warns in development. A group nobody described inside stays silent, because it is a model detail this screen is not rendering.

## API

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `field`required | The form to draw — the value `form()` returned, or any object-valued field inside one. | `FieldTree<T>` | — |
| `columns` | Grid columns. A field spans one of them unless its spec says otherwise. | `number` | `1` |

## CSS variables

Custom properties `ngwr/schema-form` publishes. Each default below is declared on the component's own selector, so a `:root` override is shadowed by it — set them on that selector, on a wrapper you scope yourself, or inline on the element. Unlike the BEM class names, these are the supported way to restyle the component.

| Variable | Default | Declared on |
| --- | --- | --- |
| `--wr-schema-form-gap` | `1rem` | `.wr-schema-form` |

## See also

- [wr-form-field](https://ngwr.dev/reference/components/form-field) — What this component draws around every field. Reach for it directly when you write the markup yourself.
- [Forms](https://ngwr.dev/guides/forms) — How Signal Forms binds to ngwr controls, and which states reach a control on their own.
- [wr-select](https://ngwr.dev/reference/components/select) — What `kind: 'select'` renders — including the search and multi modes a hand-written field can ask for.
