useCwaResourceModel
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
| Parameter | Type | Description |
|---|---|---|
iri | Ref<string | undefined> | The reactive IRI ref returned by useCwaResourceManagerTab. It decides which resource is edited |
property | string | 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.debounceTime | number | Milliseconds to wait after a change before saving. Default 250 |
options.longWaitThreshold | number | Milliseconds of isBusy before isLongWait turns true. Default 5000 |
<T> | type | Optional 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)
| Return | Type | Purpose |
|---|---|---|
model | WritableComputedRef<T | null | undefined> | The field value — setting it triggers the debounced PATCH |
states.pendingSubmit | Ref<boolean> | A change is queued but the debounce has not fired yet |
states.submitting | Ref<boolean> | The PATCH is in flight |
states.isBusy | ComputedRef<boolean> | pendingSubmit || submitting — bind to a saving indicator |
states.isLongWait | Ref<boolean> | true once isBusy has held for longWaitThreshold ms (default 5000) |
resetValue | (iri?: string) => void | Drop the local pending value and fall back to the store value |
remoteChange | ComputedRef<RemoteFieldChange<T> | undefined> | Set while another admin's save conflicts with your unsaved change to this field. See Co-Editing Conflicts |
focusedBy | ComputedRef<FocusedPeer[]> | The other admins who have this field in focus. See Field Presence |
focusListeners | object | Event listeners to v-bind on your own editor's root, so other admins see when you're in the field |
focus / blur | () => void | Tell 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:
| Property | Type | Description |
|---|---|---|
user | { id: string, name?: string } | The admin's user IRI and name |
name | string | The name to show: their name, Someone without one, or You (other window) |
colour | string | Their presence colour |
isSelf | boolean | true for your own other windows |
clientIds | string[] | 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.
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:
- Show who's in the field. Wrap it in
<CwaUiFieldPresence :model>and bind the model'sfocusListenerson 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. - 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. - Apply another admin's save without moving your caret. When
model.valuechanges under you, replace only the part that differs, and leave the change out of undo history. The template'stiptap/co-editing.tsdoes this for TipTap withapplyRemoteContent.
<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:
| Property | Type | Description |
|---|---|---|
value | T | Their value |
name | string | Who saved it: their name, Someone when the module hasn't heard their name, or You (other window) |
user | { id: string, name?: string, isSelf: boolean } | undefined | The user who saved it |
accept | () => void | Use 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>
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.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.