Skip to content

Views, Layout Builder, theming and widgets

Views

Add the compound's sub-fields to a view of its host content. Each uses its native field type's filters, sorts and arguments, and references provide relationships to their targets. Choose the sub-field itself for formatter output, or a stored column for escaped raw output. Nested compound leaves appear too.

For repeated compounds, choose whether to show each value or group by host. A sub-field's formatted and raw values, REST export raw output included, show only when the user may view the host entity, its compound field and that sub-field. A stored column follows the same rule: it is empty on rows where its sub-field is hidden, and leaves the view when the compound field is. On a display with aggregation turned on, a stored column, whether grouped or aggregated (Count, Sum, Minimum, Maximum and the other aggregation types), shows only when the user may view the compound field and every sub-field on the column's path, judged from the field definitions alone. Otherwise the column leaves the view before its query runs. A result row there folds many host rows into one, so a denial that depends on the host entity or on the stored value cannot apply to it. Don't use such a column in an aggregated display if a rule like that hides it from some users.

Layout Builder

Place the whole compound as an ordinary field block. To place individual sub-fields, enable compound_field_layout_builder and follow the layout tutorial. Sub-field blocks use the compound's shared Manage display formatters, not host-field overrides, and the compound's own Manage display has no layout editor.

Canvas

Every compound row has a computed cfi_item property: an entity reference to the logical item it is (see compound items as entities). Canvas follows entity references when it matches component props to fields, so it reaches each sub-field through that property, with the sub-field's own computed properties such as processed text and image URLs. A content template stores the link as an ordinary expression, for example ℹ︎␜entity:node:conference␝field_speaker␞␟cfi_item␜␜entity:cfi_speaker␝name␞␟value. The property stores nothing and stays out of serialized values. Follow the Canvas tutorial to link one.

Theming and callbacks

A sub-field formatter receives a real content entity: the compound item, of type cfi_<compound id> (see compound items as entities).

For status in compound speaker_card, the most specific field template is field--cfi-speaker-card--status--cfi-speaker-card.html.twig. Less specific names omit the sub-field or bundle segment, in core's usual suggestion order. Host templates such as field--node--status--article.html.twig do not apply.

The render array's #object is the compound item. It exposes sibling sub-fields, including their references, through its own field lists, but has no host entity access of its own; call $item->getHost() for the entity that owns the compound field. Treat the item as read-only in Twig: writing through it is an ordinary content-entity save, not a display-time operation.

An allowed_values_function receives the same compound item as its entity argument. A callback written for the host node cannot read host fields from it directly; call $item->getRootHost(), or make the callback independent of the host.

For field-type authors, FieldProxy::getEntity() answers which entity declares the sub-field: the compound item. FieldProxy::getHostCoordinates() returns the outermost stored entity type, entity ID and field name, for durable references that must survive outside the typed-data tree.

Widgets inside a compound

A sub-field widget runs unchanged inside a compound and sees the item through two views.

Its form(), extractFormValues() and massageFormValues() receive the item element in the host's coordinates: #parents of [<host field>, <delta>], the #field_parents core derives from them, and a form state whose values and raw input are the host's. Every name core derives from those coordinates is unique per item, and a key a widget derives from the form it is handed is the same at build and at extraction, as core's EntityFormDisplay guarantees for a plain field.

Its callbacks (#process, #after_build, #value_callback, #element_validate, #validate, #submit, #ajax and #pre_render) receive an item-local complete form whose #parents is empty and whose #build_id is the item's own: distinct per item, stable within one build, derived from the host form's build ID and the item's UUID, and for a nested item from its enclosing item's. The form state's values, input, triggering element and validation limits are the item's slice in local coordinates, and its form object's entity is the item. Widget state written with host parents at build is found with local parents in callbacks.

Form-state storage is split by key. What core keeps about the item's own form, its widgets' state (field_storage), its form display and its language, is the item's, so it follows the item's identity through reorders rather than its position. Every other key a widget stores is the host form's: key it uniquely per item, with $items->getEntity()->uuid() or the host-coordinate #field_parents. A module's form-level alter of the host form, which fires once for the host, finds it there and can decide whether to attach its own host-level handlers. Temporary values are the item's.

A button #name that repeats across items, whether a constant or one computed from local #parents in a #process callback as core's managed_file does, is rendered on the host form with a per-item suffix (<name>--<host field>-<delta>, nested paths joined the same way), so that Form API attributes a click to the item it was made in. The first item to register a name keeps it unchanged, so a unique name and a single-item form render exactly as they would without the boundary, and the name appears unsuffixed inside the item's own callbacks, including on the triggering element.

Two names are not covered by that suffix. The files[...] name of a managed_file upload input is not a button: core builds it from local #parents in #process, so two items whose upload slot is empty render the same upload input name. An AJAX upload still lands on the right item, because the upload button that submits it is suffixed, but a non-JavaScript submission carrying uploads for two items would not. And a DOM id a widget chooses itself, such as the wrapper of an #ajax setting, repeats once per item when it is keyed on the field name alone, so an AJAX response replaces the first item's wrapper; derive such an id from the item's UUID or the host-coordinate #field_parents.

Structural-change subscribers

Renames and removals dispatch CompoundFieldSubFieldStructureEvent::NAME (compound_field.sub_field_structure_change) once per applied plan, while its lock is held, before the pipeline rewrites the host configuration it manages. The read-only payload is the event's public readonly properties: compoundId, subFieldMoves, droppedSubFields, and the corresponding flattened columns, capped slots included, in flattenedColumnMoves and flattenedColumnDrops. Use these column mappings; the pipeline does not rewrite arbitrary consumers, so subscribers update their own Views, contrib or site configuration.

For Token and Search API, see nested values, tokens and Search API.