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 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 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

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