Events
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 placed | field-placed | field-placed | field-placed |
| Field changed | field-updated | field-updated | field-updated |
| Field removed | field-removed | field-removed | field-removed |
| Signed status | signed-status-changed | signed-status-changed | signedStatus-changed |
| Pages viewed | update:viewedPages | — | viewedPages-changed |
| All pages read | all-pages-read | — | all-pages-read |
| Form-edit mode | update:formEditMode | — | formEditMode-changed |
| Stylus mode | update:stylusMode | — | stylusMode-changed |
| Scroll boundary | — | — | scroll-boundary |
| Bridge ready | — | — | ready |
| Tabs | — | update:activeTab, tab-added, tab-close, tab-removed | — |
Form fields
| Event | Payload | Fires 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-updatedper 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-updatedas an upsert keyed byfield.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
| Event | Payload | Fires 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
| Event | Payload | Fires 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
| Event | Payload | Fires 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:
| Event | Payload | Fires 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 event | Payload | Vue 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:
readyfires once the viewer inside the iframe has mounted.eventslists the events this iframe actually emits, andmethodsthe methods it supports — both areundefinedon older viewers that predate capability discovery, where you can assume everything is available.scroll-boundaryreports 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()