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 { toRef } from 'vue'
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaResource } from '#imports'
const props = defineProps<IriProp>()
const { getResource, exposeMeta } = useCwaResource(toRef(props, 'iri'))
const resource = getResource()
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
const resource = getResource()
// 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— still loadingnull— not foundobject— loaded successfully
Always guard with v-if="resource?.data" or v-if="resource" before rendering.
Opting Out of Admin Management
For presentational components that have no editable fields:
const { getResource, exposeMeta } = useCwaResource(toRef(props, 'iri'), {
disableManager: true
})
The component still renders but the admin overlay won't attach to it.
UI Class Name Variants
The uiClassNames field holds the CSS class(es) the admin chose from the options you define in nuxt.config:
<template>
<div :class="resource?.data?.uiClassNames ?? ''">
<h2>{{ resource?.data?.title }}</h2>
</div>
</template>
For conditional logic based on which variant is selected:
const variant = computed(() => resource.value?.data?.uiClassNames)
// Check the variant name in your template
// <div :class="{ 'bg-black text-white': variant === 'dark' }">
Part 2: Admin Manager Tabs
One Tab
<!-- app/cwa/components/Title/admin/Title.vue -->
<template>
<div class="p-4 space-y-4">
<CwaUiFormInput
v-model="model.title"
label="Title text"
/>
</div>
</template>
<script setup lang="ts">
import { toRef } from 'vue'
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaResourceManagerTab, useCwaResourceModel } from '#imports'
const props = defineProps<IriProp>()
useCwaResourceManagerTab({ name: 'Content', order: 1 })
const { model } = useCwaResourceModel(toRef(props, 'iri'))
</script>
useCwaResourceManagerTab Options
| Option | Type | Description |
|---|---|---|
name | string | Tab label shown in the admin panel |
order | number | Lower numbers appear first |
disabled | boolean | ComputedRef<boolean> | Conditionally disable the tab |
useCwaResourceModel
Returns a reactive model object mirroring the component's data. Setting a field PATCHes the API automatically with debouncing:
const { model } = useCwaResourceModel(toRef(props, 'iri'))
// Bind directly to form inputs
// <CwaUiFormInput v-model="model.title" />
// Setting model.title = 'New value' triggers a PATCH
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>{{ props.iri }}</code></p>
<p>Created: {{ resource?.data?.createdAt }}</p>
<p>Published: {{ resource?.data?.publishedAt ?? 'Not published' }}</p>
</div>
</template>
<script setup lang="ts">
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaResource, useCwaResourceManagerTab } from '#imports'
const props = defineProps<IriProp>()
useCwaResourceManagerTab({ name: 'Info', order: 99 })
const { getResource } = useCwaResource(toRef(props, 'iri'))
const resource = getResource()
</script>
Part 3: nuxt.config Registration
Without registration a component still renders and is admin-selectable. Registration adds it to the "Add Component" dialog with a name and description, and enables UI class name options:
// 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'
},
classes: [
{ value: '', label: 'Default' },
{ value: 'text-center', label: 'Centered' },
{ value: 'text-right', label: 'Right-aligned' }
]
},
Article: {
name: 'Article',
instantAdd: false
}
}
}
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.