Data Model
A Grid Element is a regular tt_content record with
CType = gridelements_pi1. What makes it a grid is that it can act
as a container for other tt_content records, its children, which
are themselves ordinary content elements and may in turn be Grid
Elements (see Nesting).
This page describes how that container/child structure is actually
stored: which fields exist, which one carries the relation itself, and
why colPos is still involved even though it is not that relation.
Parent-Child Relations and IRRE builds on this to explain how TYPO3's IRRE
(Inline Relational Record Editing) and the DataHandler cooperate to
keep these fields consistent while editors work.
Overview
Grid Element (tt_content, CType = gridelements_pi1)
|
| tx_gridelements_children (IRRE, parent side)
v
Child tt_content
tx_gridelements_container = <parent uid> -- the relation
tx_gridelements_columns = <cell number> -- position inside the parent's grid
colPos = -1 -- sentinel, not the relation
backupColPos -- saved colPos while unused
| Field | Stored on | Role |
|---|---|---|
tx_gridelements_container | child | The parent-child relation. Holds the uid of the container record. |
tx_gridelements_columns | child | Which cell of the container's own grid layout the child occupies. |
tx_gridelements_children | container | IRRE parent-side declaration plus a maintained child count. Not an independent relation, see below. |
colPos | both | TYPO3 Core placement and compatibility state, not the Grid Elements relation. Carries sentinel values, see below. |
backupColPos | both | The colPos value to restore once an element's current column
becomes available again. |
The relation: tx_gridelements_container
tx_gridelements_container is a select field on the child record
that points, by uid, at its parent Grid Element. This is the parent-child
relation. Every query that needs to find the children of a given container,
including the frontend rendering pipeline in Nesting, filters on
this field:
-- GridChildrenProcessor::process(), simplified
SELECT * FROM tt_content WHERE tx_gridelements_container = <container uid>
Its TCA (Configuration/TCA/Overrides/tt_content.php) restricts the
selectable containers to tt_content records with
CType = gridelements_pi1 on the same page and in a matching language, and
excludes the record itself and anything the record is already the
container of, which prevents the most direct kind of self-reference at
the form level (the full cycle guard is described in
Parent-Child Relations and IRRE).
The cell: tx_gridelements_columns
tx_gridelements_columns identifies which cell of the container's
own grid layout the child sits in. Its value corresponds to a
colPos key defined inside that layout's own grid configuration
(rows.<n>.columns.<m>.colPos, see the Grid TS Syntax reference), not
to the child's own page-level colPos. A child's position is
therefore fully described by the pair (tx_gridelements_container,
tx_gridelements_columns): which container, and which cell of that
container.
The parent side: tx_gridelements_children
tx_gridelements_children is declared in TCA as an inline field
(foreign_table = tt_content,
foreign_field = tx_gridelements_container). This is what makes the container's edit
form present and manage its children through TYPO3's IRRE machinery, see
Parent-Child Relations and IRRE for how that works.
The field is also a stored integer column, and it is easy to read that
as a second, parent-side copy of the relation. It is not. TYPO3's IRRE
already determines a container's children purely by querying
tx_gridelements_container, exactly as shown above; no code in Grid
Elements ever reads which children exist from
tx_gridelements_children. What the stored integer holds is a
maintained count, kept in sync by
AbstractDataHandler::doGridContainerUpdate() every time a child is
added to, removed from, or moved out of a container. It exists so the
count is available without a separate query, not as a source of truth.
If it ever drifts out of sync with the actual number of children, see
Data Consistency Tools.
colPos and its sentinel values
colPos does not establish the Grid Elements parent-child relation.
It is TYPO3 Core's own placement field, and Grid Elements keeps writing
it for every content element, container children included, because
Core and its Page module, list module, and workspace preview all group
tt_content by colPos and need a value there regardless of
whether an element also happens to be a grid child. The field's
underlying database column had to change from an unsigned integer to a
signed one to make this possible at all, since Grid Elements relies on
two negative sentinel values:
| Value | Meaning |
|---|---|
>= 0 | Page-level placement. The element is placed in a column of the page-level Backend Layout and is not a child of another Grid Element. |
-1 | Grid element column. Set on every child of every container,
regardless of which container or which cell,
tx_gridelements_columns carries that detail instead. |
-2 | Non-used elements column. The element currently has no valid
column, typically because a layout change removed the column it
used to occupy. Surfaced in the backend by giving a page's own
backend layout a column configured with colPos = -2. |
This column type change must never be reverted by a later upgrade
script or manual schema fix. Reverting it back to an unsigned integer
means the negative sentinels can no longer be stored. This does not
detach a child from its Grid Element: tx_gridelements_container,
the actual parent-child relation, is untouched by a colPos schema
change. What breaks is TYPO3 Core's own placement handling and backend
presentation, which read colPos directly and, unable to see the
sentinel value, interpret the affected record incorrectly, typically
as if it were an ordinary page-level element rather than a grid child.
If this happens, see Data Consistency Tools for the repair tool,
which restores the -1 sentinel for any row whose
tx_gridelements_container relation is still intact.
backupColPos exists to make the -2 state reversible. When
AfterDatabaseOperations::setUnusedElements() detects that a layout
change has removed an element's column, it moves the element to
colPos = -2 and saves the previous value in backupColPos. When a
later layout change makes a matching column available again, the two
values are swapped back, restoring the element to its original
position without editor intervention.
A related case is a child being detached from its container entirely
(tx_gridelements_container reset to 0 while colPos was
-1). Rather than defaulting the freed element to column 0,
PreProcessFieldArray::checkForRootColumn() walks up the chain of
former containers to find the colPos of the outermost page-level
ancestor, so the element reappears in the same major page column it
structurally originated from.
One container-record field and one separate layout table are worth
naming here, since they appear throughout this section: the
container's own selected grid layout is stored in
tt_content.tx_gridelements_backend_layout as a string identifier.
Database-based layout definitions are stored separately in the
tx_gridelements_backend_layout table, which contains their title,
configuration and icons. Layouts may also be defined through TSconfig,
so the value in tt_content.tx_gridelements_backend_layout is a
layout identifier rather than an unconditional reference to a row in
that table.