Controlling Root Components in Hyvä CMS
Root components are the components that sit at the top level of a Hyvä CMS content tree, rather than inside another component. Hyvä CMS gives you two sets of controls over them, and they live in different places:
- Component properties go in a component's
etc/hyva_cms/components.jsondeclaration:root_only,protected_rootandcontext_flags. They describe the component. - Content type settings go in the
init-content-propertiesevent your content type sends when the Liveview Editor opens:allowed_root_component_context_flags,strict_component_context,allowed_root_components,disallowed_componentsandmax_root_components. They describe the content being edited.
Most of the confusion comes from mixing the two up, so this page walks through each side, then shows how a real content type combines them.
Which Hyvä CMS Root Component Control to Use
Pick the control by the question you're answering. Each row links to its section below.
| You want to... | Use | Where | Since |
|---|---|---|---|
| Stop a component from being nested inside other components | root_only |
components.json |
1.3.0 |
| Make one component the fixed outer wrapper of every document | protected_root |
components.json |
1.3.0 |
| Tie a component to the content types that use it | context_flags |
components.json |
1.1.0 |
| Accept only components with certain flags at the root | allowed_root_component_context_flags |
event | 1.1.0 |
| Keep unflagged components out of nested pickers too | strict_component_context |
event | 1.3.0 |
| Accept only named components at the root | allowed_root_components |
event | 1.3.0 |
| Ban named components everywhere in the editor | disallowed_components |
event | 1.3.0 |
| Limit how many components sit at the root | max_root_components |
event | 1.3.0 |
| Hide your flagged components from other content types | disallowedRootComponentContextFlags |
Alpine store | 1.2.0 |
All of these are editor restrictions. They decide what the Liveview Editor offers and accepts. Your own save logic and any API that writes content still has to enforce its own rules.
Component Properties for Root Components
The three root-related component properties are declared next to label, template and children in components.json. The Component Declaration Schema Reference validates all three.
root_only: Keep a Component at the Top Level
Set "root_only": true on a component that only makes sense at the top of the content tree, such as a page section wrapper or a form root. A root_only component:
- does not appear in the "Add a Component" picker of a nested component
- does not appear in the header quick-add while a nested component is selected
- can't be dragged into another component's children
root_only controls where a component can go. It doesn't decide which content types offer the component; that's what context_flags is for.
protected_root: Make a Component the Fixed Outer Wrapper
Set "protected_root": true on a component that must wrap the whole document, such as an email body or a form. When that component sits at the top level, the Liveview Editor draws it as the header of the components panel instead of a tree row, and its children become the top level of the tree. Editors change its settings through the pencil in that header.
A protected root can't be dragged, selected, copied, duplicated, hidden or deleted. New components, including ones added from the "Add a Component" picker, go into the protected root's children. Pasting at the top level pastes into the protected root too. Clearing the canvas rebuilds the protected root from its declaration defaults.
A protected root also caps the tree at one root component, with no max_root_components needed. Pair it with root_only: true so the component can never end up nested. For the full header behavior and icon requirements, see Giving a component the protected root header.
context_flags: Tie Components to Content Types
context_flags is an array of strings that says which content types a component belongs to. Hyvä's own modules use email (Email & Newsletter Templates), hyva_menu_root (Menu Builder) and hyva_form_root (Form Builder).
The rule to remember: a component with context flags only appears at the root of a content type that allows one of those flags. CMS pages and CMS blocks allow no flags, so a flagged component never shows up in their root picker. That's what keeps email, menu and form roots off your CMS pages.
So only flag components that belong to a specific content type. A component that should be available on CMS pages and blocks must have no context_flags at all.
Content Type Settings for Root Components
Your content type sends its root settings on the init-content-properties window event, from the settings template that initializes the Liveview Editor. Every setting is optional. A setting you don't send keeps the editor's default, which is described in each section below.
This minimal event detail accepts only your own flagged components at the root and limits the root to one component:
window.dispatchEvent(new CustomEvent('init-content-properties', {
detail: {
// ...your label, entity_title and other content properties
allowed_root_component_context_flags: ['vendor_banner_root'],
max_root_components: 1
}
}));
For a complete, shipped example, see module-form-builder/src/view/adminhtml/templates/core-settings/form.phtml, covered in Example: How Form Builder Combines the Controls.
allowed_root_component_context_flags: Accept Flagged Components at the Root
allowed_root_component_context_flags lists the context flags your content type accepts at the root. It changes the root "Add a Component" picker and the header quick-add as follows:
| What your content type sends | Root picker shows |
|---|---|
Nothing, null or [] |
Only components with no context_flags |
['vendor_banner_root'] |
Only components carrying vendor_banner_root |
There's no value that means "show everything". Not sending the key, sending null and sending an empty array all behave the same, and that's how CMS pages and blocks run.
Nested pickers are more relaxed. Inside a component, the picker shows unflagged components plus components carrying one of your allowed flags, unless you turn on strict_component_context. When a parent component declares children.config.accepts, that list replaces the flag check for its children entirely.
Paste, snippets and templates check flags too. With allowed flags set, they accept components carrying a matching flag and unflagged components, unless strict_component_context is on.
Changed in Hyvä CMS 1.2.0
Up to 1.2.0-beta2, sending no allowed flags (or null) showed every component at the root, flagged or not. From 1.2.0, it shows only components without context_flags. If the "Add a Component" picker comes up empty after an upgrade, check whether your general-purpose components declare context_flags. Remove the flags from components meant for CMS pages and blocks, or have your own content type send the flags it accepts.
strict_component_context: Keep Unflagged Components Out of Nested Pickers
Send strict_component_context: true when unflagged components must never appear anywhere in your content type, not just at the root. With strict mode on, nested pickers, header quick-add, paste, snippets and templates offer only components carrying one of your allowed flags. The root picker already behaves this way, so strict mode doesn't change it.
The default is false. Email & Newsletter Templates turns strict mode on so that only email-ready components can be used inside an email.
allowed_root_components: Accept Named Components at the Root
allowed_root_components is an array of component names, for example ['vendor_banner', 'vendor_hero']. Only the listed components appear in the root picker and the root header quick-add. The list narrows the flag check rather than replacing it, so a component must pass both. Nested pickers ignore allowed_root_components.
Use it when a flag is too broad, for example when your content type accepts only two of the five components carrying your flag.
disallowed_components: Ban Named Components at Every Depth
disallowed_components is an array of component names that the editor never offers in your content type: not at the root, not inside unrestricted containers, not through header quick-add, and not through paste, snippets or templates. Form Builder sends disallowed_components: ['hyva_form_widget'] so a form can't embed another form.
Unlike a parent's children.config.excludes, which only affects that parent's children, disallowed_components applies at every depth of the tree.
max_root_components: Limit the Number of Root Components
max_root_components is a positive integer that caps how many components can sit at the top level. When the root is full, the editor refuses a root-level add with "Cannot add: the maximum number of top-level components has been reached", and it refuses root-level pastes and drops the same way. The Add buttons stay visible, so editors see the error message rather than a missing button.
The count comes from the rendered tree, so it stays accurate between saves. Content types that don't send the key, or send 0, have no limit. A protected root forces the limit to one whatever you send.
Hiding Your Flagged Components from Other Content Types
allowed_root_component_context_flags already keeps flagged components out of content types that allow no flags. When another content type does allow flags, and you also need your components hidden from it, push your flag onto Alpine.store('global').disallowedRootComponentContextFlags from your module's Liveview Editor layout. Components carrying a disallowed flag are removed from the root picker, nested pickers, header quick-add, paste, snippets and templates.
Write to the store before the pickers open. Form Builder listens for init-content-properties itself and adds hyva_form_root to the array whenever the content type being edited doesn't allow it. See module-form-builder/src/view/adminhtml/templates/page/js/picker-exclusion.phtml for that implementation.
Example: How Form Builder Combines the Controls
Form Builder is the clearest example of all the root controls working together. A form must hold exactly one form root, the form root must never be nested, and forms must not leak into other content types.
On the component side, the hyva_form declaration in module-form-builder/src/etc/hyva_cms/components.json sets:
"context_flags": ["hyva_form_root"], so it only appears in content types that allow that flag"root_only": true, so it can't be nested"protected_root": true, so it becomes the fixed wrapper with its settings in the panel header"children": { "config": { "accepts": ["form_step"] } }, so the form root only takes form steps
On the content type side, the form editor's init-content-properties event in module-form-builder/src/view/adminhtml/templates/core-settings/form.phtml sends:
allowed_root_component_context_flags: ["hyva_form_root"], so only form roots appear at the rootmax_root_components: 1, so a form holds one root even before the protected root exists in new contentdisallowed_components: ["hyva_form_widget"], so a form can't embed another form
Finally, picker-exclusion.phtml adds hyva_form_root to disallowedRootComponentContextFlags in every other content type.
Troubleshooting Root Component Restrictions
The "Add a Component" Picker Is Empty or Missing Components
Problem: after upgrading to Hyvä CMS 1.2.0 or later, components you declared no longer appear at the root of CMS pages or blocks, or the root picker is completely empty.
Cause: the components declare context_flags, and CMS pages and blocks allow no flags. Since 1.2.0, a content type that allows no flags shows only components without context_flags.
Solution: remove context_flags from components meant for CMS pages and blocks. Keep flags only on components that belong to a specific content type, and send that flag in allowed_root_component_context_flags from the content type that uses them.
A Root-Only Component Still Appears Inside a Nested Component
Problem: a component with root_only: true ends up nested.
Cause: root_only is enforced by the pickers and by drag and drop. Paste and templates don't check it.
Solution: if a content type must never nest the component, make it a protected root, or add it to the children.config.excludes of the containers it could be pasted into.
Related Topics
- Extending Hyvä CMS for Other Content Types - Build the content type that sends
init-content-properties - Parent-Child Hyvä CMS Components - Restrict children with
accepts,excludesandrequire_parent - Component Declaration Schema Reference - Every
components.jsonproperty - Create a Custom Form Root - A worked example of a flagged, protected root component