Layout JSON
The JSON v1 layout document, field by field, with the layout defaults, the state it loads into and how a document is validated before it loads.
A layout is saved and loaded as a LayoutJson<T> document, version 1. createModel(json) and the
layout.load command read it, and model.get("layout-json") writes it. It is plain JSON: the
state the model holds has the same shape, with the omitted fields filled in.
{
"version": 1,
"defaults": { "tab": { "poppable": true }, "border": { "size": 220 } },
"root": {
"type": "row",
"id": "root",
"children": [
{
"type": "tabset",
"id": "editors",
"weight": 70,
"children": [
{ "id": "readme", "component": "editor", "label": "README.md", "data": { "path": "/README.md" } }
]
},
{
"type": "tabset",
"id": "tools",
"weight": 30,
"droppable": false,
"children": [{ "id": "log", "component": "log", "label": "Log", "pinned": true }]
}
]
},
"active": "editors",
"borders": [
{ "location": "start", "mode": "overlay", "children": [{ "component": "files", "label": "Files" }] }
],
"windows": [
{
"id": "w1",
"rect": { "x": 100, "y": 100, "width": 600, "height": 400 },
"root": { "type": "row", "children": [{ "type": "tabset", "children": [{ "component": "log", "label": "Build" }] }] }
}
]
}A tab's text (its name, icon, tooltip) is not a layout field: it lives in the tab's data, typed
by your registry (Typed data). The layout stores structure, sizes and
behaviour only.
The top-level row is horizontal (unless defaults.layout.rootOrientation is "vertical"); a row
inside a row takes the opposite orientation, so it acts as a column. Orientation is never stored.
The document
LayoutJson<T>:
| field | type | description |
|---|---|---|
version | 1 | the format version; v1 accepts only 1 (the hook for future migrations) |
defaults | LayoutDefaults | the layout's defaults for tabs, tabsets and borders, and its settings (Defaults); default {} |
root | RowJson<T> | the main layout's root row |
active | string | the id of the main layout's active tabset |
maximized | string | the id of the main layout's maximized tabset |
borders | BorderJson<T>[] | the borders of the main layout, at most one per side; default [] |
windows | WindowJson<T>[] | the popout windows, each with its own layout; default [] |
active and maximized are stored on the layout, not on a tabset: a layout has at most one of
each, and each must name a tabset of that layout.
Rows
RowJson<T>:
| field | type | description |
|---|---|---|
type | "row" | required |
id | string | generated when missing |
weight | number | its size relative to its siblings (flex grow); default 100 |
data | RowDataOf<T> | the app's data for the row, typed by the registry's row |
children | (RowJson<T> | TabsetJson<T>)[] | tabsets and nested rows; default [] |
Tabsets
TabsetJson<T>, which also takes every field of TabsetDefaults:
| field | type | description |
|---|---|---|
type | "tabset" | required |
id | string | generated when missing |
weight | number | its size relative to its siblings; default 100 |
selected | number | the index of the selected tab; default 0, -1 when empty; clamped to the tabs |
data | TabsetDataOf<T> | the app's data for the tabset (its accessible name, for example), typed by the registry's tabset |
children | TabJson<T>[] | its tabs, pinned ones first; default [] |
droppable | boolean | tabs can be dropped into it (its strip and center) |
draggable | boolean | the whole tabset can be dragged (tabset.move) |
splittable | boolean | dropping on one of its edges splits it |
maximizable | boolean | it can be maximized |
closable | boolean | it can be closed (tabset.close), or merged away into another tabset |
deleteWhenEmpty | boolean | it is removed when its last tab leaves |
autoSelectTab | boolean | a tab added or moved into it is selected |
minWidth, minHeight, maxWidth, maxHeight | number | its size limits in px, used by the splitters |
A behaviour field left out follows the layout's defaults.tabset, then the built-in value
(Settings).
Tabs
TabJson<T> is a TabInit (the fields a new tab is created with, shared with tab.add and drag
sources) whose type may be omitted:
| field | type | description |
|---|---|---|
type | "tab" | optional |
id | string | generated when missing |
component | string | what the tab shows: a component of your registry, which types data |
label | string | required: the tab's name, as your app wrote it. The model never translates or renders it; your strip renders it ({tab.label}), and tab.rename changes it |
data | TabDataOf<T, K> | the app's data for the tab (its icon, dirty flag, …); required when the component's data type is |
pinned | boolean | the tab sits at the start of its strip, cannot close and cannot leave its tabset; a border's tabs cannot be pinned |
closable | boolean | it can be closed |
draggable | boolean | it can be dragged or moved |
poppable | boolean | it can be popped out into a window, or dropped into one |
renamable | boolean | it can be renamed (tab.rename) |
minWidth, minHeight, maxWidth, maxHeight | number | its size limits in px (SizeLimits) |
borderWidth | number | its own panel width in a start or end border (the border's size otherwise) |
borderHeight | number | its own panel height in a top or bottom border |
The types:
TabInit<K, D>: the fields above for componentKwith dataD.TabOwnFields:pinned,borderWidthandborderHeight, the fields of a tab no layout default applies to; the others followTabDefaults.TabInitOf<T>: a union ofTabInitover the registry's components, sodatais checked againstcomponent.TabJson<T>:TabInitOf<T>with an optionaltype: "tab".DataField<D>: howdatais declared, optional exactly whenDallowsundefined.
import type { TabJson } from "@fragiola/dockable";
const readme: TabJson<Types> = {
id: "readme",
component: "editor",
label: "README.md",
data: { path: "/README.md", dirty: false },
};Borders
BorderJson<T>, which also takes every field of BorderDefaults:
| field | type | description |
|---|---|---|
type | "border" | optional |
id | string | default border_<location> |
location | BorderLocation | "top", "bottom", "start" or "end"; one border per side |
selected | number | the index of the open tab; default -1 (the panel is closed) |
show | boolean | false hides the border entirely |
data | BorderDataOf<T> | the app's data for the border, typed by the registry's border |
children | TabJson<T>[] | its tabs; default [] |
size | number | its panel's size in px (width or height by side) |
minSize, maxSize | number | the limits of size |
mode | BorderMode | "docked" (the panel sits beside the layout and shrinks it) or "overlay" (the panel opens over it) |
autoHide | boolean | hide the strip while the border has no tabs (a drag near its edge reveals it) |
droppable | boolean | tabs can be dropped into it |
autoSelectTabWhenOpen | boolean | a tab added while the panel is open is selected |
autoSelectTabWhenClosed | boolean | a tab added while the panel is closed is selected (which opens it) |
See Borders.
Windows
WindowJson<T>, one entry of windows: a popout window and its layout.
| field | type | description |
|---|---|---|
id | string | the window's id, which is also its layout id; generated when missing |
rect | Rect | the window's screen rect (x, y, width, height); default 600 by 400, cascaded from 50, 50 |
root | RowJson<T> | the window's root row |
active | string | the id of the window's active tabset |
maximized | string | the id of the window's maximized tabset |
The engine records a window's rect when the user moves or resizes it (window.configure), so a
saved layout reopens its windows in place (Popouts).
Defaults
defaults holds the value a node's behaviour field takes when the node does not set it, and the
layout-wide settings. LayoutDefaults:
| field | type | description |
|---|---|---|
tab | TabDefaults | the defaults of every tab |
tabset | TabsetDefaults | the defaults of every tabset |
border | BorderDefaults | the defaults of every border |
layout | LayoutSettings | the settings of every layout (main and windows) |
The rule is node.x ?? defaults[kind].x ?? BUILT_IN[kind].x, with no other inheritance
(Settings). Change the defaults at runtime with
layout.configure (LayoutDefaultsPatch).
TabDefaults (built-in values in the last column):
| field | type | built-in |
|---|---|---|
closable | boolean | true |
draggable | boolean | true |
poppable | boolean | false |
renamable | boolean | true |
minWidth, minHeight | number | 1 |
maxWidth, maxHeight | number | 99999 |
TabsetDefaults:
| field | type | built-in |
|---|---|---|
droppable, draggable, splittable, maximizable, closable | boolean | true |
deleteWhenEmpty, autoSelectTab | boolean | true |
minWidth, minHeight | number | 1 |
maxWidth, maxHeight | number | 99999 |
BorderDefaults:
| field | type | built-in |
|---|---|---|
size | number | 200 |
minSize | number | 1 |
maxSize | number | 99999 |
mode | BorderMode | "docked" |
autoHide | boolean | false |
droppable | boolean | true |
autoSelectTabWhenOpen | boolean | true |
autoSelectTabWhenClosed | boolean | false |
LayoutSettings:
| field | type | built-in | description |
|---|---|---|---|
rootOrientation | Orientation | "horizontal" | the orientation of every layout's root row; nested rows alternate |
edgeDock | boolean | true | a drag offers the layout's edges as drop targets |
edgeDockMargin | number | 10 | the depth in px of each edge band |
edgeDockLength | number | 100 | the length in px of each edge band, centred on its edge (at most the edge) |
SizeLimits is the size part of tabs, tabsets and their defaults, in px:
| field | type | description |
|---|---|---|
minWidth | number | the smallest width |
minHeight | number | the smallest height |
maxWidth | number | the largest width |
maxHeight | number | the largest height |
The state
Loading a document gives the state model.state holds: a LayoutState<T> whose nodes are the
document's, frozen, with every omitted structural field filled in. The node types are what the
queries return.
| state type | from | filled in on load |
|---|---|---|
LayoutState<T> | LayoutJson<T> | defaults ({}), borders and windows ([]); no version |
RowNode<T> | RowJson<T> | id, weight, children |
TabsetNode<T> | TabsetJson<T> | id, weight, selected, children |
TabNode<K, D> (TabOf<T>) | TabJson<T> | type, id |
BorderNode<T> | BorderJson<T> | type, id, selected, children |
WindowLayout<T> | WindowJson<T> | id, rect |
A node's behaviour fields stay as the document set them (absent means "use the defaults"), so
model.get("layout-json") writes back exactly what was set. selected is an index into
children, -1 for none. Loading also tidies the tree, as every command does: an empty tabset is removed when it
allows it (deleteWhenEmpty and closable), an empty nested row is removed, a nested row with one child is
replaced by that child, and an empty window is removed. The main layout always keeps a tabset: when
tidying would leave it with none, one of its empty tabsets stays, with its id: the active one,
else the first (so
{ "type": "tabset", "id": "editor", "children": [] } loads as is, ready for a tab.add to
"editor"), and a main layout with no tabset at all gets a new empty one. Either becomes active,
and neither stays maximized.
Validation
createModel and layout.load validate a document before anything changes, and report every
problem, each with a JSON pointer (RFC 6901) into the document:
- against
layoutSchema, the JSON Schema ofLayoutJson(exported, so other tools can use it); - then what a schema cannot check: duplicate ids, the reserved id
"main", two borders on one side, a pinned tab in a border, and anactiveormaximizedthat is not a tabset of its layout; - and each tab's
dataagainst its component's schema, when you registered one (dataSchemas, on Model).
Each problem is a ValidationIssue:
| field | type | description |
|---|---|---|
path | string | where: a JSON pointer (/root/children/0/id; "" is the document itself) |
message | string | what, in English, for developers |
How each entry point reports them:
| entry point | an invalid document |
|---|---|
createModel(json) | throws a LayoutValidationError: error.issues lists every issue, and its message names the first (invalid layout: /root/id "main" is reserved for the main layout) |
validateLayout(json, options?) | returns { ok: false, issues } (or { ok: true, value }), never throws; options.dataSchemas checks data too |
layout.load | returns invalid_payload with every issue in error.issues, the paths under /layout |
import { createModel, type LayoutJson, validateLayout } from "@fragiola/dockable";
const checked = validateLayout<LayoutJson<Types>>(JSON.parse(text));
const model = checked.ok
? createModel<Types>(checked.value)
: createModel<Types>(defaultLayout);
if (!checked.ok) {
for (const issue of checked.issues) console.warn(issue.path || "/", issue.message);
}In a running app, prefer loading into the model you have, as untrusted JSON:
model.dispatch({ command: "layout.load", payload: { layout } }) validates it and changes
nothing when it is invalid (Saving and restoring).
JSON Schema types
layoutSchema, a command's payloadSchema and your dataSchemas are JsonSchema objects: the
subset of JSON Schema 2020-12 that the model's validator checks.
| keyword | type |
|---|---|
type | JsonSchemaType: "object", "array", "string", "number", "integer", "boolean" or "null" |
properties, required, additionalProperties | objects (additionalProperties is a boolean) |
items, minItems | arrays |
enum, const | fixed values (JsonValue) |
oneOf, anyOf | alternatives |
minimum, exclusiveMinimum, minLength | bounds |
$ref, $defs | local references (#/$defs/…) |
title, description | documentation |
Other keywords may be present (a data schema written for another validator), and are ignored.
JsonValue is any JSON value. FromSchema<S> turns such a schema into its TypeScript type
(Model).
From the old format
Documents written for FlexLayout's format do not load: v1 is a new format. What became of the old attributes:
| old | now |
|---|---|
layout (the root row), subLayouts | root; windows, an array of window layouts with an id and a root |
global | defaults, by kind: tabEnableClose is defaults.tab.closable, tabSetMinWidth is defaults.tabset.minWidth, borderSize is defaults.border.size, enableEdgeDock is defaults.layout.edgeDock |
rootOrientationVertical | defaults.layout.rootOrientation |
a tabset's active, maximized | the layout's active and maximized |
enableDeleteWhenEmpty | deleteWhenEmpty |
a border's borderType, enableAutoHide | mode, autoHide |
borderWidth, borderHeight of -1 | omitted |
a tab's name | label: required, rendered by your strip (the packages never render it) |
a tab's altName, icon, helpText | moved to data: they are your text, rendered by your strip |
a tab's enableRename | renamable: tab.rename refuses a tab that is not |
a tab's enablePin | moved to data: it only gates your own menu item |
a tabset's name | moved to data (the tab list's accessible name is yours to pass) |
enableRenderOnDemand, enableScrollbars, enableWindowReMount | props: renderOnDemand on Dockable.Panels, scrollable and remountInWindow on Dockable.Panel |
borderLeftTabDirection, enableEdgeDockIndicators | props: tabDirection on Dockable.Border; render Dockable.EdgeIndicator or not |
className, contentClassName, classNameTabStrip, tabsetClassName, closeType, tabLocation, enableTabStrip, enableTabWrap, enableTabScrollbar, enableActiveIcon, enableCloseButton, enableSingleTabStretch, enablePopoutIcon, enablePopoutOverlay, enableRotateBorderIcons | removed: styling and markup are yours |
floats, tab groups, tab sub-layouts (enableFloat, subLayoutId, tabGroupType, …) | removed: not built (Limitations) |