<CwaComponentGroup />
<CwaComponentGroup> is the primary building block of every CWA page. Place it inside any layout, page template, or component to create a named content region that admins can populate with components in the CMS.
<CwaComponentGroup reference="hero" :location="iri" />
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
reference | string | Yes | — | Name of the component group within this resource. Determines which ComponentGroup entity this region maps to. |
location | string | Yes | — | IRI of the parent resource that owns this group (your layout, page, or component IRI). |
allowed-components | string[] | null | No | null | Collection endpoints of the component types admins may add to this group (e.g. ['/component/hero_banners', '/component/text_blocks']). All CWA component endpoints are prefixed with /component/. When set, the value is synced to the ComponentGroup entity in the API whenever an admin is signed in. null allows all types. |
Basic Usage
Every layout, page template, and component that has editable content regions uses <CwaComponentGroup>. Pass the component's own iri prop as location:
<!-- app/cwa/pages/HomePage.vue -->
<template>
<div>
<CwaComponentGroup reference="hero" :location="iri" />
<CwaComponentGroup reference="features" :location="iri" />
<CwaComponentGroup reference="cta" :location="iri" />
</div>
</template>
<script setup lang="ts">
import type { IriProp } from '@cwa/nuxt/runtime/composables'
defineProps<IriProp>()
</script>
The reference name is how the admin CMS identifies the region. It can be any string — keep it short and descriptive. Multiple groups on the same resource each need a unique reference.
In a Layout
<!-- app/cwa/layouts/PrimaryLayout.vue -->
<template>
<div class="min-h-screen flex flex-col">
<header>
<CwaComponentGroup reference="navigation" :location="iri" />
</header>
<main class="flex-1">
<!-- CWA renders the current page template here -->
<slot />
</main>
<footer>
<CwaComponentGroup reference="footer" :location="iri" />
</footer>
</div>
</template>
<script setup lang="ts">
import type { IriProp } from '@cwa/nuxt/runtime/composables'
defineProps<IriProp>()
</script>
In a Component
Components can themselves contain component groups, enabling nested composition:
<!-- app/cwa/components/TwoColumn/TwoColumn.vue -->
<template>
<div class="grid grid-cols-2 gap-8">
<div>
<CwaComponentGroup reference="left" :location="iri" />
</div>
<div>
<CwaComponentGroup reference="right" :location="iri" />
</div>
</div>
</template>
<script setup lang="ts">
import type { IriProp } from '@cwa/nuxt/runtime/composables'
defineProps<IriProp>()
</script>
How It Works
When <CwaComponentGroup> mounts it:
- Looks up the
ComponentGroupentity for{reference}_{location IRI}in the resource store - Reads the ordered list of
ComponentPositionentities from that group - Renders each
ComponentPosition's component using theuiComponentfield as the Vue component name - In admin edit mode, wraps each component with selection handles and the add-component button
The group reference is stable — it identifies the same region across environments as long as the location IRI is the same.
Events
<CwaComponentGroup> emits two events so you can react when its content is ready or changes — useful for fade-ins, analytics, or gating a "content ready" state.
| Event | Fires | Payload |
|---|---|---|
@components-loaded | Once, when every one of the group's own positions has resolved to a persisted component in a terminal API state | { component, position }[] |
@components-updated | On later persisted changes to the loaded set — an add, publish, or remove (debounced) | { component, position }[] |
Each payload entry pairs a component IRI with the ComponentPosition IRI that placed it:
<template>
<CwaComponentGroup
reference="gallery"
:location="iri"
@components-loaded="onLoaded"
@components-updated="onLoaded"
/>
</template>
<script setup lang="ts">
import type { IriProp } from '@cwa/nuxt/runtime/composables'
defineProps<IriProp>()
function onLoaded(pairs: { component: string, position: string }[]) {
// e.g. reveal the section, fire analytics, mark "content ready"
console.log(`${pairs.length} components ready`, pairs)
}
</script>
- Temporary/unpersisted components (a draft being added in the admin) are excluded — they never fire either event or appear in a payload.
- A component that errored or is absent still counts as terminal (so
components-loadednever hangs waiting on it), but it is omitted from the payload. - An empty but loaded group fires
components-loadedonce with an empty array. - Scope is this group's own positions only — a nested
<CwaComponentGroup>emits its own events independently.
Allowed Components
The API can restrict which component types are allowed in a specific group. The restriction is enforced in two places:
- Write-side (admin UI) — components not in the allowed list are hidden from the "Add Component" dialog for that group.
- Read-side (rendering) —
ComponentPositionentries whose resolved component type is not inallowedComponentsare omitted from the API response entirely. This means a component that was positioned beforeallowedComponentswas set (or whose type was later removed from the list) will silently not render.
Via the Vue prop — pass collection endpoint paths directly in the template. The module syncs this to the API entity whenever an admin is signed in, creating or updating the group as needed:
<CwaComponentGroup
reference="hero"
:location="iri"
:allowed-components="['/component/hero_banners', '/component/video_blocks']"
/>
Via fixtures — pass an array of PHP FQCNs as the second argument to ->group():
$cwa->layout('primary', 'PrimaryLayout')
->group('hero', [App\Entity\HeroBanner::class, App\Entity\VideoBlock::class]);
Via REST API — send PHP FQCNs in allowedComponents when creating or updating a ComponentGroup. The API normalizes them to collection IRIs automatically:
{
"allowedComponents": [
"App\\Entity\\HeroBanner",
"App\\Entity\\VideoBlock"
]
}
The normalizer converts these FQCNs to their /component/ collection endpoints (e.g. /component/hero_banners) before persisting.
The allowedComponents field is also returned when reading a Layout or Page with embedded component groups.
Opt-in component types (explicitAllowOnly)
allowedComponents is a per-group allow list — leaving it null allows every component type. Sometimes you want the inverse for a specific type: a component that should never appear in a group unless that group explicitly opts in. Mark the component entity with #[Silverback\ExplicitAllowOnly]:
use Silverback\ApiComponentsBundle\Annotation as Silverback;
use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent;
#[Silverback\ExplicitAllowOnly] #[ORM\Entity]
#[ApiResource]
class SectionDivider extends AbstractComponent
{
// ...
}
Once flagged, the type is opt-in everywhere:
- It is hidden from the "Add Component" dialog for any group that does not list it in
allowedComponents. - It is rejected on save by the API — a
ComponentPositionpointing at it in a group that doesn't allow it fails validation. This is enforced for both directly placed components and dynamic page-data positions, so the dynamic path can't bypass the rule.
To use the component, add its collection IRI to the group's allowedComponents (via any of the three methods above). A group whose allowedComponents is null still allows all non-flagged types, but flagged types must always be listed by name.
explicitAllowOnly: true boolean on the component's Hydra supportedClass entry in the API docs, which is how the admin UI knows to filter it out of the add dialog.For the full attribute reference and server-side enforcement details, see #[Silverback\ExplicitAllowOnly].