Guides

Add a field widget

Write a Vue component for a field of a content type.

A field widget is the input that the generic form shows for one field. Core has widgets for most field types. A layer adds a widget for richtext or media, or a special widget for one field.

The contract

Every widget has the same three props and one event:

NameKindMeaning
fieldPropThe field definition. The form passes a translated copy, so the label is ready to show. The copy is made only when the form knows the content type name. Otherwise the field keeps its own text.
modelValuePropThe current value
disabledProptrue when the form is read-only
update:modelValueEventEmit it with the new value
app/components/MyColorField.vue
<script setup lang="ts">
defineProps<{
  field: PlutoField
  modelValue: unknown
  disabled?: boolean
}>()

defineEmits<{ 'update:modelValue': [value: string] }>()
</script>

<template>
  <UFormField :label="field.label" :description="field.description">
    <UInput
      :model-value="String(modelValue ?? '')"
      :disabled="disabled"
      @update:model-value="$emit('update:modelValue', String($event))"
    />
  </UFormField>
</template>

Register the widget

Register the widget with contentFieldWidgets and defineAsyncComponent:

app/plugins/pluto-extension.ts
const MyColorField = defineAsyncComponent(() => import('../components/MyColorField.vue'))

export default defineNuxtPlugin(() => {
  definePlutoExtension({
    id: 'my-layer',
    contentFieldWidgets: [
      { id: 'my-color', widget: 'my-color', component: MyColorField }
    ]
  })
})

Core shows a field skeleton while the chunk loads.

Choose by name or by type

An entry can match in two ways:

  • widget: 'my-color'. A field selects the widget with widget: 'my-color'. This affects only that field.
  • fieldType: 'media'. The widget becomes the default for every field of that type.

Resolution order: an exact field.widget name first. Then the last registered widget whose fieldType matches the field type. Otherwise nothing, and the form shows the message No widget registered for field type "<type>".

Register a widget by widget name when it is for one layer. A fieldType entry becomes the app-wide default for that type, and it can take the fields of other layers.

Core has no richtext widget on purpose. @plutocms/supabase-blog is the default for richtext. Core has the media widget: the picker of the media library. The Git CMS registers a Markdown widget for richtext in CMS mode. The @plutocms/docs layer registers an mdc widget by name. A field selects it with widget: 'mdc'.

Use a widget in a field

{ name: 'color', type: 'text', label: 'Color', widget: 'my-color' }

Share state between widgets

Each widget mounts on its own and cannot read the props of another widget. If a rule spans fields, share the state with useState. supabase-shop does this for the custom, availability, and stock rule. Reset the state when the widgets unmount, so the next form starts clean.

Load your own options

A content field widget is mounted dynamically. An async composable with a top-level await can suspend an already mounted page. Fetch your options in onMounted instead.