CWA 2 is a prerelease (2.0.0-alpha), heading for a beta. It runs production sites; configuration can still change between releases, always with upgrade notes. View versions and support
Admin Manager

useCwaResourceModel

Two-way reactive binding between an admin form input and a single API resource field — every change PATCHes the API automatically.

useCwaResourceModel binds an admin form input to one field of the selected resource. Every change is saved to the API with a debounced PATCH. Use it in admin tab components (app/app/cwa/components/*/admin/*.vue), not in display components.

import { useCwaResourceManagerTab, useCwaResourceModel } from '#imports'

const { exposeMeta, iri } = useCwaResourceManagerTab({ name: 'Content' })
const titleModel = useCwaResourceModel<string>(iri, 'title')

defineExpose(exposeMeta)
<template>
  <CwaUiFormLabelWrapper label="Title">
    <CwaUiFormInput :model="titleModel" />
  </CwaUiFormLabelWrapper>
</template>

Pass the whole return as :model, so the control can show which other admin has the field in focus and a co-editing conflict; see Field Presence and Co-Editing Conflicts.

Signature

useCwaResourceModel<T>(
  iri: Ref<string | undefined>,  // from useCwaResourceManagerTab
  property: string | string[],   // the PHP entity property name (or dot-notation path)
  options?: {
    debounceTime?: number        // default: 250ms
    longWaitThreshold?: number   // ms before isBusy is considered a "long wait"; default: 5000ms
  }
)

Parameters

ParameterTypeDescription
iriRef<string | undefined>The reactive IRI ref returned by useCwaResourceManagerTab. It decides which resource is edited
propertystring | string[]The PHP entity property name (case-sensitive). Dot notation ('address.city') or an array of path segments (['address', 'city']) reaches a nested value; the whole root property is then sent as a merged PATCH
options.debounceTimenumberMilliseconds to wait after a change before saving. Default 250
options.longWaitThresholdnumberMilliseconds of isBusy before isLongWait turns true. Default 5000
<T>typeOptional TypeScript generic for the field type: <string>, <number>, <string | null>

Return Values

const { model, states, resetValue, remoteChange, focusedBy, focusListeners } = useCwaResourceModel<string>(iri, 'title')

model.value   // the current field value (string | null | undefined)
ReturnTypePurpose
modelWritableComputedRef<T | null | undefined>The field value — setting it triggers the debounced PATCH
states.pendingSubmitRef<boolean>A change is queued but the debounce has not fired yet
states.submittingRef<boolean>The PATCH is in flight
states.isBusyComputedRef<boolean>pendingSubmit || submitting — bind to a saving indicator
states.isLongWaitRef<boolean>true once isBusy has held for longWaitThreshold ms (default 5000)
resetValue(iri?: string) => voidDrop the local pending value and fall back to the store value
remoteChangeComputedRef<RemoteFieldChange<T> | undefined>Set while another admin's save conflicts with your unsaved change to this field. See Co-Editing Conflicts
focusedByComputedRef<FocusedPeer[]>The other admins who have this field in focus. See Field Presence
focusListenersobjectEvent listeners to v-bind on your own editor's root, so other admins see when you're in the field
focus / blur() => voidTell other admins you're in the field, or that you've left it, when the listeners don't fit

Pass the whole return to a kit control as :model:

<CwaUiFormInput :model="titleModel" />

<CwaUiFormInput>, <CwaUiFormToggle> and <CwaUiSelect> take :model. For any other control, bind model.value with v-model, and add field presence and remoteChange yourself, as in Your Own Field Editor. Setting model.value triggers a debounced PATCH. Multiple rapid changes are batched into one request.

Use states to render save feedback — for example a subtle spinner while isBusy, escalating to a "still saving…" message once isLongWait flips:

<CwaUiFormInput :model="titleModel" />
<span v-if="titleModel.states.isLongWait.value">Still saving…</span>

Multiple Fields

Call useCwaResourceModel once per field:

const { exposeMeta, iri } = useCwaResourceManagerTab({ name: 'Link' })
const labelModel = useCwaResourceModel<string>(iri, 'label')
const routeModel = useCwaResourceModel<string>(iri, 'route', { debounceTime: 0 })
const rawPathModel = useCwaResourceModel<string | null>(iri, 'rawPath')

Relation Fields

Set a relation by assigning an IRI string:

const categoryModel = useCwaResourceModel<string | null>(iri, 'category')
// Assign:
categoryModel.model.value = '/component/categories/uuid'
// Clear:
categoryModel.model.value = null

Setting debounceTime: 0

For fields where immediate save is preferred (e.g. a route field where you need instant validation feedback), pass debounceTime: 0.

Field Presence

While an admin has a field in focus, the other admins see it outlined in that admin's colour, with their name in a flag above it. The flag fades after 3 seconds and shows again on hover. focusedBy lists them, one entry per admin:

PropertyTypeDescription
user{ id: string, name?: string }The admin's user IRI and name
namestringThe name to show: their name, Someone without one, or You (other window)
colourstringTheir presence colour
isSelfbooleantrue for your own other windows
clientIdsstring[]Their browser tabs with this field in focus

A field counts as one field by its top-level property: 'address.city' and 'address.street' are both address. A draft and its live version count as one resource. An idle admin (tab hidden, or no input for 5 minutes) isn't listed.

<CwaUiFormInput>, <CwaUiFormToggle> and <CwaUiSelect> given :model show the outline and report your own focus. A disabled <CwaUiFormInput> or <CwaUiFormToggle> doesn't report focus.

IMAGE: An admin tab's Title input outlined in teal, with a small teal flag above it reading "Ana Ruiz"

Your Own Field Editor

A control that isn't a kit control, such as a rich-text editor, needs three things to co-edit like the kit fields:

  1. Show who's in the field. Wrap it in <CwaUiFieldPresence :model> and bind the model's focusListeners on the wrapper. Focus is also taken on a pointer press, for buttons and switches that Safari doesn't focus on click, and kept while you use the editor's own menus.
  2. Show a conflict. Put <CwaUiRemoteChangeConflict v-if="remoteChange" :change="remoteChange" /> beside it. It shows the same Use theirs / Keep mine line as the kit fields.
  3. Apply another admin's save without moving your caret. When model.value changes under you, replace only the part that differs, and leave the change out of undo history. The template's tiptap/co-editing.ts does this for TipTap with applyRemoteContent.
<template>
  <CwaUiFieldPresence :model="model" v-bind="model.focusListeners">    <MyEditor v-model="model.model.value" />
    <CwaUiRemoteChangeConflict v-if="model.remoteChange.value" :change="model.remoteChange.value" />  </CwaUiFieldPresence></template>

<script setup lang="ts">
import type { useCwaResourceModel } from '#imports'

defineProps<{
  model: ReturnType<typeof useCwaResourceModel<string>>
}>()
</script>

To show other admins' carets as well, send them with useCwaRealtime. Rich-Text Editor › Editing Together covers a complete editor.

Co-Editing Conflicts

When another admin saves this field while you have a change to it that isn't saved yet, the module keeps your value on screen and holds your save. remoteChange is then set, until you choose:

PropertyTypeDescription
valueTTheir value
namestringWho saved it: their name, Someone when the module hasn't heard their name, or You (other window)
user{ id: string, name?: string, isSelf: boolean } | undefinedThe user who saved it
accept() => voidUse theirs: drops your change
keep() => Promise<void>Keep mine: saves your value over theirs

The same choice appears when a save of yours crosses theirs on the way to the API. Each admin save sends the version of the resource it was based on (If-Match), and the API refuses a save based on an older version with a 412 (see API › Versions & Conflicts › Saving with If-Match). The module then shows their value with Use theirs / Keep mine. If their save created a draft, the choice moves to that draft. Saves to one resource from one tab go one at a time, so a tab never trips over its own saves.

A kit control given :model shows Name changed this · Use theirs / Keep mine beside itself. With any other control, show the choice yourself, with <CwaUiRemoteChangeConflict> or your own markup:

<template>
  <MyColourPicker v-model="colourModel.model.value" />
  <p v-if="colourModel.remoteChange.value">    {{ colourModel.remoteChange.value.name }} changed this to {{ colourModel.remoteChange.value.value }}.     <button @click="colourModel.remoteChange.value.accept()">Use theirs</button>    <button @click="colourModel.remoteChange.value.keep()">Keep mine</button>  </p></template>
A held save waits for a choice. If nothing shows remoteChange, the admin's change isn't saved until they publish (publishing chooses Keep mine) or a newer save from someone else no longer conflicts. Bind kit controls with :model, and show remoteChange beside any other control.
Without co-editing, a refused save is an error. With cwa.realtime.enabled: false, or for a save that no model-bound field holds, a 412 shows the error Someone else saved this first, so your change was not saved. Reload to see their version.

How co-editing works for the admin is in Real-Time Updates › Co-Editing in the Admin. To react to every change to a resource, use onRemoteChange.