Skip to main content

7. Submit the form

Add a submission action to the root form options:

form = form({
// Account, profile, address, and contacts branches from previous steps...
}, {
debounce: 200,
validatorMessages: {
required: 'Complete this value.',
},
onSubmit: async value => {
const response = await fetch('/api/profiles', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(value),
});

if (!response.ok) throw new Error('Could not save the profile.');
},
onSubmitBlocked: formNode => {
formNode.focus();
},
});

The value argument is inferred from the complete form tree, including nested addresses and contact items.

πŸ”Œ Bind the native form​

The existing FormNodeDirective import handles both the native form and its controls:

@Component({
selector: 'app-profile-editor',
imports: [FormNodeDirective],
templateUrl: './profile-editor.html',
})
export class ProfileEditor {
// Signals and form declaration...
}
<form [formNode]="form">
<!-- Controls from previous steps... -->

<button type="reset">Reset interaction state</button>
<button type="submit" [disabled]="form.submitting()">
{{ form.submitting() ? 'Saving…' : 'Save profile' }}
</button>
</form>

Submission:

  1. Marks the form subtree touched.
  2. Commits pending control values.
  3. Runs onSubmitBlocked instead of the action when errors block submission.
  4. Sets submitting() while the asynchronous action runs.
  5. Prevents overlapping actions.

Native reset delegates to the form tree. It clears touched and dirty state, cancels pending control work, and preserves current committed values.

πŸ”— Where to go next​

You now have a typed form that scales from local fields to nested and repeated data without changing its core access pattern.