Form Builder
ProRenders a form from a JSON schema — conditional visibility, per-field validation, and a payload with hidden fields pruned out. The three things it exists for are the three a schema form silently gets wrong: a hidden field whose value is still submitted, a required field in a hidden branch that blocks the form with nothing on screen to fix, and a schema whose own mistakes never surface because it came from a database rather than a compiler.
import { SvFormBuilderComponent } from '@styloviz-pro/forms';Demo & usage
Basic usage
ProUsage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Conditional visibility, and what gets submitted
ProA field inside a hidden group is hidden whatever its own condition says, and its value never reaches `submitted`. The component keeps it in memory, so ticking the box back restores what the user typed — destroying it on hide loses the typing of anyone who unticked a box just to read what was underneath.
Validation ignores what is not on screen
ProA required field in a hidden branch would otherwise keep the form invalid forever: submit disabled, every visible field green, and the thing blocking the user not rendered at all. A field that is not shown is not asked, so it cannot be unanswered.
The schema’s own mistakes
ProA schema arrives from a database or a CMS, so its author has no compiler and often no console — and every one of these is silent. The component renders them rather than logging them.
A field kind the builder does not know
ProA schema form’s premise is that the form’s shape is data — and a closed list of eight field kinds was the one part of it still in code. Every control the kit ships but the builder does not draw was unreachable from a schema. Register a renderer and it renders.
Rules only the server knows
ProThe bug this closes is not the request — it is that a form with an unanswered check LOOKS VALID. Press Submit before the check lands and every schema rule passes, so the form submits a value whose server-side guarantee was never established. Invisible in development, where the check answers in 4ms.
When to use
- The form’s shape is data — an admin builder, a per-tenant intake form, a survey.
- Fields appear and disappear based on other answers, and what is submitted has to match what was on screen.
- You need the same visibility and pruning rules on the server: the engine is exported separately from the component.
Async states
None — this component has nothing to load, so it has no loading, empty or error state to drive.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| schema | readonly SvFormField[] | [] | Field definitions to render — each `SvFormField` sets its key, kind, label, validation rules, `showIf` condition and nested `fields`; defaults, errors and the submitted payload derive from it. |
| value | SvFormValue (two-way) | {} | The working value, including fields that are currently hidden. Two-way bindable. A host that wants what is *submittable* wants `payload()`. |
| disabled | boolean | false | Disables every field and the submit button; edits and submits are ignored while true. |
| submitLabel | string | 'Submit' | Text of the submit button. |
| eagerErrors | boolean | false | Show every error immediately rather than after the field is left. THE DEFAULT IS `false` BECAUSE A FORM THAT IS RED BEFORE IT IS TOUCHED TEACHES THE USER TO IGNORE RED. Errors appear on blur, and all at once on a failed submit — which is the moment they are actually useful. |
| asyncChecks | SvAsyncChecks | {} | Server-side rules, by field path. ```ts checks = { username: async (value, ctx) => { const res = await fetch(`/api/free?u=${value}`, { signal: ctx.signal }); return (await res.json()).taken ? 'That username is taken.': null; }, }; ``` Resolve with `null` to accept and with the message to show. **Rejecting means the check could not run**, which blocks the submit and says so — reporting a 503 as "That username is taken" is a lie the user cannot act on, and treating it as a pass submits the thing the check was guarding. |
| asyncDebounceMs | number | 300 | Quiet period before a check fires, in milliseconds. A check runs per *value*, not per keystroke, so the debounce is about request volume rather than correctness — pressing Submit cuts a counting-down debounce short rather than waiting it out. |
| renderers | SvFieldRenderers | {} | Draw a field kind this package does not know. Keys are field kinds; values are a `TemplateRef` or a component `Type`. A key matching a built-in kind overrides it. ```html <ng-template #rating let-ctx> <sv-rating [value]="$any(ctx.value)" (valueChange)="ctx.set($event)" /> </ng-template> <sv-form-builder [schema]="schema" [renderers]="{ rating }" /> ``` A component renderer reads `SV_FIELD_CONTEXT` instead. Either way the context is LIVE — `value`, `error`, `disabled` and `asyncStatus` are accessors over the component's signals, so reading one in a template or a `computed` tracks it. Copying it into a field does not. Everything else still applies to a custom kind: `showIf` hides it, `svPruneHidden` prunes it, `required` / `minLength` / `pattern` validate it, and `asyncChecks` can check it. ⚠️ It coerces as TEXT — see `SvFieldKind` for when that is the wrong answer. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| submitted | SvFormValue | The pruned value, once the form is valid. |
| rejected | readonly SvFieldError[] | A submit that was refused, with the errors that refused it. |
| valueChange | SvFormValue | Emits the new `value` whenever it changes — the output half of `[(value)]`. |
Customization & theming
- **Not a `ControlValueAccessor`, and there is no single value for one to carry.** This component has two: `value()` is the working state including hidden fields, and `payload()` is what may be submitted, with those pruned. A CVA exposes one — pick `value` and every host silently submits answers the user could not see; pick `payload` and a user who unticks a box to read what is underneath loses their typing the moment they tick it back.
- The contract instead: `[(value)]` is the working state, `payload()` is what to send, and **`await submit()`** returns a promise — it runs any server check that has not run, then emits `submitted` or `rejected`. `valid()` means *established as valid* (no schema errors, no rejected checks, nothing outstanding); `awaitingChecks()` is the middle state Angular's `PENDING` does not cover, because it also includes a check that has not been REQUESTED yet — the debounce window, which is exactly where a user pressing Enter lands.
- Field kinds: `text`, `textarea`, `number`, `checkbox`, `select`, `radio`, `date` and `group`. Each renders through the kit’s own control, so a schema form looks like the rest of the app rather than like a form generator.
- Set `cropType`-style output shaping per field with `min` / `max` / `minLength` / `maxLength` / `pattern` — the pattern is anchored for you, because an unanchored one matches a substring and quietly accepts anything containing a match.
- `showIf` takes three narrow predicates (`equals`, `in`, `truthy`) rather than an expression language, so a schema stays serialisable, diffable in review, and reasonable about visibility.
- `eagerErrors` shows every problem immediately; the default reveals them on blur and all at once on a refused submit, because a form that is red before it is touched teaches the user to ignore red.
- Call `reset(partial)` to go back to `svSchemaDefaults` merged per level — a spread would lose half of every group.
- **Schema evolution.** Store submissions as `{ schemaVersion, value }` and load them through `svMigrateValue(stored, migrations, currentVersion, schema)` before binding. Migrations are yours — one pure `SvSchemaMigration` step per version, because only you know `dept` became `team` rather than being deleted — and a missing step stops the chain (`incomplete: true`) rather than running a later one on input it was not written for. The result also lists `orphaned` keys the current schema no longer has: `payload()` prunes those, so a renamed field without a migration is otherwise silently dropped on the next save. `svCoerceToSchema` fixes values that kept their old type (a v1 string in a now-`number` field) on load rather than on first touch.
- `renderers` maps a field kind to a `TemplateRef` or a component `Type`, so a schema can use controls this package does not ship. A key matching a built-in kind overrides it — a host that wants its own date control everywhere registers `date`, rather than forking the schema’s vocabulary into "my-date". `SvFieldKind` is open, so a custom kind typechecks while the built-ins still autocomplete.
- `asyncChecks` gives a field a server-side rule, keyed by path, with `asyncDebounceMs` for the quiet period. The verdict belongs to a VALUE, not to a field — edit the box and the previous answer is gone rather than stale, which is what stops a form reading green for something nobody checked. Declare `dependsOn` when the answer turns on another field too.
- Validity is three-valued: `valid()`, the errors, and `awaitingChecks()` for the state that is neither. `submit()` returns a promise, runs any check that has not run (cutting a counting-down debounce short) and refuses if the value moved while it waited. `busy()` and each row’s `asyncStatus` drive the in-progress copy.
Accessibility
- Each control is the kit’s own accessible component, so labels, descriptions, required marking and error wiring come from there rather than being re-implemented.
- A group renders as a `<fieldset>` with a `<legend>`, so a screen reader announces which section a field belongs to.
- Schema problems are rendered in a `role="alert"` region rather than logged, because the person who authored the schema is working in a CMS and has no console.
- The submit button is deliberately never disabled: a disabled submit refuses silently, and pressing it is what reveals the form’s objections — which matters most when the offending field is scrolled off screen.
- A check in progress is announced with `role="status"`, not `role="alert"`: a field being checked is not something the user got wrong, and an assertive announcement per keystroke-pause would make a form with two checked fields unusable with a screen reader.
Feedback on Form Builder
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.