Dock manager
@xui/dock-managerAn IDE-style docking layout driven by one serialisable tree: resizable split panes, tab groups, a document host for editor tabs, panes that collapse to the edges, and floating windows. Drag a pane header or tab and Visual Studio's docking targets appear: a five-way joystick over the pane under the pointer — arms to split it, centre to tab with it — plus an outer ring at the edges for docking against the whole layout. The target that would be used lights up and an outline previews the space it would take. Drop outside the layout to float the pane. Every action is also on the header: pin, float, maximize, close. Splitter gutters take arrow keys, and tab strips take arrow/Home/End. A pane's body is declared once as <ng-template xuiDockContent="id"> and then *moved* rather than rebuilt, so scroll position and half-typed input survive a drag.
Install
pnpm add @xui/dock-manager Add the barrel to a standalone component's imports:
import { XuiDockManagerImports } from '@xui/dock-manager';@Component({ imports: [XuiDockManagerImports], // …})Examples
Default
<xui-dock-manager [(layout)]="layout" class="border-border h-[26rem] w-[52rem] rounded-lg border"> <ng-template xuiDockContent="explorer"></ng-template> <ng-template xuiDockContent="details"></ng-template></xui-dock-manager>Ide
<xui-dock-manager [(layout)]="layout" class="border-border h-[34rem] w-[60rem] rounded-lg border"> <ng-template xuiDockContent="explorer"></ng-template> <!-- A bare "{" would start an ICU expression in an Angular template. --> <ng-template xuiDockContent="doc1"></ng-template> <ng-template xuiDockContent="doc2"></ng-template> <ng-template xuiDockContent="terminal"> <pre class="text-foreground-muted m-0 p-3 text-xs">$ nx build dock-managerBuilt @xui/dock-manager</pre> </ng-template> <ng-template xuiDockContent="problems"></ng-template> <ng-template xuiDockContent="outline"></ng-template> <ng-template xuiDockContent="search"> <div class="p-2"> <input xuiInput size="sm" class="w-full" type="search" placeholder="Search…" /> </div> </ng-template> </xui-dock-manager>Preserves content
<xui-dock-manager [(layout)]="layout" class="border-border h-[22rem] w-[46rem] rounded-lg border"> <ng-template xuiDockContent="form"> <div class="flex flex-col gap-2 p-3"> <input xuiInput size="sm" placeholder="Type something, then drag this pane" /> <textarea xuiTextarea size="sm" class="h-24" aria-label="Notes"></textarea> </div> </ng-template> <ng-template xuiDockContent="notes"></ng-template></xui-dock-manager>Restricted panes
<xui-dock-manager [(layout)]="layout" class="border-border h-[20rem] w-[52rem] rounded-lg border"> <ng-template xuiDockContent="fixed"></ng-template> <ng-template xuiDockContent="anchored"></ng-template> <ng-template xuiDockContent="free"></ng-template></xui-dock-manager>Right to left
<div dir="rtl"> <xui-dock-manager [(layout)]="layout" class="border-border h-[20rem] w-[46rem] rounded-lg border"> <ng-template xuiDockContent="explorer"></ng-template> <ng-template xuiDockContent="details"></ng-template> </xui-dock-manager></div>API
Declares the body of one content pane.
Inputs
xuiDockContentstringMatches the contentId of a content pane in the layout.Hosts the content view for a contentId at its own position.
Inputs
xuiDockContentOutletstringWhich pane's content view to mount here — the contentId of the matching xuiDockContent.Methods
host(): HTMLElement | nullRead lazily: the anchor is only parented once the outlet is in the DOM.An IDE-style docking layout: resizable split panes, tab groups, a document host, collapsible edge panels and floating windows, all driven by one serialisable {@link XuiDockManagerLayout} tree.
Inputs
classClassValueExtra classes, merged into the component's own rather than replacing them.''layoutXuiDockManagerLayoutThe layout tree. Two-way bindable, and edited **in place** — see the class docs for why, and use cloneDockLayout() for snapshots.Outputs
paneCloseXuiDockContentPaneA pane was closed and removed from the layout.activePaneChangeXuiDockContentPane | nullThe pane the user last interacted with, or null once it is gone.panePinnedChangeXuiDockPaneStateEventEmits the pane and its new pinned state whenever a pane is pinned or unpinned.paneMaximizedChangeXuiDockPaneStateEventEmits the pane and its new maximized state whenever a pane is maximized or restored.paneFloatingChangeXuiDockPaneStateEventEmits the pane and its new floating state whenever a pane is torn out of the layout or docked back into it.paneDragStartXuiDockPaneA pointer drag on a pane header or tab began.paneDragEndXuiDockPaneA pointer drag ended, whether or not it changed the layout.Methods
mountContent(contentId: string, target: XuiDockContentTarget): voidPut contentId's view inside target's host, building it on first use. A view is created once and moved, never rebuilt: a pane keeps its DOM — and so its scroll position, focus and form state — as it is dragged, docked, floated and tabbed. ⚠ A pane body therefore has to outlive the frame it is rendered in, and two facts about Angular pull against each other over how: - Hydration reads a view's position from the container it was **created** in. Creating a body somewhere convenient and then appendChilding its nodes into the pane leaves the annotation describing one place and the markup another, and the browser walks from the annotation: locateOrCreateAnchorNode → locateDehydratedViewsInContainer → validateSiblingNodeExists, which finds nothing. That was the old defect, and ngSkipHydration did not cover it, because the container Angular could not locate belonged to the *consumer's* view rather than this component's. - A view left **inside** a container dies with it. destroyViewTree descends into an LContainer's views and cleans them up before it reaches the host view's own destroy hooks, so an outlet cannot rescue its body on the way out: by the time the outlet's cleanup runs the view is already destroyed. Maximizing, tabbing, floating and unpinning each destroy an outlet. So a body is *born* in the outlet's own container — which is what makes the server's annotations agree with its own markup, and the browser's first render agree with both — and is {@link adopt}ed into this component's holding bay once that render is on screen, after which it is moved by hand as before. Hydration only ever looks at the first render, so the hand-off costs it nothing. dock-manager.hydration.spec.ts holds one line and the "moving a pane keeps its view instance" cases in dock-manager.spec.ts hold the other.releaseContent(contentId: string, target: XuiDockContentTarget): voidclosePane(pane: XuiDockContentPane): voidRemove pane from the layout. Respects allowClose.closeWindow(window: XuiDockSplitPane): voidClose every pane in a floating window.unpinPane(pane: XuiDockContentPane): voidCollapse pane to a tab on the nearest edge, keeping its place in the tree.pinPane(pane: XuiDockContentPane): voidRestore pane to the position it was collapsed from.togglePinned(pane: XuiDockContentPane): voidmaximizePane(pane: XuiDockContentPane): voidBlow pane up to fill the whole dock manager, hiding everything else.restorePane(pane: XuiDockContentPane): voidtoggleMaximized(pane: XuiDockContentPane): voidfloatPane(pane: XuiDockPane, location?: XuiDockPoint, size?: { width: number; height: number }): voidMove pane into a floating window of its own.dockPane(pane: XuiDockPane, target: XuiDockPane, position: XuiDockPosition): booleanDock pane at position relative to target.selectPane(pane: XuiDockContentPane): voidMake pane the active one, selecting its tab and opening its fly-out.