EmbedPDF

Building forms

The form plugin can also add fields to a PDF, and change or remove the ones it has, so people can turn any document into a form. Switch to design mode, pick a field tool, and click or drag on the page:

field-palette.svelte
<script lang="ts">
  import { useInteraction, useInteractionState } from '@embedpdf/svelte/interaction';

  const FIELD_TOOLS = [
    ['form-text', 'Text'],
    ['form-checkbox', 'Checkbox'],
    ['form-radio', 'Radio button'],
    ['form-combobox', 'Dropdown'],
    ['form-listbox', 'List'],
    ['form-signature', 'Signature'],
  ] as const;

  const interaction = useInteraction();
  const state = useInteractionState();
</script>

{#each FIELD_TOOLS as [id, label] (id)}
  <button aria-pressed={state.activeToolId === id} onclick={() => interaction.activateTool(id)}>
    {label}
  </button>
{/each}

Building needs the annotation plugin next to the form plugin: in design mode, fields are boxes you select, move, resize and style like any annotation.

ToolPlacesA click makes
form-textA text box160 × 24
form-checkboxA checkbox18 × 18
form-radioA radio button18 × 18
form-comboboxA dropdown140 × 24
form-listboxA list140 × 72
form-buttonA push button100 × 28
form-signatureA field someone signs later160 × 48

A see-through preview of the field follows the pointer, drawn as the field will be, so people see where it lands before they click. Drag instead of clicking for another size. The tool stays active, so placing ten checkboxes is ten clicks.

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 { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { FormLayer, formPlugin } from '@embedpdf/svelte/form';
  import { localEngine } from '@embedpdf/engine';
  import FieldPalette from './FieldPalette.svelte';

  import '../designer.css';

  const engine = localEngine();
  // Building forms needs the annotation plugin: in design mode, fields are boxes like any annotation.
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin(),
    annotationPlugin(),
    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}
    <FieldPalette />
    <Stage class="stage">
      <RenderLayer />
      <AnnotationLayer />
      <FormLayer />
    </Stage>
  </DocumentGate>
</Viewer>

Design mode#

The form-edit tool is design mode: fields stop being fillable and become boxes you can select, move and resize. The pointer tool makes them fillable again:

interaction.activateTool('form-edit'); // design the form
interaction.activateTool('pointer'); // fill it in

In design mode the form lends its boxes to the annotation plugin, which selects, moves, resizes, restyles, reorders and deletes them with its own tools, next to the annotations. Every edit of a box is still the form’s: its place or look is updateWidget(), and deleting every box of a field deletes the field. A drag of two fields and a stamp is one change: one request, and one step to undo. Like every change, it shows at once and the engine’s answer follows.

A field just placed is selected at once, before the engine answers, and the tool goes back to form-edit rather than the default pointer, so design mode goes on. Without canDesign() nothing is lent: the fields stay fillable boxes the annotation tools leave alone.

Change a field’s settings#

useFormState().selectedField is the field of the selected box, for a settings panel:

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

  const form = useForm();
  const state = useFormState();
</script>

{#if state.selectedField}
  {@const field = state.selectedField}
  <input value={field.name} onblur={(e) => form.update(field.ref, { name: e.currentTarget.value })} />
  <label>
    <input
      type="checkbox"
      checked={field.required}
      onchange={(e) => form.update(field.ref, { required: e.currentTarget.checked })}
    />
    Required
  </label>
{/if}

update() takes only what you change, with the same settings a field reads:

SettingWhat it is
nameIts own part of its name: renaming customer.email to phone makes customer.phone
requiredPDF apps ask for a value before the form is submitted
readOnlyPeople can’t change the value
alternateNameThe tooltip, also read by screen readers
defaultValueWhat a reset puts back
optionsFor a dropdown or list: { label, value } pairs
maxLength, multiline, password, combFor a text box

getFieldSettings(ref) describes a field’s settings for a panel you draw without knowing the field’s family: { properties, values }, each property a key (what update() takes), a control (text, flag, number, choice, choices or options), and a label in English with its translation key, labelKey (form.settings.tooltip), in display order. A choice‘s options are the field’s own values, shown as they are.

A field’s look, such as its border, background and font, belongs to its box: change it like any annotation’s, with the style panel, or from code with form.updateWidget(widget.ref, { color: '#1d4ed8' }).

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 { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { FormLayer, formPlugin } from '@embedpdf/svelte/form';
  import { localEngine } from '@embedpdf/engine';
  import FieldSettings from './FieldSettings.svelte';

  import '../field-settings.css';

  const engine = localEngine();
  const plugins = [
    stagePlugin(),
    renderPlugin(),
    interactionPlugin(),
    annotationPlugin(),
    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}
    <div class="viewer">
      <Stage class="stage">
        <RenderLayer />
        <AnnotationLayer />
        <FormLayer />
      </Stage>
      <FieldSettings />
    </div>
  </DocumentGate>
</Viewer>

Add fields from code#

create() takes the same settings the engine takes, with the page and the box for each place the field shows:

const { field } = await form.create({
  family: 'combobox',
  name: 'country',
  options: [
    { label: 'Netherlands', value: 'NL' },
    { label: 'Belgium', value: 'BE' },
  ],
  widgets: [{ page: page.ref, rect: { x: 72, y: 252, width: 148, height: 20 } }],
});

A radio group is one field with a box per button, each with the value it stands for:

await form.create({
  family: 'radio',
  name: 'plan',
  widgets: [
    { page: page.ref, rect: { x: 72, y: 136, width: 16, height: 16 }, exportValue: 'monthly' },
    { page: page.ref, rect: { x: 72, y: 166, width: 16, height: 16 }, exportValue: 'yearly' },
  ],
});

A field placed with a tool gets a name like text_1; rename it in your settings panel. Every family’s settings are on Creating form fields.

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 BuildToolbar from './BuildToolbar.svelte';

  import '../create.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}
    <BuildToolbar />
    <Stage class="stage">
      <RenderLayer />
      <FormLayer />
    </Stage>
  </DocumentGate>
</Viewer>

One more button for a group#

addWidget(ref) shows a field in one more place: for a radio group a new choice, for a checkbox a box that checks together with the others.

const { widget } = await form.addWidget(plan.ref);
annotation.selection.set([widget.ref]); // in design mode, so it can be dragged

Without a place, it goes next in line: below the last button if they stand in a column, beside it if in a row, at the same size and the same spacing, starting a new column where the page ends. It looks like the field’s last button, and a radio group’s new choice gets the next free Choice<n>. Pass what you want instead: form.addWidget(plan.ref, { rect, exportValue: 'yearly' }) takes everything an entry of create()‘s widgets takes.

Scripts and calculations#

A field’s scripts go in actions when you create or change it: a calculated total, a formatted amount, a check on what the user typed. The viewer runs them while the user fills the form in.

await form.update(total.ref, {
  actions: {
    calculate: {
      type: 'javascript',
      script: 'event.value = this.getField("subtotal").value * 1.21;',
    },
  },
});
await form.reorderCalculations([tax.ref], { before: total.ref });

A calculate script puts the field at the end of the order calculations run in; reorderCalculations() moves it. Writing a script needs canWriteScripts(). See Scripts and calculations.

A box can do something when it’s clicked, too, such as reset the form: give its entry of widgets in create() its actions. A script, a submit or a web link there needs canWriteScripts() as well. See Widget actions. A push button (family: 'pushbutton') is a box that only does that, with a caption; see A push button.

Remove a field#

await form.delete(field.ref);

This removes the field and every box it has. To remove one box of a field that shows in several places, such as a reference number on every page, use form.deleteWidget(widget.ref). It resolves the field as it is now. In design mode, deleting the selection does the same: one box goes from its field, and a field whose every box is selected goes whole.

Every building method shows its change at once, is one step to undo, and resolves once the engine answered.

Methods#

What useForm() gives you for building forms. Reading, filling and the form’s state are on Filling forms.

MethodWhat it does
create(draft)Adds a field, with a box for each place it shows. Resolves { field }. Add fields from code
update(ref, patch)Changes a field’s settings; pass only what changes. Resolves { field }. Change a field’s settings
addWidget(ref, placement?)Shows a field in one more place: a radio group’s next choice, a checkbox’s next box. Resolves { field, widget }. One more button for a group
delete(ref)Removes a field and every box it has. Remove a field
updateWidget(widget, patch)Changes one box’s place or look; pass only what changes. Resolves { widget }. Change a field’s settings
getFieldSettings(ref)A field’s settings described for a panel: { properties, values }, or null. Change a field’s settings
deleteWidget(widget)Removes one box from its page, and from its field when it has one. Resolves { field }, or { field: null } for a box in no field. Remove a field
removeWidget(ref, widget)Takes one box out of its field; it stays on the page, in no field, until deleteWidget() removes it
reorderCalculations(fields, position)Moves fields in the order calculate scripts run in. Resolves { calculationOrder }. Scripts and calculations
repair(options?)Fixes a form other PDF apps read differently, such as fields missing from the form’s list, and resolves what it fixed. See Repair a broken form.
canDesign()Whether adding, changing and removing fields is allowed. Permissions
canRestoreAttribution()Whether an import may keep who created and filled in its fields, instead of naming the importing user. Permissions
canWriteScripts()Whether scripts, submit buttons and links may be written into fields and their boxes. Permissions

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

Events#

select-new-field.svelte
<script lang="ts">
  import { useFormEvent } from '@embedpdf/svelte/form';
  import { selectField } from './editor';

  useFormEvent(
    (form) => form.onFieldCreated,
    ({ field }) => selectField(field),
  );
</script>
EventFires whenCarries
onFieldCreatedA field was addedfield, origin
onFieldUpdatedA field’s settings changedfield, origin
onFieldDeletedA field was removedref, origin

Permissions#

PermissionWhat it allowsCheck
doc.forms.modifyAdding, changing, removing and repairing fieldsform.canDesign()
doc.forms.importImporting fields and values with who created and filled them inform.canRestoreAttribution()
doc.forms.scriptWriting scripts, submit buttons and links into fields and boxesform.canWriteScripts()

Hide the field tools when canDesign() is false, and the script, submit and link options of a field’s or a box’s actions when canWriteScripts() is false. Scripts already in the form run either way.

Good to know#

Fields and boxes#

A field is the data: its name, its value, its settings. A widget is a box where the field shows on a page. Most fields have one; a radio group has one per button, and a field can show on several pages, all with the same value.

Signature fields#

A signature field is a place for someone to sign later. Signing it is on Signatures.

Was this page helpful?

Your feedback goes directly to the documentation team.