Saving and restoring layouts
Save the layout's JSON, restore it with createModel or layout.load, and handle a stored layout that no longer validates.
A layout is plain JSON, version 1 (LayoutJson). Save model.get("layout-json"); restore it
with createModel(json) at start-up, or with the layout.load command into a model that is already on
screen. Both validate the document first and say exactly what is wrong with one that does not fit.
Saving on every change
model.subscribe calls a listener after every committed command, with the command's event. Skip
the transient steps of a live gesture (a splitter drag: its last command is not transient), or
debounce, and guard storage access: it can throw (private mode, quota).
import type { Model } from "@fragiola/dockable-react";
const KEY = "my-app:layout";
function persist(model: Model<Types>) {
return model.subscribe((event) => {
if (event.transient) return; // the gesture ends with a normal command
try {
localStorage.setItem(KEY, JSON.stringify(model.get("layout-json")));
} catch {
// storage unavailable: keep working without persistence
}
});
}
// in a component: subscribe once per model, unsubscribe on unmount
useEffect(() => persist(model), [model]);Restoring at start-up
createModel validates the document against JSON v1 and throws a LayoutValidationError when it
does not fit, with every problem in error.issues (each a ValidationIssue: a JSON pointer
path and a message). A saved layout outlives your code, so expect it: fall back to your
default layout, and tell the user why if it matters.
import { createModel, LayoutValidationError, type Model } from "@fragiola/dockable-react";
function loadModel(): Model<Types> {
try {
const saved = localStorage.getItem(KEY);
if (saved) return createModel<Types>(JSON.parse(saved));
} catch (error) {
if (error instanceof LayoutValidationError) {
// every problem: a JSON pointer into the document and a message
console.warn(error.issues.map((issue) => `${issue.path} ${issue.message}`));
}
// corrupt, invalid or unavailable: fall back to the default
}
return createModel<Types>(defaultJson);
}
const [model] = useState(loadModel);To check a document without building a model (and without throwing), use validateLayout(json):
it returns { ok: true, value } or { ok: false, issues }.
Validating your data too
The schema checks the layout's shape, not your tabs' data. Register a JSON Schema per component
with dataSchemas, and createModel, layout.load, tab.add, tab.set-data and tab.set-component validate each
tab's data with it, so a stored editor that lost its path is reported instead of crashing the
editor:
import { createModel, type JsonSchema } from "@fragiola/dockable-react";
const editor: JsonSchema = {
type: "object",
properties: { path: { type: "string" }, dirty: { type: "boolean" } },
required: ["path"],
};
const model = createModel<Types>(json, { dataSchemas: { editor } });validateLayout(json, { dataSchemas }) takes the same schemas.
Restoring into a mounted layout
To load a layout while one is on screen (a "Reset layout" button, a saved workspace, an undo step),
run layout.load on the model you already have. The model keeps its identity, so nothing is
re-created: tabs whose id survives keep their mounted content, and the result lists the ids
added and removed.
const reset = () => model.run("layout.load", { layout: defaultJson });A stored document is untrusted JSON: dispatch takes it as is, validates it, and returns a result
instead of throwing. An invalid layout is an invalid_payload error whose issues carry every
problem (their paths start with /payload/layout; with run, /layout), and the current layout
stays:
const result = model.dispatch({ command: "layout.load", payload: { layout: JSON.parse(text) } });
if (!result.ok) {
showError(result.error.issues?.map((issue) => `${issue.path} ${issue.message}`) ?? [result.error.message]);
}Give tabs stable ids in your default JSON if content should survive a reset. Tabs without an id
get a generated one, which a different JSON will not match.
What is saved
model.get("layout-json") returns a LayoutJson (version, root, and when they are set
defaults, active, maximized, borders and windows), and
createModel(model.get("layout-json")) gives back the same state. It holds:
- the tree: rows, tabsets and tabs with their ids, weights,
componentanddata, and every behaviour field set on a node (closable,pinned,minWidth, …); - the selected tab of each tabset and border, and each layout's active and maximized tabset;
- the layout
defaults; - popout windows (
windows), with their screen rect. Restoring reopens them, which the browser may block without a user gesture.
It does not save your content's state (a text field's value, a scroll position of your own
component). Store that yourself, keyed by tab id, or in the tab's data.
Saving content state with the layout
Keep content state in your own store, keyed by tab id, and save it next to the layout:
const saved = { layout: model.get("layout-json"), drafts: Object.fromEntries(drafts) };When the state should travel inside the layout JSON (to a popout, into undo history), write
it into the tab's data with tab.set-data when it changes (debounced for typing). It takes a
patch: only the keys you pass change.
model.run("tab.set-data", { tabId: tab.id, data: { draft } });Each such command is an undo step; add "tab.set-data" to your
undo history's ignored commands if it should not be (renames are
tab.rename, so they stay undoable).
Versioning saved layouts
The document carries its format's version (1), and loading refuses any other value. Your own
data changes too: store your app's version next to the layout and fall back to the default when it
does not match, or migrate: the JSON is yours to transform before createModel or layout.load.
With dataSchemas, a tab whose data no longer fits is reported rather than rendered, and a
component your registry no longer has is yours to check (or drop) before loading.
Undo and redo
The package ships no undo/redo. It gives you the change events and layout.load to build yours; the examples' UndoManager is a starting point to copy.
Keyboard and accessibility
The fixed ARIA keys, a configurable key map for closing tabs and moving focus, and shortcuts of your own that run commands.