@ngx-signal-form/zod
v0.0.1
Published
Angular Signal Forms powered by Zod schemas. Build a typed `FieldTree` from any Zod schema and get parsed values, errors, and validity as signals.
Downloads
44
Readme
@ngx-signal-form/zod
Angular Signal Forms powered by Zod schemas. Build a typed FieldTree from any Zod schema and get parsed values, errors, and validity as signals.
Install
npm install @ngx-signal-form/zod zodPeer deps: @angular/core/@angular/forms v21+ and zod v4.
Quick start (string)
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Field } from '@angular/forms/signals';
import z from 'zod';
import { NgxSignalForm, NullableFieldCoercionPipe } from '@ngx-signal-form/zod';
@Component({
selector: 'app-name-form',
imports: [Field, NullableFieldCoercionPipe],
template: `
<label>
Name
<input type="text" [field]="form | nullableField" />
</label>
<p>Parsed: {{ form.parsedValue$() }}</p>
<p>Valid: {{ form.isValid$() }}</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NameFormComponent {
private readonly ngxSignalForm = inject(NgxSignalForm);
readonly schema = z.string().min(1);
readonly form = this.ngxSignalForm.create(this.schema);
// Internal init (conceptual):
// form(signal(null), path => { required(path); minLength(path, 1); });
// Initial model shape: form.model$() => null
}Internally, create builds an Angular Signal Forms FieldTree (via form(model$, schemaFn)) and maps your Zod rules to the matching signal-forms checks. For example, z.string().min(1) becomes a minLength rule on that path, so [field] enforces it automatically.
Why nullableField
Angular's [field] typing currently rejects FieldTree<T | null> (see angular/angular#65839). Use the provided NullableFieldCoercionPipe (form.someOptional | nullableField) to bridge optional/nullable nodes until Angular fixes it.
API surface
NgxSignalForm.create(schema, options?)->NgxZodFieldTree<T>(aFieldTreeyou can call and navigate likeform.userName()orform()).parsedValue$: signal of parsed Zod output ornullparsedError$: signal ofZodError | undefinedisValid$: signal of boolean validitymodel$: writable signal of the form model (null-filled shape)
- Types:
NgxZodFieldTree,DeepNullModel,PartialFormForSchema - Utilities:
NullableFieldCoercionPipefor template bindings
Options
// Use `$ZodType` from `zod/v4/core`.
type NgxSignalFormOptions<T extends $ZodType> = {
// Optional initial value that follows the form shape (missing parts are filled with nulls for you).
initialValue?: PartialFormForSchema<T>;
useSchemaDefaults?: boolean; // defaults true
};
// Example signature
ngxSignalForm.create(schema, options);PartialFormForSchema<T> is the partial, developer-friendly shape of the null-filled model produced from your schema, so you can pass only the fields you care about.
Advanced (checks + initialValue)
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Field } from '@angular/forms/signals';
import z from 'zod';
import { NgxSignalForm, NullableFieldCoercionPipe } from '@ngx-signal-form/zod';
const orderSchema = z.object({
sku: z.string().regex(/^[A-Z0-9]{8}$/),
quantity: z.number().int().min(1).max(100).multipleOf(5),
notes: z.string().min(5).max(50).optional(),
});
@Component({
selector: 'app-order-form',
imports: [Field, NullableFieldCoercionPipe],
template: `
<input type="text" [field]="form.sku | nullableField" />
<input type="number" [field]="form.quantity | nullableField" />
<textarea [field]="form.notes | nullableField"></textarea>
<p>Parsed: {{ form.parsedValue$() | json }}</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OrderFormComponent {
private readonly ngxSignalForm = inject(NgxSignalForm);
readonly form = this.ngxSignalForm.create(orderSchema, {
initialValue: { quantity: 5 },
});
// Internal init (conceptual):
// form(signal({ sku: null, quantity: 5, notes: null }), path => {
// required(path); // root object
// required(path.sku); // string regex /^[A-Z0-9]{8}$/
// pattern(path.sku, /^[A-Z0-9]{8}$/);
// required(path.quantity); // min(1), max(100), multipleOf(5)
// min(path.quantity, 1);
// max(path.quantity, 100);
// validate(path.quantity, /* multiple-of-5 check */);
// // notes is optional; minLength/maxLength apply only when present
// minLength(path.notes, 5);
// maxLength(path.notes, 50);
// });
// Initial model shape: form.model$() => { sku: null, quantity: 5, notes: null }
}Building & testing
ng build ngx-signal-form-zod
ng test ngx-signal-form-zod