CWA 2 is a prerelease (2.0.0-alpha), heading for a beta. It runs production sites; configuration can still change between releases, always with upgrade notes. View versions and support
Building Your Ui

Component Files

The reference for building a CWA component — display template, admin manager tabs, and UI class name variants.
New to components? Start with the Your First Component guide. This page is the reference.

A CWA component has two parts: a display component the visitor sees, and optionally one or more admin manager tabs that appear when an admin selects the component in edit mode. The file structure drives auto-discovery, so no registration is needed. A nuxt.config entry is optional; see Part 3 below.

File Convention

app/app/cwa/components/
    Title/
        Title.vue          # display component → CwaComponentTitle
        admin/
            Title.vue      # manager tab (optional)

The directory name matches the PHP class name. The display file must have the same name as the directory. Admin tabs live in admin/ — each file is one tab, and alternative templates live in ui/. See How uiComponent Names Resolve for the names these register under.

Part 1: The Display Component

The Mandatory Pattern

Every display component must do four things:

  1. Take the iri prop, typed with IriProp.
  2. Pass its props to useCwaComponent(props), which returns the resource.
  3. Call defineExpose(exposeMeta), so the admin can select it.
  4. Render only once resource.data has loaded.
<!-- app/app/cwa/components/Title/Title.vue -->
<template>
    <div v-if="resource?.data">
        <h2>{{ resource.data.title }}</h2>
    </div>
</template>

<script setup lang="ts">
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaComponent } from '#imports'

const props = defineProps<IriProp>()

const { resource, exposeMeta } = useCwaComponent(props)
defineExpose(exposeMeta)  // required — allows the admin panel to select this component
</script>

If you forget defineExpose(exposeMeta), the admin overlay cannot attach to this component.

Accessing Your Data

// resource is returned directly from useCwaComponent(props)
// Your PHP entity fields are under resource.value?.data
const title = computed(() => resource.value?.data?.title)
const publishedAt = computed(() => resource.value?.data?.publishedAt)

resource.value states:

  • undefined — not requested yet
  • an object with data === undefined — in flight, or errored (check resource.value.apiState.status)
  • an object with data — loaded

Always guard with v-if="resource?.data" before rendering.

Opting Out of Admin Management

For presentational components that have no editable fields:

const { resource, exposeMeta } = useCwaComponent(props, [], {
    manager: { disabled: true }
})

The component still renders but the admin overlay won't attach to it.

Style Variants

Declare the styles an admin can choose in the component itself, as a map of style name → class string:

const { resource, exposeMeta, getCurrentStyleName } = useCwaComponent(props, [], {
    styles: {
        classes: {
            Light: 'bg-white text-black',
            Dark: 'bg-black text-white'
        }
    }
})

The selected classes are applied to the component's root element automatically — no :class binding needed. Pass autoClass: false to bind uiClassNames yourself instead.

For logic keyed off which variant is selected, use getCurrentStyleName — the resource stores the class string of each selected style in uiClassNames, never the style name:

const variant = computed(() => resource.value?.data && getCurrentStyleName(resource.value.data))

// <div :class="{ 'ring-2': variant === 'Dark' }">

See useCwaComponent › Style variants for multiple selection and the storage shape.

Part 2: Admin Manager Tabs

One Tab

<!-- app/app/cwa/components/Title/admin/Title.vue -->
<template>
    <div class="p-4 space-y-4">
        <CwaUiFormLabelWrapper label="Title text">
            <CwaUiFormInput :model="titleModel" />
        </CwaUiFormLabelWrapper>
    </div>
</template>

<script setup lang="ts">
import { useCwaResourceManagerTab, useCwaResourceModel } from '#imports'

const { exposeMeta, iri } = useCwaResourceManagerTab({ name: 'Content', order: 1 }) 
const titleModel = useCwaResourceModel<string>(iri, 'title') 
defineExpose(exposeMeta) </script>
defineExpose(exposeMeta) is required — it's how the resource manager discovers the tab's name and order. Without it the tab renders with no label and no ordering.

The composable returns the currently-selected iri, so a tab doesn't need an iri prop of its own. Text inputs and selects take no label prop — wrap them in <CwaUiFormLabelWrapper>. <CwaUiFormToggle> takes its own label.

useCwaResourceManagerTab Options

OptionTypeDescription
namestringTab label shown in the admin panel
ordernumberLower numbers appear first
disabledboolean | Ref<boolean>Hide the tab. A plain boolean is read once at setup; pass a ref or computed to toggle the tab on and off

useCwaResourceModel

useCwaResourceModel(iri, property) binds a single property of the resource. Its model is a writable ref: writing to it debounces and PATCHes the API:

const titleModel = useCwaResourceModel<string>(iri, 'title')

// Pass the whole return to a kit control, which can then show a co-editing conflict
// <CwaUiFormInput :model="titleModel" />
// Setting titleModel.model.value = 'New value' triggers a PATCH

Call it once per editable field. Dot and array paths address nested properties, and it also returns states (pendingSubmit, submitting, isBusy, isLongWait), resetValue() and remoteChange — see useCwaResourceModel.

Multiple Tabs

Create one file per tab:

app/app/cwa/components/Article/admin/
    Content.vue     # useCwaResourceManagerTab({ name: 'Content', order: 1 })
    Image.vue       # useCwaResourceManagerTab({ name: 'Image', order: 2 })
    Settings.vue    # useCwaResourceManagerTab({ name: 'Settings', order: 3 })

Each file is independent — they can use different composables and form inputs. A common pattern: Content.vue uses useCwaResourceModel, Image.vue uses useCwaResourceUpload.

Read-Only Admin Tab

Not every tab needs to write. Use a tab to show computed values, metadata, or instructions:

<template>
    <div class="p-4 space-y-2 text-sm text-gray-500">
        <p>IRI: <code>{{ iri }}</code></p>
        <p>Created: {{ resource?.data?.createdAt }}</p>
        <p>Published: {{ resource?.data?.publishedAt ?? 'Not published' }}</p>
    </div>
</template>

<script setup lang="ts">
import { useCwaResourceManagerTab } from '#imports'

const { exposeMeta, iri, resource } = useCwaResourceManagerTab({ name: 'Info', order: 99 })

defineExpose(exposeMeta)
</script>

Part 3: nuxt.config Registration (Optional)

Every component in app/app/cwa/components/ is discovered automatically and gets a default name derived from its directory (HeroBlock → "Hero Block"), so it already appears in the "Add Component" dialog without any config. Registering it lets you override that name and add a description, instantAdd and defaultData:

// nuxt.config.ts
cwa: {
    resources: {
        Title: {
            name: 'Title Block',
            description: 'A heading or section title',
            instantAdd: false,          // true = skip config dialog, add immediately
            defaultData: {              // pre-fill fields when created
                title: 'New Title'
            }
        },
        Article: {
            name: 'Article',
            instantAdd: false
        }
    }
}
There is no classes key here — style variants are declared in the component itself, via useCwaComponent's styles.classes option (see Style Variants above).

instantAdd

When true, the admin's add dialog shows Add Now instead of Insert, and the component is inserted straight away without opening it for editing first. Best for simple, self-contained components (e.g. a divider or spacer) where there's nothing to configure up front.

defaultData

defaultData is an object of field values the new component starts with. When an admin adds the component from the add dialog, these values are copied into the new resource before it is saved, so with instantAdd: true they are what gets saved. Keys are the PHP entity's property names. A component added from a page data position's tab starts without them.