Skip to content

Custom component CRUD

QForm Builder can manage a local registry of serializable business components. The registry is available from the Custom tab in the left panel. It supports creating, editing, duplicating, importing, exporting and deleting a definition, then adding an instance to the form.

A definition contains a stable key, title, icon, description and field schema. The schema remains QForm Builder JSON: it contains no functions and cannot inject an arbitrary Vue component.

Meal-voucher example

ts
import type { QFormBuilderCustomComponentRegistry } from '@vevedh/qform-builder-layer/types'

const customComponents: QFormBuilderCustomComponentRegistry = [
  {
    key: 'dsi-meal-voucher-information',
    title: 'New meal-voucher offer',
    description: 'Configurable business information block.',
    icon: 'restaurant',
    schema: {
      $el: 'div',
      name: 'tickets_restaurant_information',
      label: 'New meal-voucher offer',
      qformKind: 'dsi-meal-voucher-information',
      dsiComponent: 'dsi-meal-voucher-information',
      ignore: true,
      informationTitle: 'Lunch offer changes',
      informationParagraphs: [
        'The meal-voucher face value changes from €100.00 to €100.25.',
        'One additional voucher is allocated to each formula: 21 vouchers for formula 1 and 26 vouchers for formula 2.',
        'The split remains unchanged: 60% paid by the employer and 40% paid by the employee.',
        'This new offer permanently replaces the current offer.',
      ],
      informationWarning: 'Without consent submitted by 30 October 2026, the current lunch offer will be interrupted and remain suspended until the situation is regularized.',
      deadline: '2026-10-30T23:59:59-04:00',
      faceValue: 100.25,
      employerRatePercent: 60,
      employeeRatePercent: 40,
      formulas: [
        {
          key: 'formula_1',
          label: 'Formula 1',
          titleCount: 21,
          totalValue: 2105.25,
          employerContribution: 1263.15,
          employeeContribution: 842.1,
        },
        {
          key: 'formula_2',
          label: 'Formula 2',
          titleCount: 26,
          totalValue: 2606.5,
          employerContribution: 1563.9,
          employeeContribution: 1042.6,
        },
      ],
      attrs: {
        class: 'dsi-meal-voucher-information-placeholder q-pa-md rounded-borders',
      },
    },
  },
]
vue
<FormBuilder
  v-model:schema="schema"
  v-model:custom-components="customComponents"
/>

Top-level business properties are inferred automatically:

  • short strings: text input;
  • long strings: textarea;
  • numbers: numeric input;
  • booleans: toggle;
  • string arrays: list editor;
  • structured objects and arrays: validated JSON editor;
  • Base64 JPEG, PNG or WebP images: thumbnail and size without exposing the complete string.

An inserted instance keeps qformCustomComponentKey, qformCustomComponentVersion and qformCustomProperties. The right panel uses this metadata to expose editable properties.

Persistence

By default, the registry is saved in local storage with a key derived from builderId:

text
qform-builder:<builderId>:custom-components

Persistence can be disabled:

vue
<FormBuilder :custom-components-autosave="false" />

An explicit key can be supplied:

vue
<FormBuilder custom-components-storage-key="dsi:components:forms" />

For server persistence, use v-model:custom-components and store the registry through the host NFZ/Feathers business service.

Public API

ts
const builder = ref<InstanceType<typeof FormBuilder> | null>(null)

builder.value?.getCustomComponents()
builder.value?.createCustomComponent(definition)
builder.value?.updateCustomComponent('old-key', definition)
builder.value?.removeCustomComponent('my-key')
builder.value?.importCustomComponents(json)
const exported = builder.value?.exportCustomComponents()

Available events:

text
update:customComponents
custom-component-create
custom-component-update
custom-component-remove
custom-component-import
custom-component-export

Security and limits

The registry reuses the public schema sanitizer. Prototype-pollution keys, on* handlers, $cmp, dangerous HTML tags, active URLs and active CSS are rejected. The registry is limited to 200 definitions, 100 properties per definition and 2 MB per JSON import.

Deleting a definition only removes it from the catalogue. Existing form instances remain intact to avoid business-data loss.

Validation and automation

The renderer accepts extensible locale codes supplied by the host application. The generic formatter explicitly falls back to French, except for codes starting with en, which use English formatting.

For browser tests, the CRUD dialogs expose stable selectors: data-qform-custom-component-editor-dialog, data-qform-custom-component-delete-dialog and data-qform-custom-component-transfer-dialog. Content assertions should prefer exact semantic roles, such as formula headings, to avoid partial matches inside business paragraphs.

Graphical drag-and-drop designer

From Custom > Create component, the Visual designer tab builds the presentation without requiring hand-written schema JSON. The palette provides heading, styled text, paragraph, warning, value card, formula grid, section, columns, list, badge, button, embedded image, divider and spacer blocks.

A block can be inserted by click or drag-and-drop, reordered on the canvas, duplicated or removed. Its inspector binds it to a business property such as informationTitle, informationParagraphs.0 or formulas. Missing properties are created with a suitable serializable default value.

The designer stores a versioned qformCustomLayout object in the definition and every inserted instance. The Advanced JSON tab remains available for business structures that are not yet covered graphically.

Component icon, QColor and styled text

The component icon is selected from a searchable visual catalog powered by quasar-ui-qiconpicker. Normalization only preserves Material identifiers made of letters, digits and underscores. The registry therefore remains serializable and cannot inject an arbitrary icon component.

Background, text and border colors now use a QColor picker. A value can be selected from the palette or entered as HEX, RGB(A), an allowed Quasar token or a var(--q-...) variable. Values outside that contract are rejected before persistence.

The Styled text block stores plain text only, never HTML. Its inspector controls font family, a 10–96 px size, weight, line height, letter spacing, italic, underline, case transform, text shadow, and a linear or radial gradient. Font families, shadows and transforms come from fixed safe lists, while gradient colors pass through the same normalizer as the other visual styles.

ts
{
  id: 'text-hero',
  type: 'text',
  propertyPath: 'heroText',
  text: 'Presentation text',
  textOptions: {
    fontFamily: 'georgia',
    fontSize: 34,
    fontWeight: 700,
    lineHeight: 1.2,
    letterSpacing: 0.5,
    shadow: 'medium',
    gradientType: 'linear',
    gradientFrom: '#b45309',
    gradientTo: '#7c3aed',
    gradientAngle: 90,
  },
}

Quasar and UnoCSS styles

The complete component and each block expose appearance controls for colors, borders, spacing, radius, shadow, alignment and full width. Advanced classes are selected from a safe Quasar and UnoCSS allowlist; arbitrary class input is not executed directly. The class selector is searchable: enter a value such as q-pa-lg, bg-primary or grid-cols-2 to filter the catalog before selecting it.

ts
layout: {
  version: 1,
  rootStyle: {
    backgroundColor: 'blue-grey-1',
    padding: 'lg',
    radius: 'lg',
    classes: ['w-full', 'shadow-sm'],
  },
  blocks: [
    {
      id: 'heading-1',
      type: 'heading',
      propertyPath: 'informationTitle',
      style: { textColor: 'primary', alignment: 'left' },
    },
  ],
}

Paths containing __proto__, prototype or constructor, active CSS colors and classes outside the allowlist are rejected. The registry remains portable across the builder, FormViewer, import/export and server persistence.

WYSIWYG canvas and direct editing

The designer canvas is a direct editor rather than a read-only preview. Click inside a heading, paragraph, warning or value card to update the bound business property without leaving the canvas. Numeric values keep their number type, and indexed paths such as informationParagraphs.0 are updated through a protected detached schema copy.

Formula grids expose the primitive properties of each row as inputs. Strings and numbers use text or numeric inputs, while booleans use a toggle so their type is preserved. Formula rows can be added or removed graphically. More complex objects remain available through the Advanced JSON tab.

Blocks are moved from a dedicated drag handle. The block card is the only native drag source: pointer-down on the handle arms the card, while drag starts from inputs or buttons are rejected. This keeps direct editing separate from reordering. Before/after drop indicators become visible during a drag. Move up, Move down, Alt + ArrowUp and Alt + ArrowDown provide deterministic keyboard alternatives.

Every direct edit immediately synchronizes:

  • the business schema value;
  • the canvas rendering;
  • the advanced JSON representation;
  • the definition stored in the registry.

Prototype-pollution path segments and unreasonable array indexes are rejected by the designer’s internal path writer.

12-column layout

The canvas uses a responsive 12-column grid. Every block has a width from 1/12 to 12/12. Width can be changed directly from the card with the and + buttons, by dragging the lower-right resize grip, or from the inspector with the slider and 3/12, 4/12, 6/12, 8/12 and 12/12 presets.

Adjacent blocks share a row while their combined width does not exceed 12. On narrow screens, blocks automatically collapse to one column to preserve readability.

The Columns block provides its own 12-column inner grid. Each inner card contains a title, text, Quasar icon and configurable span. Up to six columns can be added graphically. The Section block structures a visual area with a title, description, icon and the usual style controls.

Additional blocks cover common UI composition needs:

  • List: a string collection edited one row at a time;
  • Badge: a highlighted status or short value;
  • Button: label, icon and safe URL (https, mailto, tel, relative path or anchor);
  • Section: a semantic and visual information boundary;
  • Columns: editable cards aligned on an inner grid.

Width is serialized in qformCustomLayout.blocks[].span. Older definitions without span remain compatible and default to 12/12.

Embedded Base64 image

The Embedded image block accepts JPEG, PNG and WebP raster files only. A source file may be up to 10 MB, but it is resized and re-encoded in the browser before the result is stored in the block business property as data:image/...;base64,.... SVG and GIF are rejected to avoid active content and uncontrolled animation.

The inspector controls:

  • a target size from 16 to 128 KB;
  • WebP, JPEG or PNG output;
  • maximum encoder quality;
  • maximum source dimension;
  • alternative text, caption and contain, cover, fill or scale-down fitting;
  • internal image width and pixel height.

After each encoding pass, the designer shows the source size and optimized size so the reduction remains measurable. The optimizer first searches for the highest JPEG or WebP quality that fits the target. When quality reduction is not enough, it progressively reduces pixel dimensions until it reaches the target or the best safe result. Embedded output is capped at 128 KB so versioned documents and JSON imports remain within their established string limits.

The image can be resized directly on the canvas from its lower-right handle. Left and right arrows change width in 5% steps, while up and down arrows change height. This internal size is separate from the block width in the main grid: the block still owns a span from 1 to 12 columns.

The runtime validates MIME type, Base64 encoding and size again before producing the <img> element. Rendering uses loading="lazy" and decoding="async". In an inserted instance properties panel, the Base64 property is shown as a thumbnail with its size instead of a long textarea.

ts
{
  id: 'image-hero',
  type: 'image',
  propertyPath: 'embeddedImage',
  span: 12,
  text: 'Service overview',
  image: {
    alt: 'Team meeting in the project room',
    widthPercent: 85,
    height: 320,
    fit: 'cover',
    targetKb: 96,
    quality: 0.82,
    format: 'webp',
    maxDimension: 1600,
  },
}

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