Skip to main content

Reset and restore initial values

Use resetToInitial() to restore the values captured when a field or array was initialized and clear interaction state. It is available on fields, groups, forms, and arrays, and recursively applies to the selected subtree. It takes no arguments.

Choose the reset operation

OperationCommitted valuesDirty and touchedPending control input
reset()PreservedCleared in the subtreeDiscarded
reset(value)Replaced with the supplied valueCleared in the subtreeDiscarded
resetToInitial()Restored from captured initial valuesCleared in the subtreeDiscarded

All three reset operations synchronize controls with the committed model and clear control parsing state through the normal reset hooks. resetToInitial() does not replace reset() or change its existing meaning. Calling it on a child affects that branch; sibling values and interaction state remain unchanged. Parent aggregate values and validity reflect the restored branch, but flags explicitly set on an ancestor are not cleared by resetting a descendant.

profile-form.ts
import { field, form, required } from '@ngblocks/form-nodes';

const profile = form({
name: field('Marco'),
address: { city: field('Zurich') },
email: field('', [required]),
});

profile.reset({ name: 'Server name', address: { city: 'Madrid' }, email: 'ada@example.com' });
profile.name.value.control.set('Edited name');
profile.markAsTouched();
profile.resetToInitial();

profile(); // { name: 'Marco', address: { city: 'Zurich' }, email: '' }
if (profile.name() !== 'Marco' || profile.address.city() !== 'Zurich' || profile.email() !== '') {
throw new Error('Restoration should use declaration values, not the last loaded values.');
}
if (!profile.pristine() || !profile.untouched() || !profile.invalid()) {
throw new Error('Restoration should clear interaction state while preserving required validation.');
}

profile.name.set('Keep this sibling');
profile.address.city.set('Bern');
profile.address.resetToInitial();
profile.address.city(); // 'Zurich'
if (profile.name() !== 'Keep this sibling' || profile.address.city() !== 'Zurich') {
throw new Error('Restoring a branch should leave sibling values unchanged.');
}

What counts as initial?

A field captures its declaration value. A field declared without a value restores null; an explicit undefined restores undefined. Attaching an existing field to a form does not redefine its baseline, even if it was edited before attachment.

New array items capture their effective initialization values after supplied item data has been applied. This includes options.initialValue and values supplied when inserting or creating a row. For example, a template field('Template') initialized with 'Ada' restores 'Ada' when that row or field is reset to initial. Updating a retained row does not redefine its own baseline.

set(), patch(), update(), reset(value), and ordinary control edits do not redefine initial values. Repeated restoration uses the same captured baseline.

Loading a record is not redefining its defaults

If a form declares empty strings and later calls reset(recordFromServer), resetToInitial() still restores the declared empty strings. It does not mean "undo edits since loading" or "restore the last saved record". For that behavior, keep an application-owned snapshot of the loaded/saved record and pass it to reset(snapshot). Use a snapshot strategy appropriate to your value types; do not share mutable data with the editable form if it must remain a reliable restore point.

There is no public operation to replace the captured initial baseline in this release.

Arrays: values, count, order, and identity

An array restores its initial collection, including the number and order of records. Rows added later are removed, missing initial rows are recreated, and an initially empty array becomes empty. Nested arrays restore their effective original collection as part of the enclosing restore.

Restoration uses the existing reconciliation rules: nodes are reused by position without trackBy, or by matching keys with trackBy. Removed node instances are not resurrected. External references to removed nodes remain detached; obtain a recreated row from the array again. Matching retained nodes keep their current validators, options, and dynamically added object fields.

For a numeric initialValue, the array captures the concrete values produced for its initial rows. A factory can run again to construct missing nodes, including its ordinary side effects, but its newly generated defaults are overwritten by the captured data. This preserves original generated IDs and dates; it does not guarantee zero factory calls. Missing nodes use the schema/configuration produced by the factory when they are reconstructed.

contact-forms.ts
import { array, field, form } from '@ngblocks/form-nodes';

const profile = form({
contacts: array({ id: field.strict(0), name: field.strict('Template') }, {
initialValue: [{ id: 1, name: 'Ada' }, { id: 2, name: 'Lin' }],
trackBy: 'id',
}),
});

const ada = profile.contacts[0];
profile.contacts.set([{ id: 2, name: 'Edited' }, { id: 1, name: 'Edited' }, { id: 3, name: 'New' }]);
profile.contacts.resetToInitial();
profile.contacts(); // [{ id: 1, name: 'Ada' }, { id: 2, name: 'Lin' }]
if (profile.contacts[0] !== ada || profile.contacts.length() !== 2 || profile.contacts[1]!.name() !== 'Lin') {
throw new Error('Restoration should recover initial rows and preserve matching keyed nodes.');
}

profile.contacts[0]!.name.set('Changed again');
profile.contacts[0]!.name.resetToInitial();
profile.contacts[0]!.name(); // 'Ada'
if (profile.contacts[0]!.name() !== 'Ada') throw new Error('Effective item values should override template defaults.');

let nextId = 0;
const generated = form({
contacts: array(() => ({ id: field.strict(++nextId) }), {
initialValue: 2,
}),
});

generated.contacts.clear();
generated.contacts.resetToInitial();
generated.contacts(); // [{ id: 1 }, { id: 2 }]
if (nextId !== 4 || generated.contacts[0]!.id() !== 1 || generated.contacts[1]!.id() !== 2) {
throw new Error('Factories may rebuild nodes, but restored data must use the captured IDs.');
}

Use stable tracking keys. Reconciliation still applies its normal duplicate-key checks, and object keys compared by reference may not match after supported data containers have been copied.

Restoring an individual row leaves the array's other rows and length unchanged. A row added later restores its own creation values when reset individually, but restoring its owning array removes that row if it was absent from the array's initial collection.

Dynamic object fields: preserve the current schema

For forms and groups, restoration traverses the children that exist now. Fields added through add() return to their own initial values, and removed fields are not recreated. This also applies to dynamically added fields on retained array rows. This is value restoration, not a rollback of schema changes. Groups and nested forms follow the same rules.

dynamic-profile.ts
import { field, form } from '@ngblocks/form-nodes';

const name = field('Declared name');
name.set('Changed before attachment');

const profile = form({ name });
const nickname = profile.add('nickname', field('Initial nickname'));
profile.add('temporary', field('Temporary field'));
profile.remove('temporary');
nickname.set('Edited nickname');
profile.resetToInitial();

profile(); // { name: 'Declared name', nickname: 'Initial nickname' }
if (profile.name() !== 'Declared name' || nickname() !== 'Initial nickname' || profile.get('temporary') !== undefined) {
throw new Error('Restoration should keep the current schema and each existing field baseline.');
}

Mutable values and snapshot boundaries

The library stores a private copy of supported containers and makes fresh copies when restoring those captured values. The returned form value is not the protected baseline.

Value kindRestoration policy
Primitives, including null and undefinedPreserve the initial value.
Ordinary arrays and plain objects, including null-prototype objectsCopy their own property descriptors and recursively copy data-property values.
Standard DateCopy the timestamp and own properties.
Standard Map and SetRecursively copy entries and own properties.
Cycles and shared referencesPreserve them within each captured value graph.
Accessor propertiesPreserve descriptors without invoking getters; external getter/setter state is not captured.
Custom classes/subclasses, functions, File, Blob, DOM objects, typed arrays, and other opaque valuesPreserve references; in-place changes to their contents cannot be undone.

Property descriptors include symbol and non-enumerable properties. Object-wide frozen/sealed status is not a snapshot guarantee. Copying also does not preserve reference identity with the original supplied object, or shared identity across independently captured fields.

The library does not call structuredClone() or serialize values to JSON. Those approaches would reject or alter some values that fields already accept. Prefer immutable updates for opaque values, or use reset(applicationOwnedSnapshot) with your own cloning policy. In-place edits are not a replacement for signal updates, even when a later restoration can recover supported containers.

snapshot-boundaries.ts
import { field, form } from '@ngblocks/form-nodes';

class Selection {
constructor(public label: string) {}
}

const preferences = { tags: ['initial'], date: new Date('2026-01-01') };
const selection = new Selection('initial');
const profile = form({
preferences: field.strict(preferences),
selection: field.strict(selection),
});

// Deliberate in-place mutations demonstrate the snapshot boundary; prefer immutable updates.
preferences.tags.push('mutated');
preferences.date.setUTCFullYear(2030);
selection.label = 'mutated';
profile.resetToInitial();

profile.preferences().tags; // ['initial']
profile.selection().label; // 'mutated'
if (profile.preferences().tags.length !== 1 || profile.preferences().date.getUTCFullYear() !== 2026) {
throw new Error('Supported data containers should restore independent initial copies.');
}
if (profile.selection() !== selection || profile.selection().label !== 'mutated') {
throw new Error('Custom instances should retain their references and cannot undo in-place edits.');
}

profile.preferences().tags.push('another mutation');
profile.resetToInitial();
if (profile.preferences().tags.length !== 1) throw new Error('Restored copies must not corrupt the stored baseline.');

Validation, availability, and asynchronous work

Restoration clears dirty and touched, cancels numeric/blur/custom debounce work, and discards pending control values. Cancelled pending edits cannot later overwrite the restored values.

The restored values are evaluated by the validators currently configured, not an old copy of the validator configuration. Restoring an empty required field can leave it pristine, untouched, and invalid. Normal reactive validation and cancellation rules still apply when committed values change; asynchronous validation can be pending after the method returns. Restoration does not promise to restart an unchanged value's validation or clear unrelated application errors.

Availability overrides and configured rules are retained. Reactive disabled, readonly, or hidden state can still change as a consequence of restored values. Restoration does not cancel an in-progress submission or undo external side effects.

Configured public equal behavior remains active: public value reads can retain a previous equivalent snapshot. Internally committed data and rendered controls use the restored value.

Bound controls and value outputs

Native controls, CVAs, and signal custom controls synchronize through their existing reset/render paths. Parsing state is cleared and optional custom reset() hooks run for retained bindings. resetToInitial() does not emit formNodeValueChange or formNodeControlValueChange, because it is a programmatic operation. It also cancels notifications belonging to discarded pending input.

Use an explicit button handler to restore defaults:

profile-editor.component.ts
import { Component } from '@angular/core';
import { field, form, FormNodeDirective } from '@ngblocks/form-nodes';

@Component({
imports: [FormNodeDirective],
template: `
<form [formNode]="form">
<input [formNode]="form.name" />
<button type="button" (click)="form.resetToInitial()">Restore initial values</button>
</form>
`,
})
export class ProfileEditor {
form = form({
name: field('Marco', { debounce: 300 }),
});
}

A native <button type="reset"> on a [formNode] form still invokes the existing reset() behavior; it does not automatically call resetToInitial(). A binding's reset() method also keeps its existing behavior. To restore a node obtained through a binding, call binding.node().$api.resetToInitial() when you need a collision-safe generic path.

All three form reset methods also clear submitted() on the reset form and descendant forms. Resetting only a field leaves its owner form's history intact. An already running submission action is not cancelled, and its eventual completion does not restore the cleared history. See submission history.