Skip to main content

CnFormBuilder

Visual form composer. Three columns: a field-type palette (click to append), a reorderable field list, and a per-field config panel. Emits the live fields[] array via v-model so consumers can save / preview / render via CnFormDialog.

MVP. Drag-drop reorder is a follow-up — for now the field list uses up/down buttons. The v-model contract is forward-compatible so adding drag-drop later won't touch consumers.

Try it​

<template>
<div>
<CnFormBuilder v-model="fields" @save="onSave" />
<hr>
<CnFormDialog
v-if="preview"
:schema="{ properties: schemaProperties(fields) }"
:item="null"
@close="preview = false" />
</div>
</template>

<script>
import { CnFormBuilder, CnFormDialog } from '@conduction/nextcloud-vue'

export default {
components: { CnFormBuilder, CnFormDialog },
data() { return { fields: [], preview: false } },
methods: {
schemaProperties(fields) {
// Translate to the CnFormDialog `properties` shape.
const out = {}
for (const f of fields) {
out[f.key] = { title: f.label, type: f.type === 'number' ? 'number' : 'string', ...(f.options ? { enum: f.options } : {}) }
}
return out
},
async onSave(fields) {
await axios.post('/api/forms', { fields })
},
},
}
</script>

Field shape​

{
key: string,
type: 'string' | 'number' | 'boolean' | 'enum' | 'textarea',
label?: string,
placeholder?: string,
required?: boolean,
options?: string[], // only for type='enum'
}

Props​

PropTypeDefaultDescription
valueArray<Field>[]v-model. Current field list.
modelValueArray<Field>undefinedThe same value under Vue 3's v-model name. v-model binds THIS, not value — both are accepted.
availableTypesArray<{type,label,icon?}>default 5Extend / replace the palette.
title / descriptionString''Optional header.
hidePreviewBooleanfalseHide the JSON preview footer.
paletteTitle / fieldsTitle / editorTitle / previewTitleString—Column headers.
emptyLabel / noSelectionLabel / untitledKeyString—State texts.
keyLabel / typeLabel / labelLabel / placeholderLabel / requiredLabel / optionsLabel / optionsPlaceholderString—Per-field editor labels.
moveUpLabel / moveDownLabel / deleteLabelString—Row-action button titles.

Why two props. Vue 3 compiles v-model="x" to :modelValue + @update:modelValue. A component declaring only value/input never receives the prop and its emit is never heard — silently. value is kept as the public name for existing consumers; modelValue is what a plain v-model binds, and both emit on every change.

Events​

EventPayloadDescription
inputArray<Field>v-model emit on every mutation.
saveArray<Field>Triggered by the public save() method. The parent renders the actual save UI.

Public methods​

MethodDescription
save()Emit a @save with a copy of the current model.

Follow-up​

  • Drag-drop reorder (HTML5 dragstart/dragover/drop on field rows).
  • Per-field validation rule editor (regex, min/max).
  • Live preview via embedded CnFormDialog.
  • Conditional visibility rules (showIf).