Angular · TypeScript · Open Source

Define fields.
Attach triggers.
Ship the form.

A TypeScript-first form engine for Angular. Async validation, API-driven selects, dynamic arrays, lazy-loaded components, and custom field models all declared in field models, no component glue.

$npm i @preforms/ts @preforms/angular
login.component.ts
1new UserName({
2  required: true,
3  triggers: [
4    {
5      on: FormFieldEventType.CHANGE, reacts to input
6      action: TriggerAction.VALIDATE_ASYNC, async validation
7      fetchUrl: "/api/check/$value",
8    },
9  ],
10})
11 
12new SelectField({
13  key: "region",
14  triggers: [{
15    on: FormFieldEventType.INIT, fires on load
16    action: TriggerAction.FETCH,
17    fetchUrl: "/api/regions",
18    projection: {
19      target: "options", patches the field
20      source: "results",
21    },
22  }],
23})

All the hard parts, declared.

The common patterns in complex forms, extracted into a composable model layer.

Trigger system
Attach async validation, fetches, patches, toggles, dialogs, and resets to any field event.
API projections
Fetch remote data on INIT, reshape it, and patch it straight into form state.
Aggregates
Validate dynamic arrays with product, sum, unique, and other cross-row rules.
FieldArray
Build repeatable rows with add/remove actions, per-row calculations, and array-level validation.
ƒ
OutputField
Define calculated fields that react to other values without wiring extra subscriptions.
Lazy loading
Register heavy custom field components only when the form actually needs them.

One object replaces a subscriber.

Triggers are the unit of reactivity in Preforms. Attach them to any field, point them at an event, and let the engine handle the rest.

Async validation hits your API without handwritten glue.

A dependent select fetches its options when a parent value changes.

A dialog opens when an icon is clicked and closes on a button command.

No RxJS, no ViewChild, no parent component managing it all.

Async validation
1new UserName({
2  triggers: [{
3    on:     FormFieldEventType.CHANGE,
4    action: TriggerAction.VALIDATE_ASYNC,
5    fetchUrl: "/api/username/$value",
6  }],
7})
Dependent select
1new SelectField({
2  key: "city",
3  triggers: [{
4    on:     FormFieldEventType.CHANGE,
5    watchField: "country",
6    action: TriggerAction.FETCH,
7    fetchUrl: "/api/cities/$country",
8    projection: { target: "options" },
9  }],
10})
custom-field.ts
1// 1. Extend FormField
2export class EditorField extends FormField<string> {
3  constructor(key: string) {
4    super({ component: "editor", key });
5  }
6}
7 
8// 2. Register with lazy loader
9provideDynamicFormLazyFields([{
10  type: "editor",
11  loader: () =>
12    import("./editor.component")
13      .then(m => m.EditorComponent),
14}])

Custom fields in two steps.

The engine doesn't know what an "editor" is. You tell it. Extend FormField, register an Angular component against the type key, and the renderer handles the rest.

Rich text editors, date pickers, map inputs, anything Angular can render.

Components load lazily, so there is no bundle cost until the field appears.

Custom fields participate in triggers, validation, and aggregates like any built-in.

Ready to stop writing form glue?

Preforms is open source and MIT licensed. The docs have working examples for every feature.