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.