Component Files
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:
- Take the
iriprop, typed withIriProp. - Pass its props to
useCwaComponent(props), which returns the resource. - Call
defineExpose(exposeMeta), so the admin can select it. - Render only once
resource.datahas 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 (checkresource.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
| Option | Type | Description |
|---|---|---|
name | string | Tab label shown in the admin panel |
order | number | Lower numbers appear first |
disabled | boolean | 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
}
}
}
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.
Page Templates
How to create CWA page template components that place a page's component groups using CwaComponentGroup and CwaPage.
CLI Generator
Scaffold a new CWA display component from the command line — generates the Vue file with the correct useCwaComponent boilerplate and prints the matching API command.