Skip to content

Dynamic Fields ​

Dynamic fields let host apps customize FormBuilder inspector controls and write values directly into field objects, including nested metadata paths.

Why This Exists ​

Use dynamic fields when the default inspector rows are not enough, for example:

  • Assigning fields to users from an external system.
  • Capturing workflow metadata such as ticketReference or externalAccountId.
  • Showing or hiding default controls by product role.

Inspector Customization Model ​

FormBuilder supports two layers:

  • Declarative controls via fieldInspectorControls for common controls.
  • Escape hatch slot via field-inspector for fully custom UI.

Declarative Controls Example ​

vue
<template>
  <FormBuilder
    :pdf="pdfUrl"
    v-model="template"
    :fieldInspectorControls="fieldInspectorControls"
    :omitDefaultInspectorControls="['label']"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { FormBuilder } from '@signkit/core'
import type { FieldInspectorControl, Template } from '@signkit/core'

const pdfUrl = ref('/sample.pdf')
const template = ref<Template>({
  id: 'template-1',
  version: '1.0.0',
  pages: [],
  fields: [],
  createdAt: new Date().toISOString(),
})

const fieldInspectorControls: FieldInspectorControl[] = [
  {
    key: 'assignee-user',
    type: 'select',
    label: 'Assign user',
    path: 'meta.assigneeUserId',
    helpText: 'Choose a user from your upstream system',
    options: [
      { label: 'Unassigned', value: null },
      { label: 'Jordan Example', value: 'usr_1001' },
      { label: 'Robin Chen', value: 'usr_1002' },
    ],
  },
  {
    key: 'ticket-reference',
    type: 'text',
    label: 'Ticket reference',
    path: 'meta.ticketReference',
    placeholder: 'TKT-12345',
  },
]
</script>

Custom Slot Example ​

vue
<template>
  <FormBuilder :pdf="pdfUrl" v-model="template">
    <template #field-inspector="{ field, getValue, setValue, save }">
      <label>External account id</label>
      <input
        :value="String(getValue('meta.externalAccountId') || '')"
        @input="onExternalAccountInput($event, setValue, save)"
      />
      <small>Editing {{ field.id }}</small>
    </template>
  </FormBuilder>
</template>

<script setup lang="ts">
function onExternalAccountInput(
  event: Event,
  setValue: (path: string, value: unknown) => void,
  save: () => void,
) {
  const target = event.target as HTMLInputElement | null
  setValue('meta.externalAccountId', target?.value || '')
  save()
}
</script>

Update Behavior ​

  • Inspector edits are made on a draft copy first.
  • Save commits the draft back through the normal field update flow.
  • Nested paths such as meta.assigneeUserId are supported.
  • Use updateField from the slot for direct patch emissions when needed.

Best Practices ​

  • Keep custom keys under meta.* for backend-agnostic data.
  • Use stable key names so templates remain diff-friendly.
  • Keep control labels task-oriented for non-technical users.

Released under the Apache License 2.0.