-
Issue #3535447 by thoward216, wim leers, justafish, effulgentsia: XB should provide no components of its own: move all XB's SDCs into the `xb_test_sdc` module
Issue #3535447 by thoward216, wim leers, justafish, effulgentsia: XB should provide no components of its own: move all XB's SDCs into the `xb_test_sdc` module
- The Experience Builder Data Model
- Finding issues 🐛, code 🤖 & people 👯♀️
- 1. Terminology
- 1.1 Existing Drupal Terminology that is crucial for XB
- 1.2 XB terminology
- 2. Product requirements
- 3. Implementation
- 3.1 Data Model: from Front-End Developer to an XB data model that empowers the Content Creator
- 3.2 Data Model: storing a component tree
- 3.2.1 The columns (field props) storing the tree structure
- 3.2.2 The column (field prop) storing the component input values
- 3.2.3 Validation
- 3.2.4 Facilitating component inputs changes
- 3.3 Data Model: rendering a stored component tree
- 3.4 UI Data Model: communicating a component tree to the front end
- 3.4.1 component nodes
- 3.4.2 slot nodes
- 3.4.3 region nodes
- 3.4.4 The complete API response
The Experience Builder Data Model
In the rest of this document, Experience Builder
will be written as XB
.
This builds on top of the XB Components
doc. Please read that first.
Some of the examples here refer to details that component type
s that use
XB Shape Matching into Field Types
doc. It should be possible to first read this
without having read that, to understand the big picture. It is recommended to first read this, then that one, followed
by a second pass of this document.
It also builds on top of the XB Config Management
doc, which itself refers back to this one
for a few things. The data model is built on top of the configuration architecture.
Also see the diagram.
🐛 , code 🤖 & people 👯♀️
Finding issues Related XB issue queue components:
Those issue queue components also have corresponding entries in CODEOWNERS
.
If anything is unclear or missing in this document, create an issue in one of those issue queue components and assign it
to one of us!
1. Terminology
1.1 Existing Drupal Terminology that is crucial for XB
-
content entity
: an entity that can be created by a Content Creator, containing variousfield
s, potentially including theXB field type
, of a particular entity type (e.g. "node") -
data type
: Drupal's smallest unit of representing data, defines semantics and typically comes with validation logic and convenience methods for interacting with the data it represents⚠️ Not all data types in Drupal core do what they say, see\Drupal\experience_builder\Plugin\DataTypeOverride\UriOverride
for example.⚠️ -
field
: synonym offield item list
-
field prop
: a property defined by afield type
, with a value for that property on such afield item
, represented by adata type
. Often a single prop exists (typically:value
), but not always (for example: theimage
field type:target_id
,entity
,alt
,title
,width
,height
— withentity
acomputed field prop
) -
field instance
: a definition for instantiating afield type
into afield item list
containing >=1field item
-
field item
: the instantiation of afield type
-
field item list
: to support multiple-cardinality values, Drupal core has opted to wrap everyfield item
in a list — even if a particularfield instance
is single-cardinality -
field type
: metadata plus a class defining thefield prop
s that exist on this field type, requires afield instance
to be used -
SDC
: seeXB Components
doc -
theme region
: seeXB Config Management
doc -
view mode
: view modes lets acontent entity
be displayed in multiple ways
1.2 XB terminology
-
component
: seeXB Components
doc -
Component config entity
: seeXB Config Management
doc -
component instance
: a UUID uniquely identifying this instance +component version
+ values for each requiredcomponent input
(if any) + optionally values for itscomponent slot
s (if any) -
component node
: one of the node types in the UI data model, representing acomponent instance
in thecomponent tree
-
component input
: seeXB Components
doc -
component slot
: seeXB Components
doc -
Component Source Plugin
: seeXB Components
doc -
component tree
: a tree ofcomponent instance
s, by placing >=1component instance
s in a particular order in anothercomponent instance
's slot -
component tree field type
: XB's field type that allows storing acomponent tree
⚠️ This is currently limited to the "default"view mode
, and hence one component tree percontent entity
.⚠️ -
component tree root
: the root of thecomponent tree
is the special case: it does not exist in anothercomponent
, but it behaves the same as any othercomponent slot
-
component type
: seeXB Components
doc -
component version
: a version (a deterministic hash) identifying the version of aComponent config entity
either because the underlyingcomponent
itself changed, or because the defaultstatic prop source
s changed due to modified shape matching -
content type template
: seeXB Config Management
doc. -
layout
: synonym ofcomponent tree
-
prop expression
: seeXB Shape Matching into Field Types
doc -
prop source
: seeXB Shape Matching into Field Types
doc -
static prop source
: seeXB Shape Matching into Field Types
doc -
dynamic prop source
: seeXB Shape Matching into Field Types
doc -
region node
: one of the node types in the UI data model, representing atheme region
'scomponent tree
-
slot node
: one of the node types in the UI data model, representing acomponent instance
'scomponent slot
-
XB field
: an instance of thecomponent tree field type
-
XB field type
: seecomponent tree field type
2. Product requirements
This uses the terms defined above.
This adds to the product requirements listed in XB Components
doc and XB Config Management
doc.
(There are more, but these in particular affect XB's data model.)
- MUST have validation logic that generates consistent validation error messages for either content (a
component tree
created by the Content Creator and stored in acontent entity
) or config (acomponent tree
created by the Site Builder and stored in acontent type template
) - MUST support both symmetric and asymmetric translations (same vs different
layout
per translation, respectively) - SHOULD facilitate real-time collaborative editing
3. Implementation
This uses the terms defined above.
Given a component developed by a Front-End Developer: how does XB allow a
Content Creator to place a component instance
in the component tree
, specify values for the component input
s and
component slot
s?
3.1 Data Model: from Front-End Developer to an XB data model that empowers the Content Creator
Moved to the XB Shape Matching into Field Types
doc.
3.2 Data Model: storing a component tree
The component tree
is represented by a \Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItemList
, which
contains one field value for each component instance
in the tree.
Each component instance
is represented by a \Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem
, which
each allow accessing the Component config entity
and Component Source Plugin
that represents the component
.
See \Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem
+ its validation constraint.
XB defines a new XB field type
with the following field prop
s:
-
uuid — A unique ID for this
component instance
-
component_id — This is the ID of the
Component config entity
thiscomponent instance
references -
parent_uuid — If this
component instance
is placed inside anothercomponent instance
in the tree, the UUID of the parentcomponent instance
-
slot — If this
component instance
is placed inside anothercomponent instance
in the tree, the machine name of thecomponent slot
in which it is placed. This slot must exist in the parentcomponent instance
. - inputs — see 3.2.2
When parent_uuid and slot are empty, the component instance
is at the root of the component tree
.
Additionally there are two computed field prop
s:
-
component - this is an entity reference to the
Component config entity
thecomponent instance
uses, meaning also the appropriate version will be loaded. Any methods on theComponent config entity
can be chained. E.g.$item->get('component')?->getComponentSource()
. -
parent_item - this is a data reference to the sibling
\Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem
in the tree that represents thecomponent instance
's parentcomponent instance
in thecomponent tree
. If thecomponent instance
has no parent, this will be NULL. Any methods on the parentcomponent instance
can be chained, e.g.$item->get('parent_item')->getComponent()?->getComponentSource()?->getSlotDefinitions()
Additionally, convenience methods for accessing/setting values on the ComponentTreeItem
exist including:
-
getParentUuid(): ?string
- gets the value of parent_uuid if it exists -
getParentComponentTreeItem(): ?ComponentTreeItem
- gets the parentcomponent instance
if it exists -
getSlot(): ?string
- gets thecomponent slot
machine name if it exists -
getComponent(): ComponentInterface
- gets theComponent config entity
at the specified version -
getComponentId(): string
- gets the ID of theComponent config entity
-
getComponentVersion(): string
- gets the version of theComponent config entity
used for this instance -
getUuid(): string
- gets the UUID of thecomponent instance
-
getInputs(): ?array
- gets the explicit inputs of thecomponent instance
as an array (JSON decoded) -
getInput(): ?string
- gets the explicit inputs of thecomponent instance
as a string (JSON encoded) -
setInput(array|string $input): static
- sets the inputs, can be passed as either a string (JSON encoded) or an array -
getLabel(): ?string
- gets the (optional) label for thecomponent instance
to provide context for content authors -
setLabel(?string $label): self
- sets the (optional) label for thecomponent instance
to provide context for content authors
Storing these as separate field prop
s simplifies supporting both symmetric and asymmetric translations:
- the inputs column group (just the
inputs
column) group SHOULD always be translatable - the tree column group (comprising
uuid
,component_id
,component_version
,parent_uuid
andslot
) can be either:- marked translatable for asymmetric translations (a different
component tree
percontent entity
translation) - marked untranslatable for symmetric translations (same
component tree
for allcontent entity
translations)
- marked translatable for asymmetric translations (a different
(Drupal's Content Translation module natively supports configuring this.)
field prop
s) storing the tree structure
3.2.1 The columns (The uuid
, component_id
, component_version
, parent_uuid
and slot
columns model the tree structure.
See \Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure
+ its validation constraint.
These columns always meet the following requirements
- every
component instance
is represented by a "uuid, component_id, component_version" triple, with:
- the value for "component_id" being the ID of a
Component config entity
(NOT that of the underlyingcomponent
) - the value for "component_version" being a version on the (versioned!)
Component config entity
(see\Drupal\experience_builder\Entity\VersionedConfigEntityInterface::getVersions()
) - the "uuid" being a randomly generated UUID
- Any top-level items have NULL for both the
parent_uuid
andslot
. - Nested components must have a value for both the
parent_uuid
andslot
.- The
parent_uuid
must exist in a sibling field item in theComponentTreeItemList
. - The
slot
must be present in the parentcomponent
's slot definitions - The
parent_uuid
must not be the same as theuuid
- you cannot reference yourself as a parent
- The
- Each
uuid
must be unique in the list of items - The
delta
of each field item represents the order that components in the same level of the tree appear in.
field prop
) storing the component input
values
3.2.2 The column (See
\Drupal\experience_builder\Plugin\DataType\ComponentInputs
\Drupal\experience_builder\ComponentSource\ComponentSourceInterface::getExplicitInput()
\Drupal\experience_builder\ComponentSource\ComponentSourceInterface::validateComponentInput()
This uses 3.1.
The component tree
's inputs field prop
has a trivial representation that could easily change. It is stored as a
JSON blob, and meets the following requirements:
- it contains opaque arrays that are validated by that source's
::validateComponentInput()
and are decodable using that source's::getExplicitInput()
- the
inputs
for a given component live in the same field-item as its correspondinguuid
,component_id
,parent_uuid
andslot
Note: this simplifies different (symmetric) translation strategies: it's trivial to either reuse another translation's
inputs field prop
(to show what to translate from) or not reuse anything at all — that needs only array intersection.
Note: a welcome bonus is that when real-time collaborative editing is eventually added, one user can move a
component instance
while another edits the inputs of that same component instance
, without causing a conflict.
This is because editing will be specific to a component instance
, which is modeled as a single delta.
No validation is necessary for this field prop
, because it is more easily validated at the field item
level of the
XB field type
, not at the field prop
level — there, the aforementioned ::validateComponentInput()
method is called
for every component instance
encountered in the stored component tree
. If the Component Source Plugin
complains, a
validation error occurs.
Example: A simple tree showing a root item (41595148-e5c1-4873-b373-be3ae6e21340
) with a child (3b305d86-86a7-4684-8664-7ef1fc2be070
) in the body
slot, plus another root item (41595148-e5c1-4873-b373-be3ae6e21340
).
[
'uuid' => '41595148-e5c1-4873-b373-be3ae6e21340',
'component_id' => 'sdc.xb_test_sdc.props-slots',
'component_version' => 'ab4d3ddce315cf64',
'inputs' => [
'heading' => [
'sourceType' => 'static:field_item:string',
'value' => "Hello, world!",
'expression' => 'ℹ︎string␟value',
],
],
],
[
'uuid' => '3b305d86-86a7-4684-8664-7ef1fc2be070',
'component_id' => 'sdc.xb_test_sdc.props-no-slots',
'component_version' => '95f4f1d5ee47663b',
'parent_uuid' => '41595148-e5c1-4873-b373-be3ae6e21340',
'slot' => 'the_body',
'inputs' => [
'heading' => [
'sourceType' => 'static:field_item:string',
'value' => "It's me!",
'expression' => 'ℹ︎string␟value',
],
],
[
'uuid' => '41595148-e5c1-4873-b373-be3ae6e21340',
'component_id' => 'block.system_branding_block',
'component_version' => '247a23298360adb2',
// Example, that populates a Block component instance.
// Note how much simpler the stored information is, because it uses the Block system's native input UX:
'inputs' => [
'label' => '',
'label_display' => FALSE,
'use_site_logo' => TRUE,
'use_site_name' => TRUE,
'use_site_slogan' => TRUE,
],
],
],
3.2.3 Validation
Assuming the tree column groups (uuid
, component_id
, parent_uuid
and slot
) has already been validated, a component tree
described in an XB field
then is valid
when: for each component instance
in the tree field prop
:
- getting the explicit input using
ComponentSourceInterface::getExplicitInput()
(which forBlock
requires no extra work but forSDC
involves resolving the storedprop source
s, resulting in values to be passed to the correspondingcomponent input
s) - calling
ComponentSourceInterface::validateComponentInput()
(which forBlock
uses config schema validation and forSDC
checking if\Drupal\Core\Theme\Component\ComponentValidator::validateProps()
does not throw an exception)
component input
s changes
3.2.4 Facilitating When a component
evolves, some component input
s cannot happen without also updating the stored component tree
. In
other words: an upgrade path is necessary if a Front-End Developer makes certain drastic changes:
- renaming a
component input
- changing the schema of a
component input
- adding a new required
component input
Here too, storing the inputs as separate field props
is helpful. An upgrade path for a component
would
require logic somewhat like this:
- SQL query to search the
component_id
column for uses of thiscomponent
, capture the UUIDs. If 0 matches: break. - If >0 matches, PHP logic computes the necessary changes.
- Insert the updated inputs JSON blob into that specific delta.
The above sequence assumes doing this per-entity. But this can actually be done per entity-type, or more precisely:
per XB field
. So if the XB field type
is only used for one entity type but is used in many bundles (i.e. many
different content entity type
s), then a single query can find all component instances
of the evolving component
.
After that point, the typical Drupal update path best practices apply. The key observation here: it is possible to
efficiently find all uses of a component
.
component tree
3.3 Data Model: rendering a stored See \Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItemList
.
This uses 3.2.1, 3.2.2 and 3.2.3.
Thanks to the validation in 3.2.3, it is guaranteed that each individual component instance
can be rendered. But the
goal is of course to render a component tree
(not component instance
s), by starting at the root and rendering each
component instance
in the specified component slot
.
To hydrate the stored component tree
:
- get (flat) list of
component instance
s from the treefield prop
(3.2.1): a list ofuuid
s - load the corresponding
Component config entity
for eachcomponent instance
given its UUID, which in turn enables loading the correspondingComponent Source Plugin
- get the explicit input from the inputs
field prop
(3.2.2) for eachcomponent instance
, by using theComponent Source Plugin
's::getExplicitInput()
method- for
Component Source Plugin
s with their own input UX (such asBlockComponent
), that's just forwarding the stored values - for those without their own input UX (such as
SingleDirectoryComponent
), that may require additional resolving or evaluating (such as resolving the storedprop source
— see 3.1)
- for
- pass those explicit inputs to each
component instance
, resulting in a list of hydratedcomponent instance
s - transform that list to a tree by respecting the tree
field prop
(3.2.1), by placing nestedcomponent instance
s in the specifiedcomponent slot
of the specified parentcomponent instance
(special case: the root)
To render the stored component tree
, it must first be hydrated it (see above), after which it can be
converted to a render array.
component tree
to the front end
3.4 UI Data Model: communicating a All prior sections refer to the data model that is stored (on the back end). But what makes sense on the back end does not necessarily make sense on the front end:
- the back end must integrate with many (server-side) Drupal subsystems, and it should as much as possible avoid burdening the front end with those implementation details
- the front end has different data structure needs, specifically the need for highly frequent changes, including concurrent ones during collaborative real-time editing
The front end needs the component tree
to generate a preview that the end
user can modify and interact with. For this we split the tree into layout
and model
parts. The layout
represents the tree's overall structure
and the model
represents data for each component
within that tree.
The model
is stored as a flat structure so it can more easily be queried
by the front end.
The front end layout
is a set of nodes, where each node can be one of the
following types, represented by the nodeType
key:
-
'component'
which represents acomponent instance
-
'slot'
which represents acomponent slot
in acomponent instance
. -
'region'
which represents a separatetheme region
in the user interface.
Each node in the component tree
is described with a nodeType
key which is one of the above 3 strings.
The top level of the layout
structure is an array of zero or more region
nodes.
component node
s
3.4.1 A component node
represents a single component instance
in the component tree
and
will contain zero or more component slot
s.
component node
s have the following keys
-
uuid
: a unique identifier for thecomponent instance
. -
type
: an opaque string containing aComponent config entity
ID + version that this instantiates -
name
: a name assigned by a Content Creator, to for example distinguish this particularcomponent instance
of somecomponent
among the 20 such in the current component tree -
slots
: an object ofslot node
s representing eachcomponent slot
of thiscomponent instance
(including empty slots)
An example simple component instance
of a component
with no component slot
s, and with a name for the component instance
specified by the Content Creator:
{
"nodeType": "component",
"id": "380aaa26-5678-4c86-9b32-12161ea34196",
"name": "Most Important Heading",
"type": "sdc.xb_test_sdc.heading@1b4f8df7c94d7e3c",
"slots": []
}
An example simple component instance
of a component
with a single component slot
that is empty:
{
"nodeType": "component",
"id": "177122af-1679-4ee4-b700-dcf5ab376c4a",
"type": "sdc.xb_test_sdc.one_column@f6a3a392e98e8342",
"slots": [
{
"id": "177122af-1679-4ee4-b700-dcf5ab376c4a/content",
"name": "content",
"nodeType": "slot",
"components": []
}
]
}
slot node
s
3.4.2 A slot node
must be the child of a component node
.
slot node
s have the following keys
-
name
: a human-readable name that may be displayed to the user. -
components
: an array ofcomponent node
s that represent the top-levelcomponent instances
for thiscomponent slot
-
id
: a unique ID made up of theuuid
of the parent component followed by thecomponent slot
name, separated by a slash.
{
"nodeType": "slot",
"id": "380aaa26-5678-4c86-9b32-12161ea34196/column_one",
"name": "Column one",
"components": []
}
region node
s
3.4.3 A region node
can only exist at the top level in the layout
tree and can be
thought of as a special case of a slot
that applies to the page rather than
a component
. Just like a slot node
, it can contain zero or more component node
s.
region node
s have the following keys
-
id
is the identifier of thetheme region
. -
name
: a human-readable name that may be displayed to the user. -
components
: an array ofcomponent node
s that represent the top-levelcomponent instances
for thistheme region
The theme region
with the ID of content
is treated specially by the server, and assumed to contain
the content entity
. The front end should not need to do anything special
here except perhaps default to editing the content
region (but perhaps the
server should express this default via a flag somewhere?).
{
"nodeType": "region",
"id": "content",
"name": "Content",
"components": []
}
3.4.4 The complete API response
The API response contains two top level keys:
-
layout
: thecomponent tree
described above, using the 3 layout tree node types -
model
: an array of model data for eachcomponent node
in the tree, keyed by the UUID of thecomponent instance
.
(What if the model and layout get out of sync? We could theoretically have UUIDs that don't have model values, or model values that are orphaned and don't have corresponding components in the layout. The server side's validation logic forbids saving in this case.)
A complete example, with three region node
s:
- A
'header'
region with a single component instance. - A
'content'
region with multiple, nested component instances a tree. - An empty
'footer'
region.
{
"layout": [
{
"nodeType": "region",
"id": "header",
"name": "Header",
"components": [
{
"nodeType": "component",
"id": "a164fa84-0460-40b0-a428-bf332b4a792a",
"type": "block.system_branding_block@247a23298360adb2",
"slots": []
}
]
},
{
"nodeType": "region",
"id": "content",
"name": "Content",
"components": [
{
"nodeType": "component",
"id": "97fb7bb9-4c8e-4fdc-87a8-c39ac9e8e618",
"type": "sdc.xb_test_sdc.two_column@e5ef92acda2ee2d1",
"slots": [
{
"nodeType": "slot",
"id": "97fb7bb9-4c8e-4fdc-87a8-c39ac9e8e618/column_one",
"components": [
{
"nodeType": "component",
"id": "e8ecc571-0221-40d8-9ab2-262389fabd58",
"type": "sdc.xb_test_sdc.heading@1b4f8df7c94d7e3c",
"slots": []
},
{
"nodeType": "component",
"id": "baf231e8-b214-4e3e-93d3-5d3f03a1eae9",
"type": "sdc.xb_test_sdc.druplicon@some-version-string",
"slots": []
}
]
},
{
"nodeType": "slot",
"id": "97fb7bb9-4c8e-4fdc-87a8-c39ac9e8e618/column_two",
"components": [
{
"nodeType": "component",
"id": "39648574-b937-4a5a-b1b2-9db0f30ae315",
"type": "sdc.xb_test_sdc.one_column@f6a3a392e98e8342",
"slots": [
{
"nodeType": "slot",
"id": "39648574-b937-4a5a-b1b2-9db0f30ae315/content",
"components": [
{
"nodeType": "component",
"id": "a1cfa9f1-0088-45d9-b837-39571485b75e",
"type": "sdc.xb_test_sdc.my-hero",
"slots": []
}
]
}
]
}
]
}
]
}
]
},
{
"nodeType": "region",
"id": "footer",
"name": "Footer",
"components": []
}
],
"model": {
"a164fa84-0460-40b0-a428-bf332b4a792a": {},
"97fb7bb9-4c8e-4fdc-87a8-c39ac9e8e618": {},
"e8ecc571-0221-40d8-9ab2-262389fabd58": {
"text": "Heading",
"style": "primary",
"element": "h1"
},
"baf231e8-b214-4e3e-93d3-5d3f03a1eae9": {},
"39648574-b937-4a5a-b1b2-9db0f30ae315": {},
"a1cfa9f1-0088-45d9-b837-39571485b75e": {
"heading": "Hero",
"subheading": "My subheading"
}
}
}