Filling forms
Many PDFs are forms: text boxes, checkboxes, dropdowns, signature fields. Register withForm() and
put <epdf-form-layer> in each page, and people can fill them in:
import { Component } from '@angular/core';
import { EpdfFormLayer, withForm } from '@embedpdf/angular/form';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { engine } from './pdf';
@Component({
selector: 'app-form-viewer',
imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfFormLayer],
providers: [provideEmbedPdf({ engine }, withStage(), withRender(), withInteraction(), withForm())],
template: `
<epdf-stage>
<ng-template epdfPage>
<epdf-render-layer />
<epdf-form-layer />
</ng-template>
</epdf-stage>
`,
})
export class FormViewer {}Loading live preview…
import { ChangeDetectionStrategy, Component, inject, ViewEncapsulation } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfForm, EpdfFormLayer, withForm } from '@embedpdf/angular/form';
import { localEngine } from '@embedpdf/engine';
const forms = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/forms.pdf');
return { kind: 'bytes', id: 'forms', bytes: new Uint8Array(await response.arrayBuffer()) };
};
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfFormLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: forms }] },
withStage(),
withRender(),
withInteraction(),
withForm(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './basic.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<div class="toolbar">
<output class="readout">{{ form.fields().length }} fields: click one and fill it in</output>
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-form-layer />
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
protected readonly form = inject(EpdfForm);
}Each field becomes a real HTML control, placed and styled like the field in the PDF: people type in text boxes, tick checkboxes, pick from lists, and move between fields with Tab. A value is saved into the document as it’s entered, so it’s there when the PDF is downloaded, and every PDF app shows it. The form layer paints the fields as the engine draws them, so while it’s on the page, the render layer leaves them out of the page picture.
Read the form#
form.fields() is every field, as a signal: your view updates when a value changes.
import { Component, inject } from '@angular/core';
import { EpdfForm } from '@embedpdf/angular/form';
@Component({
selector: 'app-field-list',
template: `
<ul>
@for (field of form.fields(); track field.name) {
<li>{{ field.name }}: {{ field.family }}</li>
}
</ul>
`,
})
export class FieldList {
protected readonly form = inject(EpdfForm);
}Each field has its full name (such as billing.name), its family ('text', 'checkbox',
'radio', 'combobox', 'listbox', 'pushbutton' or 'signature'), its value, and a ref: its
address for everything else. The fields and their values are the engine’s; see
Forms for every family’s value.
For one field, such as a total you show in your own UI, use form.valueOf():
import { Component, inject } from '@angular/core';
import { EpdfForm, toFieldRef } from '@embedpdf/angular/form';
@Component({
selector: 'app-invoice-total',
template: `
@let current = total();
@if (current && 'value' in current) {
<output>{{ current.value }}</output>
}
`,
})
export class InvoiceTotal {
protected readonly total = inject(EpdfForm).valueOf(toFieldRef('invoice.total'));
}toFieldRef(name) makes a ref from a field’s full name, for when you know a field by name.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
signal,
untracked,
ViewEncapsulation,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfForm, EpdfFormLayer, toFieldRef, withForm } from '@embedpdf/angular/form';
import type { FormFieldDTO } from '@embedpdf/angular/form';
import { localEngine } from '@embedpdf/engine';
const forms = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/forms.pdf');
return { kind: 'bytes', id: 'forms', bytes: new Uint8Array(await response.arrayBuffer()) };
};
/** A field's value as text, whatever its family. */
function valueText(field: FormFieldDTO): string {
switch (field.family) {
case 'text':
case 'combobox':
return field.value || '—';
case 'checkbox':
return field.checked ? 'checked' : 'not checked';
case 'radio':
return field.value === 'Off' ? '—' : field.value;
case 'listbox':
return field.selectedValues.join(', ') || '—';
default:
return '';
}
}
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfFormLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: forms }] },
withStage(),
withRender(),
withInteraction(),
withForm(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './read.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<div class="toolbar">
<output class="readout">Hello, {{ greeting() }}</output>
@if (changed(); as field) {
<output class="note">Last change: {{ field }}</output>
}
</div>
<div class="viewer">
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-form-layer />
</ng-template>
</epdf-stage>
<ul class="fields">
@for (field of form.fields(); track field.name) {
<li class="field">
<span class="field-name">
{{ field.name }} <span class="family">{{ field.family }}</span>
</span>
<span class="field-value">{{ valueText(field) }}</span>
</li>
}
</ul>
</div>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
protected readonly form = inject(EpdfForm);
private started = false;
protected readonly valueText = valueText;
/** One field's value: it changes only when that field does. */
private readonly name = this.form.valueOf(toFieldRef('name'));
protected readonly greeting = computed(() => {
const name = this.name();
return name && 'value' in name && name.value ? name.value : 'stranger';
});
protected readonly changed = signal<string | null>(null);
constructor() {
// Every change, whoever made it: typing, a script, or code.
this.form.valueChanged$
.pipe(takeUntilDestroyed())
.subscribe(({ field }) => this.changed.set(field.name));
// On load: a name filled in.
effect(() => {
if (this.form.status() !== 'ready' || this.started) return;
this.started = true;
untracked(() => {
void this.form.setValue(toFieldRef('name'), { value: 'Ada Lovelace' });
});
});
}
}Fill in a field#
setValue() takes the same values a field reads:
await form.setValue(name.ref, { value: 'Ada Lovelace' }); // text
await form.setValue(agree.ref, { checked: true }); // checkbox
await form.setValue(plan.ref, { value: 'yearly' }); // radio group: the button's export value
await form.setValue(country.ref, { value: 'NL' }); // dropdown: an option's value
await form.setValue(toppings.ref, { selectedValues: ['cheese', 'olives'] }); // listIt runs the same checks and scripts as typing does, and resolves with { field, status }: status
is 'applied', 'unchanged', or 'rejected' when the form’s own validation refused the value.
The field shows the new value at once, before the engine answers, and the promise resolves once it has. What the form’s scripts calculate from it goes with it, as one change: one request, and one step to undo. The field stays editable meanwhile; filling it in again is the next change. A value the engine refuses goes back to the one the field had.
Your own inputs can fill in fields too. form.controlOf(ref) gives you a field as an Angular
FormControl: it holds the field’s value, and fills in the field when the control changes, with
the same checks and scripts:
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { EpdfForm, toFieldRef } from '@embedpdf/angular/form';
@Component({
selector: 'app-billing-name',
imports: [ReactiveFormsModule],
template: `
<label>
Name
<input [formControl]="name" />
</label>
`,
})
export class BillingName {
protected readonly name = inject(EpdfForm).controlOf(toFieldRef('billing.name'));
}To fill many fields at once, such as from your backend, pass them by name:
await form.importValues({ 'billing.name': 'Ada Lovelace', 'billing.country': 'NL', agree: true });importValues() and setValues() write every value as one change, with the calculations run once
after them. Each field’s own keystroke and validation scripts don’t run, as when a script sets a
field’s value in Acrobat.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
inject,
signal,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfForm, EpdfFormLayer, toFieldRef, withForm } from '@embedpdf/angular/form';
import { localEngine } from '@embedpdf/engine';
const forms = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/forms.pdf');
return { kind: 'bytes', id: 'forms', bytes: new Uint8Array(await response.arrayBuffer()) };
};
/** One field of each family, with `setValue()` in the shape each one takes. */
async function fillIn(form: EpdfForm) {
await form.setValue(toFieldRef('name'), { value: 'Ada Lovelace' }); // text
await form.setValue(toFieldRef('updates'), { checked: true }); // checkbox
await form.setValue(toFieldRef('plan'), { value: 'yearly' }); // radio group: a button's value
await form.setValue(toFieldRef('framework'), { value: 'React' }); // dropdown: an option's value
return form.setValue(toFieldRef('topics'), { selectedValues: ['Forms', 'Signatures'] }); // list
}
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfFormLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: forms }] },
withStage(),
withRender(),
withInteraction(),
withForm(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './fill.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<div class="toolbar">
<button type="button" class="button" (click)="fillIn()">Fill in</button>
<button type="button" class="button" (click)="fillFromBackend()">
Fill from your backend
</button>
<button type="button" class="button" (click)="reset()">Reset</button>
@if (result()) {
<output class="readout">{{ result() }}</output>
}
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-form-layer />
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
private readonly form = inject(EpdfForm);
protected readonly result = signal('');
protected async fillIn() {
const { status } = await fillIn(this.form);
this.result.set(`Filled in: ${status}`);
}
protected async fillFromBackend() {
// Plain values by full name, as your backend would send them.
const { applied, skipped } = await this.form.importValues({
name: 'Grace Hopper',
framework: 'Vue',
updates: false,
topics: ['Annotations'],
phone: '555-0100',
});
this.result.set(`${applied.length} filled, ${skipped.length} skipped`);
}
protected async reset() {
const { fields } = await this.form.reset();
this.result.set(`${fields.length} fields reset`);
}
}Check required fields#
A form marks some fields as required. Check them before you submit or download:
const { valid, missing } = form.validate();
if (!valid) form.focus(missing[0].ref);missing lists the required fields that are empty, in page order, so you can take the user to the
first one: focus() brings it into view and puts the cursor in it. Only the fields the user may fill
in count: another signer’s are theirs to fill.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
signal,
untracked,
viewChild,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfForm, EpdfFormLayer, toFieldRef, withForm } from '@embedpdf/angular/form';
import type { FormValidation } from '@embedpdf/angular/form';
import { localEngine } from '@embedpdf/engine';
const forms = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/forms.pdf');
return { kind: 'bytes', id: 'forms', bytes: new Uint8Array(await response.arrayBuffer()) };
};
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfFormLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: forms }] },
withStage(),
withRender(),
withInteraction(),
withForm(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './validate.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<div class="toolbar">
<button type="button" class="button" (click)="submit()">Submit</button>
@if (check(); as check) {
@if (check.valid) {
<output class="readout">Every required field is filled in</output>
} @else {
<output class="readout missing">Fill in first: {{ missingNames(check) }}</output>
}
}
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-form-layer />
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
private readonly form = inject(EpdfForm);
private readonly stage = viewChild(EpdfStage);
private started = false;
protected readonly check = signal<FormValidation | null>(null);
constructor() {
// Fill in a name, then check the form, as if it was submitted.
effect(() => {
if (this.form.status() !== 'ready' || !this.stage() || this.started) return;
this.started = true;
untracked(() => {
void this.form
.setValue(toFieldRef('name'), { value: 'Ada Lovelace' })
.then(() => this.validate());
});
});
}
/** Check the required fields. */
private validate() {
const result = this.form.validate();
this.check.set(result);
return result;
}
/** Submit: check, and put the cursor in the first empty field. */
protected submit() {
const first = this.validate().missing[0];
if (first) this.form.focus(first.ref);
}
protected missingNames(check: FormValidation) {
return check.missing.map((field) => field.name).join(', ');
}
}Move between fields#
Tab and Shift+Tab move through the fields in the document’s order, from page to page, including pages that aren’t on screen: the view follows the field. On each page the fields come in the order the PDF gives: as its tags read (most tagged forms say so), by rows, by columns or as they were added. See Keyboard order. Fields the user can’t fill in are skipped, and a group of radio buttons is one stop, whose buttons the arrow keys move between. Tab on the last field, or Shift+Tab on the first, leaves the form, as it does on a web page.
Your code moves the cursor too. focus(ref) brings a field into view, on whatever page it is, and
puts the cursor in it; focusNext() goes on from the field the cursor is in, or was in last:
form.focus(email.ref); // the cursor in a field
form.focus(email.ref, { select: true }); // with its text selected, so typing replaces it
form.focusNext(); // the next field, as Tab does
form.focusNext({ backward: true }); // the one before
form.focusNext({ empty: true, wrap: true }); // the next empty field, starting over at the endBoth return false when there is no such field, or none the user can fill in. A field takes the
cursor as soon as it’s on screen; on iPhone and iPad, the keyboard opens for a field that was already
showing, and one that had to come into view opens it on a tap.
form.focusedField() is the field the cursor is in, or null, and
focusChanged$ tells you each time it moves, for a help text next to the
form or a step counter.
Calculations, formatting and validation#
Many forms add up totals, format dates and check values with scripts. With JavaScript on in the actions plugin, they run as people type, as in Acrobat:
import { Component } from '@angular/core';
import { withActions } from '@embedpdf/angular/actions';
import { withForm } from '@embedpdf/angular/form';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { engine } from './pdf';
@Component({
selector: 'app-form-viewer',
providers: [
provideEmbedPdf({ engine }, /* … */ withForm(), withActions({ javascript: { enabled: true } })),
],
template: `<!-- … -->`,
})
export class FormViewer {}When a script refuses a value, such as a date in the wrong format, the field keeps its old value,
the box keeps what was typed so it can be corrected, as in Acrobat, and
validationRejected$ tells you why, so you can show a message.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
signal,
ViewEncapsulation,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { withActions } from '@embedpdf/angular/actions';
import { EpdfForm, EpdfFormLayer, toFieldRef, withForm } from '@embedpdf/angular/form';
import { localEngine } from '@embedpdf/engine';
const invoice = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/invoice.pdf');
return { kind: 'bytes', id: 'invoice', bytes: new Uint8Array(await response.arrayBuffer()) };
};
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfFormLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: invoice }] },
withStage(),
withRender(),
withInteraction(),
withForm(),
// The form's scripts run with JavaScript on in the actions plugin.
withActions({ javascript: { enabled: true } }),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './scripts.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<!-- The amounts and the total are calculated by the form's scripts. -->
<div class="toolbar">
<output class="readout">Total: {{ totalText() }}</output>
@if (refused(); as refused) {
<output class="refused">{{ refused }}</output>
}
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-form-layer />
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
private readonly form = inject(EpdfForm);
private readonly total = this.form.valueOf(toFieldRef('invoice.total'));
protected readonly totalText = computed(() => {
const total = this.total();
return total && 'value' in total && total.value ? total.value : 'type a quantity';
});
protected readonly refused = signal<string | null>(null);
constructor() {
// A script refused a value, such as a date that isn't day/month/year.
this.form.validationRejected$
.pipe(takeUntilDestroyed())
.subscribe(({ field, issues }) =>
this.refused.set(`${field.name}: ${issues[0]?.message ?? 'refused'}`),
);
}
}The scripts read the form as the user sees it, values still on their way included, so nothing is read from the engine for them. Their engine starts when the first field of a form with scripts takes focus, so the first value doesn’t wait for it; a form without scripts never starts it.
Reset the form#
await form.reset(); // every field the user may fill in, back to its default
await form.reset([name.ref, email.ref]); // some fieldsMove form data in and out#
For your own backend, plain values are simplest:
const values = form.exportValues(); // { 'billing.name': 'Ada Lovelace', agree: true, … }
await form.importValues(values);Fields are matched by their full name. A field the form doesn’t have, or a value it can’t take, is skipped; the rest still land.
To copy the fields themselves to another document, with their boxes, values and scripts, take
them out as a bundle and import it there. FormTransfer turns a bundle into one JSON text to
store, and back:
const bundle = await form.export(); // every field; or form.export({ fields }), ({ pages })
const text = FormTransfer.stringify(bundle);
const { fields, dropped } = await otherForm.import(await FormTransfer.parse(text));The import adds the fields as one change you can undo. A field whose name the form has already is
left out and listed in dropped. By default each field keeps who created and filled it in, which
needs canRestoreAttribution(); pass { attribution: 'stamp' } to make them yours. See
Import & export.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
signal,
untracked,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfForm, EpdfFormLayer, FormTransfer, withForm } from '@embedpdf/angular/form';
import type { FormBundle } from '@embedpdf/angular/form';
import { localEngine } from '@embedpdf/engine';
const forms = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/forms.pdf');
return { kind: 'bytes', id: 'forms', bytes: new Uint8Array(await response.arrayBuffer()) };
};
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfFormLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: forms }] },
withStage(),
withRender(),
withInteraction(),
withForm(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './data.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<div class="toolbar">
<button type="button" class="button" (click)="export()">Export</button>
<button type="button" class="button" (click)="remove()">Remove</button>
<button type="button" class="button" [disabled]="!bundle()" (click)="import()">Import</button>
@if (result()) {
<output class="readout">{{ result() }}</output>
}
</div>
<div class="viewer">
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-form-layer />
</ng-template>
</epdf-stage>
<pre class="values">{{ values() }}</pre>
</div>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
private readonly form = inject(EpdfForm);
private started = false;
protected readonly bundle = signal<FormBundle | null>(null);
protected readonly result = signal('');
/** The values as plain data, keyed by full name: what you'd send to your backend. */
protected readonly values = computed(() => {
// The fields change with every value, so this reads them again each time.
this.form.fields();
return JSON.stringify(this.form.exportValues(), null, 2);
});
constructor() {
// On load: the form filled in.
effect(() => {
if (this.form.status() !== 'ready' || this.started) return;
this.started = true;
untracked(() => {
void this.form.importValues({
name: 'Ada Lovelace',
email: 'ada@example.com',
framework: 'Svelte',
});
});
});
}
protected async export() {
const exported = await this.form.export();
// One JSON text: what you'd store, and read back with FormTransfer.parse().
const text = FormTransfer.stringify(exported);
this.bundle.set(exported);
this.result.set(`Exported ${exported.fields.length} fields, ${text.length} characters of JSON`);
}
protected async remove() {
for (const field of this.form.list()) await this.form.delete(field.ref);
this.result.set('The form is gone');
}
protected async import() {
const bundle = this.bundle();
if (!bundle) return;
const { fields, dropped } = await this.form.import(bundle);
this.result.set(`Imported ${fields.length} fields, left out ${dropped.length}`);
}
}Methods#
What inject(EpdfForm) gives you for reading and filling. The verbs that change the form itself are
on Building forms.
| Method | What it does |
|---|---|
get(ref) | One field, with its value, or null when the form has no such field. Read the form |
getValue(ref) | One field’s value, in the shape setValue() takes, or null. In a component, use form.valueOf(ref). |
getFieldForWidget(widget) | The field a box belongs to, from the box’s ref |
getWidget(widget) | A box’s row: its page, its place (rect), its look and its state, or null when no page shows it |
getWidgets(ref) | Every box a field shows in, as rows in the field’s order: a radio group’s buttons, or a field repeated on several pages |
listWidgets(page) | Each field box on a page, with its place, its control, its value and whether it can be filled. For drawing the fields yourself. |
getWidgetAt(page, point) | The field box under a point on a page, with its field, or null |
setValue(ref, value) | Fills in a field with { value }, { checked } or { selectedValues }, running the form’s checks and scripts. Resolves { field, status }. Fill in a field |
setValues(entries) | Fills in several fields, each { ref, value }, as one change with one recalculation; a field you may not fill in, or a value it can’t take, fails on its own. Resolves { applied, skipped, failed }. |
importValues(values) | Fills in fields by full name from a plain object, as one change with one recalculation. Resolves { applied, skipped, failed }. Move form data in and out |
exportValues() | Every value, keyed by full name, as a plain object |
export(selection?) | Whole fields with their boxes, values and scripts, as a bundle: every field, or { fields } or { pages }. Move form data in and out |
import(bundle, options?) | Adds a bundle’s fields, as one change. Resolves { fields, refMap, dropped }. Needs canDesign(). Move form data in and out |
reset(refs?) | Puts back each field’s default, for every field the user may fill in or the fields you pass. Resolves { fields }, the fields it changed. Reset the form |
validate() | Checks the required fields the user may fill in. Returns { valid, missing }, the empty ones in page order. Check required fields |
focus(ref, options?) | Brings a field into view and puts the cursor in it; { select: true } selects its text. Returns false for a field the user can’t fill in. Move between fields |
focusNext(options?) | Moves the cursor to the next field, as Tab does: { backward, empty, wrap }. Returns false when there’s none. Move between fields |
getFocusedField() | The field the cursor is in, or null |
activateWidget(widget) | Does what a click on a field box does, such as running a button’s action |
getSnapshot() | Everything at once, as the user sees it: what the engine’s list() returns, with the values still on their way. fields, widgets, formKind, needsAppearances and calculationOrder |
refresh() | Reads the whole form again, and resynced$ fires. You rarely need it: the plugin follows every change, whoever made it. |
canRead(), canFill(ref) | Whether reading the form, and filling in a field, are allowed. Permissions |
getSettings(), updateSettings(changes), resetSettings() | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use inject(EpdfForm).settings(). Settings |
Every method that returns a promise also takes a signal in its options, to cancel it.
State#
Every value is a signal on inject(EpdfForm): read it in a template or a computed(), and your view updates when it changes. The method in the last column reads the same value once.
| Field | What it is | Read once |
|---|---|---|
fields() | Every field, with its value | list(), or list({ family, page, name }) for some |
status() | Whether the form is read: 'idle', 'loading', 'ready', 'forbidden' or 'error' | getStatus() |
formKind() | What the document has: 'none', 'acroform' (a regular PDF form) or 'xfa' | getFormKind() |
selectedField() | In design mode, the field of the selected box, or null; see Building forms | getSelectedField() |
focusedField() | The field the cursor is in, or null; see Move between fields | getFocusedField() |
For one field, form.valueOf(ref) updates only when that field’s value changes.
Settings#
Set them when you register the plugin, and change them while the app runs with
form.updateSettings({ … }); form.resetSettings() goes back to what you registered, and
inject(EpdfForm).settings() gives you the current settings.
| Setting | What it does | Default | From CSS |
|---|---|---|---|
validation | Run the form’s own checks ('scripts'), or accept every value ('none') | 'scripts' | — |
focus.color | The ring around the field being filled in | the viewer’s accent | --epdf-form-focus |
fields.border | The edge of a field that has no border of its own | the viewer’s accent at 55% | --epdf-form-field-border |
fields.background | Inside a field that has no background of its own | 'rgb(255 255 255 / 0.92)' | --epdf-form-field-background |
fields.color | The check mark and text of a field that has no color of its own | '#1f2a44' | --epdf-form-field-color |
fillable | Which fields people fill in by hand: true, false, or field families | true | — |
Scripts need JavaScript on in the actions plugin; without it, 'scripts' accepts every value too,
and a form with scripts logs a hint once.
fillable is for the people using your app. A field it leaves out draws as a read-only field
does, and takes no typing, clicks or picks; your code still fills it in with setValue() and
importValues(), and canFill(ref) doesn’t change:
withForm({ fillable: false }); // show the values, take no input
form.updateSettings({ fillable: ['text', 'checkbox'] }); // only these families
form.updateSettings({ fillable: true }); // every field againThe families are 'text', 'checkbox', 'radio', 'combobox', 'listbox', 'pushbutton' and
'signature' (signing a field is filling it in). To stop your code too, leave doc.forms.fill out
of the scope (see Permissions).
The colors are what the viewer draws around fields; a field’s own colors are saved in the PDF (see Building forms). A CSS variable wins over the setting: see Colors & theming.
Loading live preview…
import { ChangeDetectionStrategy, Component, inject, ViewEncapsulation } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfForm, EpdfFormLayer, withForm } from '@embedpdf/angular/form';
import { localEngine } from '@embedpdf/engine';
const forms = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/forms.pdf');
return { kind: 'bytes', id: 'forms', bytes: new Uint8Array(await response.arrayBuffer()) };
};
const COLORS = [
['Accent', null],
['Orange', '#ea580c'],
['Green', '#16a34a'],
] as const;
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfFormLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: forms }] },
withStage(),
withRender(),
withInteraction(),
// The colors the viewer draws around fields; `null` follows the viewer's accent.
withForm({ fields: { border: '#ea580c' } }),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './colors.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<div class="toolbar">
<span class="label">Edges</span>
<div class="segments">
@for (color of colors; track color[0]) {
<button
type="button"
class="segment"
[attr.aria-pressed]="form.settings().fields.border === color[1]"
(click)="form.updateSettings({ fields: { border: color[1] } })"
>
{{ color[0] }}
</button>
}
</div>
<span class="label">Focus</span>
<div class="segments">
@for (color of colors; track color[0]) {
<button
type="button"
class="segment"
[attr.aria-pressed]="form.settings().focus.color === color[1]"
(click)="form.updateSettings({ focus: { color: color[1] } })"
>
{{ color[0] }}
</button>
}
</div>
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-form-layer />
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
protected readonly form = inject(EpdfForm);
protected readonly colors = COLORS;
}Events#
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfForm } from '@embedpdf/angular/form';
import { autosave } from './api';
@Component({ selector: 'app-autosave', template: '' })
export class Autosave {
constructor() {
inject(EpdfForm)
.valueChanged$.pipe(takeUntilDestroyed())
.subscribe(({ field }) => autosave(field.name));
}
}| Event | Fires when | Carries |
|---|---|---|
valueChanged$ | A field’s value changed | field, origin |
validationRejected$ | The form’s validation refused a value | field, issues |
focusChanged$ | The cursor moved into a field, to another one, or out of the fields | field, widget |
resynced$ | The whole form was read, as when it opens, after refresh() or a repair | snapshot |
settingsChanged$ | The settings changed | settings, changed |
Fields that are added, changed or removed have their own events; see Building forms.
Permissions#
| Permission | What it allows | Check |
|---|---|---|
doc.forms.read | Reading the form | form.canRead() |
doc.forms.fill | Filling it in | form.canFill(ref) |
Without it, the fields show their values but can’t be changed. A user with a fields:fill
permission for one group fills in that group’s fields only: form.canFill(ref) answers per field,
the other fields can’t be changed and Tab skips them, validate() leaves them out, and reset()
leaves them as they are. See Signers and groups.
Good to know#
Forms and annotations#
A field’s widgets are the form’s: the annotation plugin never lists them. With the
annotation plugin registered, the <epdf-form-layer> and the annotation layer share the page: fields stay fillable with the
pointer tool, and become movable boxes in design mode.
Keyboard#
Tab and Shift+Tab move between fields in the document’s order (see Move between fields), and the arrow keys between the buttons of a radio group. In a text box, Enter confirms and Escape puts back the value it had.
Collecting responses#
On the local engine, the values live in the document in this tab until it’s downloaded.
XFA forms#
Some PDFs also carry a form in the older XFA format. The viewer works with the document’s regular fields and never runs XFA; see Forms.
Signed documents#
A signature can lock the whole form, or some fields. Locked fields show their values and can’t be changed; see Signatures.
Your feedback goes directly to the documentation team.