Creating Components
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 the auto-discovery — no registration beyond nuxt.config needed.
File Convention
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.
Part 1: The Display Component
The Mandatory Pattern
Every display component must do four things:
<!-- 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 useCwaResource for multiple selection and the storage shape.
Part 2: Admin Manager Tabs
One Tab
<!-- app/cwa/components/Title/admin/Title.vue -->
<template>
<div class="p-4 space-y-4">
<CwaUiFormLabelWrapper label="Title text">
<CwaUiFormInput v-model="titleModel" />
</CwaUiFormLabelWrapper>
</div>
</template>
<script setup lang="ts">
import { useCwaResourceManagerTab, useCwaResourceModel } from '#imports'
const { exposeMeta, iri } = useCwaResourceManagerTab({ name: 'Content', order: 1 })
const { model: 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. Form controls take no label prop — wrap them in <CwaUiFormLabelWrapper>.
useCwaResourceManagerTab Options
| Option | Type | Description |
|---|---|---|
name | string | Tab label shown in the admin panel |
order | number | Lower numbers appear first |
disabled | boolean | Disable the tab. Read once at setup — it is not reactive, so hide controls inside the tab rather than trying to toggle it |
useCwaResourceModel
useCwaResourceModel(iri, property) returns a writable ref bound to a single property of the resource. Writing to it debounces and PATCHes the API:
const { model: titleModel } = useCwaResourceModel<string>(iri, 'title')
// Bind directly to a form input
// <CwaUiFormInput v-model="titleModel" />
// Setting titleModel.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) and resetValue() — see useCwaResourceModel.
Multiple Tabs
Create one file per tab:
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
Every component in 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, clicking "Add Component" in the admin immediately inserts the component without opening a configuration dialog. Best for simple, self-contained components (e.g. a divider or spacer) where there's nothing to configure up front.
Page Templates
How to create CWA page template components that define the content regions of a page 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.