Documentation
API: core (@fragiola/dockable)

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>:

fieldtypedescription
version1the format version; v1 accepts only 1 (the hook for future migrations)
defaultsLayoutDefaultsthe layout's defaults for tabs, tabsets and borders, and its settings (Defaults); default {}
rootRowJson<T>the main layout's root row
activestringthe id of the main layout's active tabset
maximizedstringthe id of the main layout's maximized tabset
bordersBorderJson<T>[]the borders of the main layout, at most one per side; default []
windowsWindowJson<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>:

fieldtypedescription
type"row"required
idstringgenerated when missing
weightnumberits size relative to its siblings (flex grow); default 100
dataRowDataOf<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:

fieldtypedescription
type"tabset"required
idstringgenerated when missing
weightnumberits size relative to its siblings; default 100
selectednumberthe index of the selected tab; default 0, -1 when empty; clamped to the tabs
dataTabsetDataOf<T>the app's data for the tabset (its accessible name, for example), typed by the registry's tabset
childrenTabJson<T>[]its tabs, pinned ones first; default []
droppablebooleantabs can be dropped into it (its strip and center)
draggablebooleanthe whole tabset can be dragged (tabset.move)
splittablebooleandropping on one of its edges splits it
maximizablebooleanit can be maximized
closablebooleanit can be closed (tabset.close), or merged away into another tabset
deleteWhenEmptybooleanit is removed when its last tab leaves
autoSelectTabbooleana tab added or moved into it is selected
minWidth, minHeight, maxWidth, maxHeightnumberits 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:

fieldtypedescription
type"tab"optional
idstringgenerated when missing
componentstringwhat the tab shows: a component of your registry, which types data
labelstringrequired: 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
dataTabDataOf<T, K>the app's data for the tab (its icon, dirty flag, …); required when the component's data type is
pinnedbooleanthe tab sits at the start of its strip, cannot close and cannot leave its tabset; a border's tabs cannot be pinned
closablebooleanit can be closed
draggablebooleanit can be dragged or moved
poppablebooleanit can be popped out into a window, or dropped into one
renamablebooleanit can be renamed (tab.rename)
minWidth, minHeight, maxWidth, maxHeightnumberits size limits in px (SizeLimits)
borderWidthnumberits own panel width in a start or end border (the border's size otherwise)
borderHeightnumberits own panel height in a top or bottom border

The types:

  • TabInit<K, D>: the fields above for component K with data D.
  • TabOwnFields: pinned, borderWidth and borderHeight, the fields of a tab no layout default applies to; the others follow TabDefaults.
  • TabInitOf<T>: a union of TabInit over the registry's components, so data is checked against component.
  • TabJson<T>: TabInitOf<T> with an optional type: "tab".
  • DataField<D>: how data is declared, optional exactly when D allows undefined.
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:

fieldtypedescription
type"border"optional
idstringdefault border_<location>
locationBorderLocation"top", "bottom", "start" or "end"; one border per side
selectednumberthe index of the open tab; default -1 (the panel is closed)
showbooleanfalse hides the border entirely
dataBorderDataOf<T>the app's data for the border, typed by the registry's border
childrenTabJson<T>[]its tabs; default []
sizenumberits panel's size in px (width or height by side)
minSize, maxSizenumberthe limits of size
modeBorderMode"docked" (the panel sits beside the layout and shrinks it) or "overlay" (the panel opens over it)
autoHidebooleanhide the strip while the border has no tabs (a drag near its edge reveals it)
droppablebooleantabs can be dropped into it
autoSelectTabWhenOpenbooleana tab added while the panel is open is selected
autoSelectTabWhenClosedbooleana 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.

fieldtypedescription
idstringthe window's id, which is also its layout id; generated when missing
rectRectthe window's screen rect (x, y, width, height); default 600 by 400, cascaded from 50, 50
rootRowJson<T>the window's root row
activestringthe id of the window's active tabset
maximizedstringthe 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:

fieldtypedescription
tabTabDefaultsthe defaults of every tab
tabsetTabsetDefaultsthe defaults of every tabset
borderBorderDefaultsthe defaults of every border
layoutLayoutSettingsthe 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):

fieldtypebuilt-in
closablebooleantrue
draggablebooleantrue
poppablebooleanfalse
renamablebooleantrue
minWidth, minHeightnumber1
maxWidth, maxHeightnumber99999

TabsetDefaults:

fieldtypebuilt-in
droppable, draggable, splittable, maximizable, closablebooleantrue
deleteWhenEmpty, autoSelectTabbooleantrue
minWidth, minHeightnumber1
maxWidth, maxHeightnumber99999

BorderDefaults:

fieldtypebuilt-in
sizenumber200
minSizenumber1
maxSizenumber99999
modeBorderMode"docked"
autoHidebooleanfalse
droppablebooleantrue
autoSelectTabWhenOpenbooleantrue
autoSelectTabWhenClosedbooleanfalse

LayoutSettings:

fieldtypebuilt-indescription
rootOrientationOrientation"horizontal"the orientation of every layout's root row; nested rows alternate
edgeDockbooleantruea drag offers the layout's edges as drop targets
edgeDockMarginnumber10the depth in px of each edge band
edgeDockLengthnumber100the 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:

fieldtypedescription
minWidthnumberthe smallest width
minHeightnumberthe smallest height
maxWidthnumberthe largest width
maxHeightnumberthe 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 typefromfilled 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 of LayoutJson (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 an active or maximized that is not a tabset of its layout;
  • and each tab's data against its component's schema, when you registered one (dataSchemas, on Model).

Each problem is a ValidationIssue:

fieldtypedescription
pathstringwhere: a JSON pointer (/root/children/0/id; "" is the document itself)
messagestringwhat, in English, for developers

How each entry point reports them:

entry pointan 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.loadreturns 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.

keywordtype
typeJsonSchemaType: "object", "array", "string", "number", "integer", "boolean" or "null"
properties, required, additionalPropertiesobjects (additionalProperties is a boolean)
items, minItemsarrays
enum, constfixed values (JsonValue)
oneOf, anyOfalternatives
minimum, exclusiveMinimum, minLengthbounds
$ref, $defslocal references (#/$defs/…)
title, descriptiondocumentation

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:

oldnow
layout (the root row), subLayoutsroot; windows, an array of window layouts with an id and a root
globaldefaults, by kind: tabEnableClose is defaults.tab.closable, tabSetMinWidth is defaults.tabset.minWidth, borderSize is defaults.border.size, enableEdgeDock is defaults.layout.edgeDock
rootOrientationVerticaldefaults.layout.rootOrientation
a tabset's active, maximizedthe layout's active and maximized
enableDeleteWhenEmptydeleteWhenEmpty
a border's borderType, enableAutoHidemode, autoHide
borderWidth, borderHeight of -1omitted
a tab's namelabel: required, rendered by your strip (the packages never render it)
a tab's altName, icon, helpTextmoved to data: they are your text, rendered by your strip
a tab's enableRenamerenamable: tab.rename refuses a tab that is not
a tab's enablePinmoved to data: it only gates your own menu item
a tabset's namemoved to data (the tab list's accessible name is yours to pass)
enableRenderOnDemand, enableScrollbars, enableWindowReMountprops: renderOnDemand on Dockable.Panels, scrollable and remountInWindow on Dockable.Panel
borderLeftTabDirection, enableEdgeDockIndicatorsprops: tabDirection on Dockable.Border; render Dockable.EdgeIndicator or not
className, contentClassName, classNameTabStrip, tabsetClassName, closeType, tabLocation, enableTabStrip, enableTabWrap, enableTabScrollbar, enableActiveIcon, enableCloseButton, enableSingleTabStretch, enablePopoutIcon, enablePopoutOverlay, enableRotateBorderIconsremoved: styling and markup are yours
floats, tab groups, tab sub-layouts (enableFloat, subLayoutId, tabGroupType, …)removed: not built (Limitations)