EmbedPDF

Filling forms

Many PDFs are forms: text boxes, checkboxes, dropdowns, signature fields. Register formPlugin() and put <FormLayer> in each page, and people can fill them in:

form-layer.svelte
<script lang="ts">
  import { FormLayer, formPlugin } from '@embedpdf/svelte/form';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Viewer } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { engine } from './pdf';

  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];
</script>

<Viewer {engine} {plugins}>
  <Stage>
    <RenderLayer />
    <FormLayer />
  </Stage>
</Viewer>

Loading live preview…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { FormLayer, formPlugin } from '@embedpdf/svelte/form';
  import { localEngine } from '@embedpdf/engine';
  import FormToolbar from './FormToolbar.svelte';

  import '../basic.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <FormToolbar />
    <Stage class="stage">
      <RenderLayer />
      <FormLayer />
    </Stage>
  </DocumentGate>
</Viewer>

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#

useFormState() gives you every field, and updates when a value changes:

field-list.svelte
<script lang="ts">
  import { useFormState } from '@embedpdf/svelte/form';

  const state = useFormState();
</script>

<ul>
  {#each state.fields as field (field.name)}
    <li>{field.name}: {field.family}</li>
  {/each}
</ul>

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 useFormValue():

invoice-total.svelte
<script lang="ts">
  import { toFieldRef, useFormValue } from '@embedpdf/svelte/form';

  const total = useFormValue(toFieldRef('invoice.total'));
</script>

{#if total.current && 'value' in total.current}
  <output>{total.current.value}</output>
{/if}

toFieldRef(name) makes a ref from a field’s full name, for when you know a field by name.

Loading live preview…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { FormLayer, formPlugin } from '@embedpdf/svelte/form';
  import { localEngine } from '@embedpdf/engine';
  import FieldList from './FieldList.svelte';
  import Greeting from './Greeting.svelte';

  import '../read.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <Greeting />
    <div class="viewer">
      <Stage class="stage">
        <RenderLayer />
        <FormLayer />
      </Stage>
      <FieldList />
    </div>
  </DocumentGate>
</Viewer>

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'] }); // list

It 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.

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…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { FormLayer, formPlugin } from '@embedpdf/svelte/form';
  import { localEngine } from '@embedpdf/engine';
  import FillToolbar from './FillToolbar.svelte';

  import '../fill.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <FillToolbar />
    <Stage class="stage">
      <RenderLayer />
      <FormLayer />
    </Stage>
  </DocumentGate>
</Viewer>

Check required fields#

A form marks some fields as required. Check them before you submit or download:

const { valid, missing } = form.validate();
 
const widget = !valid && form.getWidget(missing[0].widgets[0]);
if (widget) stage.reveal(widget.page, { rect: widget.rect });

missing lists the required fields that are empty, in page order, so you can take the user to the first one. Only the fields the user may fill in count: another signer’s are theirs to fill.

Loading live preview…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { FormLayer, formPlugin } from '@embedpdf/svelte/form';
  import { localEngine } from '@embedpdf/engine';
  import SubmitToolbar from './SubmitToolbar.svelte';

  import '../validate.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <SubmitToolbar />
    <Stage class="stage">
      <RenderLayer />
      <FormLayer />
    </Stage>
  </DocumentGate>
</Viewer>

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:

scripts.ts
import { actionsPlugin } from '@embedpdf/svelte/actions';
import { formPlugin } from '@embedpdf/svelte/form';

export const plugins = [/* … */ formPlugin(), actionsPlugin({ javascript: { enabled: true } })];

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 onValidationRejected tells you why, so you can show a message.

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 fields

Move 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…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { FormLayer, formPlugin } from '@embedpdf/svelte/form';
  import { localEngine } from '@embedpdf/engine';
  import DataToolbar from './DataToolbar.svelte';
  import Values from './Values.svelte';

  import '../data.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <DataToolbar />
    <div class="viewer">
      <Stage class="stage">
        <RenderLayer />
        <FormLayer />
      </Stage>
      <Values />
    </div>
  </DocumentGate>
</Viewer>

Methods#

What useForm() gives you for reading and filling. The verbs that change the form itself are on Building forms.

MethodWhat 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 useFormValue(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
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 onResynced 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 useFormSettings(). Settings

Every method that returns a promise also takes a signal in its options, to cancel it.

State#

Everything useFormState() gives you, as one reactive object: read its fields, and your markup updates when they change. The method in the last column reads the same value once.

FieldWhat it isRead once
fieldsEvery field, with its valuelist(), or list({ family, page, name }) for some
statusWhether the form is read: 'idle', 'loading', 'ready', 'forbidden' or 'error'getStatus()
formKindWhat the document has: 'none', 'acroform' (a regular PDF form) or 'xfa'getFormKind()
selectedFieldIn design mode, the field of the selected box, or null; see Building formsgetSelectedField()

For one field, useFormValue(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 useFormSettings() gives you the current settings.

SettingWhat it doesDefaultFrom CSS
validationRun the form’s own checks ('scripts'), or accept every value ('none')'scripts'—
focus.colorThe ring around the field being filled inthe viewer’s accent--epdf-form-focus
fields.borderThe edge of a field that has no border of its ownthe viewer’s accent at 55%--epdf-form-field-border
fields.backgroundInside a field that has no background of its own'rgb(255 255 255 / 0.92)'--epdf-form-field-background
fields.colorThe check mark and text of a field that has no color of its own'#1f2a44'--epdf-form-field-color
fillableWhich fields people fill in by hand: true, false, or field familiestrue—

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:

formPlugin({ fillable: false }); // show the values, take no input
form.updateSettings({ fillable: ['text', 'checkbox'] }); // only these families
form.updateSettings({ fillable: true }); // every field again

The 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…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { FormLayer, formPlugin } from '@embedpdf/svelte/form';
  import { localEngine } from '@embedpdf/engine';
  import ColorToolbar from './ColorToolbar.svelte';

  import '../colors.css';

  const engine = localEngine();
  // The colors the viewer draws around fields; `null` follows the viewer's accent.
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin(),
    formPlugin({ fields: { border: '#ea580c' } }),
  ];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <ColorToolbar />
    <Stage class="stage">
      <RenderLayer />
      <FormLayer />
    </Stage>
  </DocumentGate>
</Viewer>

Events#

autosave.svelte
<script lang="ts">
  import { useFormEvent } from '@embedpdf/svelte/form';
  import { autosave } from './api';

  useFormEvent(
    (form) => form.onValueChanged,
    ({ field }) => autosave(field.name),
  );
</script>
EventFires whenCarries
onValueChangedA field’s value changedfield, origin
onValidationRejectedThe form’s validation refused a valuefield, issues
onResyncedThe whole form was read, as when it opens, after refresh() or a repairsnapshot
onSettingsChangedThe settings changedsettings, changed

Fields that are added, changed or removed have their own events; see Building forms.

Permissions#

PermissionWhat it allowsCheck
doc.forms.readReading the formform.canRead()
doc.forms.fillFilling it inform.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 <FormLayer> and the annotation layer share the page: fields stay fillable with the pointer tool, and become movable boxes in design mode.

Keyboard#

Tab moves to the next field in the page’s order. 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.

Was this page helpful?

Your feedback goes directly to the documentation team.