The CWA is in heavy development
The CWA is still in alpha and not ready for production - some code and implementations are likely to change. If you would like to try out the CWA, please enjoy what we have provided and feel free to provide feedback, or get involved on GitHub.
DraftBuilding Your Ui

Creating Components

The complete guide to building a CWA component — display template, admin manager tabs, and UI class name variants.

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 loading
  • null — not found
  • object — 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

OptionTypeDescription
namestringTab label shown in the admin panel
ordernumberLower numbers appear first
disabledboolean | 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.