API

Events

Every event KViewer, KViewerTabs, and the embed bridge emit — payloads, timing, and how to keep host state in sync

KViewer pushes state changes to the host through events. The same events reach three surfaces:

  • <KViewer> — Vue emits (@field-placed, v-model:form-edit-mode, …).
  • <KViewerTabs> — forwards a subset of the viewer events from the active tab's viewer, plus its own tab events.
  • The embed bridge — a cross-origin parent page subscribes via KViewerEmbedClient.on(). Some bridge events use different names and payload shapes than their Vue counterparts; see Embed bridge.

Overview

Topic<KViewer><KViewerTabs>Embed bridge
Field placedfield-placedfield-placedfield-placed
Field changedfield-updatedfield-updatedfield-updated
Field removedfield-removedfield-removedfield-removed
Signed statussigned-status-changedsigned-status-changedsignedStatus-changed
Pages viewedupdate:viewedPages—viewedPages-changed
All pages readall-pages-read—all-pages-read
Form-edit modeupdate:formEditMode—formEditMode-changed
Stylus modeupdate:stylusMode—stylusMode-changed
Scroll boundary——scroll-boundary
Bridge ready——ready
Tabs—update:activeTab, tab-added, tab-close, tab-removed—

Form fields

EventPayloadFires when
field-placed(field: FormFieldDefinition)The user places a field with a placement tool, in or out of form-edit mode. field already includes any fieldDefaults.
field-updated(field: FormFieldDefinition, patch: Partial<FormFieldDefinition>)The user changes a field through the viewer UI: moving or resizing it with the field chrome, or editing its properties in the form-edit sidebar. field is the full definition after the change; patch holds only the changed properties.
field-removed(field: FormFieldDefinition)The user removes a field through the viewer UI: selection-popover delete, Del/Backspace, or the form-edit sidebar. field is the last-known definition.

See FormFieldDefinition for the payload type.

User changes only

The field events report what the user did. Changes the host makes through addFormField(), updateFormField(), or removeFormField() do not fire an event — the host already knows about them. If you keep a copy of the field list, apply your own calls to it directly instead of waiting for an event.

Frequency

  • Moving or resizing a field with the chrome fires one field-updated per gesture, on release — not one per pointer move.
  • Editing a property in the form-edit sidebar fires once per keystroke while typing a field name, for example. Treat field-updated as an upsert keyed by field.id, and debounce it yourself if every event triggers a network request.

Keeping host state in sync

With these rules, the host never needs to poll getFormFields() or reconcile against it:

<template>
  <KViewer
    ref="viewer"
    :source="pdfUrl"
    :tools="tools"
    editable-placed-fields
    @field-placed="onFieldPlaced"
    @field-updated="(field) => fields.set(field.id, field)"
    @field-removed="(field) => fields.delete(field.id)"
  />
</template>

<script setup lang="ts">
import type { FormFieldDefinition } from 'kviewer'

const viewer = ref()
const fields = reactive(new Map<string, FormFieldDefinition>())
let signatureCount = 0

function onFieldPlaced(field: FormFieldDefinition) {
  // updateFormField() doesn't fire field-updated, so record the rename here too
  const fieldName = field.fieldType === 'signature'
    ? `signature_${++signatureCount}`
    : field.fieldName
  viewer.value.updateFormField(field.id, { fieldName })
  fields.set(field.id, { ...field, fieldName })
}
</script>

Signing

EventPayloadFires when
signed-status-changed({ allSigned: boolean, fields: SignatureFieldStatus[] })A signature field's signed state changes: signed, cleared, added, removed, or renamed.

This is the push-style counterpart to getSignedStatus() and getSignatureFieldStatus(). allSigned is vacuously true when the document has no signature fields — check fields.length when at least one signature must exist.

Read tracking

EventPayloadFires when
update:viewedPages(pages: number[])A page is viewed for the first time, or tracking is reset. pages is the ascending list of pages seen so far.
all-pages-read—The last unseen page is viewed. Fires once; re-arms after resetViewedPages() or loading a new document.

A page counts as viewed once it has scrolled into the viewport. See allPagesRead() for the pull-style check.

Mode changes

EventPayloadFires when
update:formEditMode(enabled: boolean)Form-edit mode is toggled from the toolbar or by setFormEditMode(). Backs v-model:form-edit-mode.
update:stylusMode(enabled: boolean)Stylus mode is toggled from the page-settings menu, the toolbar indicator, the stylus hint, or setStylusMode(). Backs v-model:stylus-mode.

Tabs

<KViewerTabs> emits its own tab events:

EventPayloadFires when
update:activeTab(id: string)The active tab changes.
tab-added(tab: ViewerTabItem)A tab is created.
tab-close(id: string)A tab is about to be removed.
tab-removed(id: string)A tab has been removed.

It also forwards field-placed, field-updated, field-removed, and signed-status-changed from the active tab's viewer, with the same payloads as <KViewer>. Read-tracking and mode events are not forwarded.

Embed bridge

A parent page that embeds the viewer in an iframe subscribes with KViewerEmbedClient.on(), which returns an unsubscribe function. Bridge payloads are always a single object, so events with multiple Vue arguments are wrapped:

Bridge eventPayloadVue equivalent
ready{ protocolVersion: number, methods?: MethodName[], events?: EventName[] }—
field-placed{ field }field-placed
field-updated{ field, patch }field-updated
field-removed{ field }field-removed
signedStatus-changed{ allSigned, fields }signed-status-changed
viewedPages-changed{ pages: number[], allRead: boolean }update:viewedPages
all-pages-read{}all-pages-read
formEditMode-changed{ enabled: boolean }update:formEditMode
stylusMode-changed{ enabled: boolean }update:stylusMode
scroll-boundary{ edge: 'start' | 'end', deltaY: number }—

Behavior matches the Vue events: field events report user changes only, and all-pages-read fires once per document.

Two events exist only on the bridge:

  • ready fires once the viewer inside the iframe has mounted. events lists the events this iframe actually emits, and methods the methods it supports — both are undefined on older viewers that predate capability discovery, where you can assume everything is available.
  • scroll-boundary reports vertical scroll the viewer couldn't consume because it reached the start or end of the document, so the parent page can continue scrolling itself.
const client = new KViewerEmbedClient(iframe, { iframeOrigin: 'https://viewer.example.com' })

client.on('ready', ({ events }) => {
  console.log('viewer emits', events)
})

const stop = client.on('field-updated', ({ field, patch }) => {
  fields.set(field.id, field)
})

client.on('signedStatus-changed', ({ allSigned }) => {
  finalizeButton.disabled = !allSigned
})

// later
stop()
client.dispose()
Copyright © 2026