Skip to content

Custom configuration panels

A host application can replace the built-in property panel for a specific field type without modifying the layer.

The public config-panels prop receives an ordered Vue component registry. The first definition matching the selected field is rendered.

Public contract

ts
import type {
  QFormBuilderConfigPanelProps,
  QFormBuilderConfigPanelRegistry,
} from '@vevedh/qform-builder-layer/types'

Each definition contains:

PropertyTypePurpose
keystringStable panel identifier.
componentComponentVue component rendered in the right drawer.
matchstring | (field) => booleanMatches $formkit, $el, qformKind, configPanel, or a custom predicate.
prioritynumberOptional priority. The highest value wins.

The panel receives:

ts
export interface QFormBuilderConfigPanelProps {
  field: BuilderSchemaField
  builderId: string
  updateField: (patch: Partial<BuilderSchemaField>) => void
  close: () => void
}

Use updateField() to apply changes. It keeps the controlled schema, history and autosave synchronized.

Panel component

vue
<script setup lang="ts">
import type { QFormBuilderConfigPanelProps } from '@vevedh/qform-builder-layer/types'

const props = defineProps<QFormBuilderConfigPanelProps>()

const label = computed({
  get: () => String(props.field.label || ''),
  set: value => props.updateField({ label: value }),
})

const placeholder = computed({
  get: () => String(props.field.placeholder || ''),
  set: value => props.updateField({ placeholder: value }),
})
</script>

<template>
  <q-list separator>
    <q-item-label header>Business configuration</q-item-label>
    <q-item>
      <q-item-section class="q-gutter-md">
        <q-input v-model="label" filled dense label="Label" />
        <q-input v-model="placeholder" filled dense label="Placeholder" />
      </q-item-section>
    </q-item>
    <q-item>
      <q-item-section side>
        <q-btn flat no-caps label="Close" @click="close" />
      </q-item-section>
    </q-item>
  </q-list>
</template>

Register it in FormBuilder

vue
<script setup lang="ts">
import { markRaw } from 'vue'
import type { QFormBuilderConfigPanelRegistry } from '@vevedh/qform-builder-layer/types'
import BusinessTextFieldPanel from '~/components/forms/BusinessTextFieldPanel.vue'

const configPanels: QFormBuilderConfigPanelRegistry = [
  {
    key: 'business-text-field',
    component: markRaw(BusinessTextFieldPanel),
    match: field => field.$formkit === 'q-input' && field.inputType === 'text',
    priority: 100,
  },
]
</script>

<template>
  <FormBuilder builder-id="business-form" :config-panels="configPanels" />
</template>

Best practices

  • Use a stable and unique key.
  • Wrap components with markRaw() so Vue does not make the component definition reactive.
  • Never mutate field directly; use updateField().
  • Keep business rules and network calls in a store or repository.
  • Use a narrow predicate to avoid replacing unrelated field panels.

QForm Builder — Nuxt 4, Quasar and FormKit layer for dynamic forms.