Dockview
Introduction
Dockview is an abstraction built on top of Gridviews where each view is a container of many tabbed panels.
You can access the panels associated group through the
panel.group
variable. The group will always be defined and will change if a panel is moved into another group.
DockviewReact Component
You can create a Dockview through the use of the DockviewReact
component.
All of these are React props available through the DockviewReact
component.
className |
|
---|---|
components |
|
defaultTabComponent |
|
disableAutoResizing |
|
disableFloatingGroups |
|
floatingGroupBounds |
|
hideBorders |
|
leftHeaderActionsComponent |
|
onDidDrop |
|
onReady |
|
prefixHeaderActionsComponent |
|
rightHeaderActionsComponent |
|
showDndOverlay |
|
singleTabMode |
|
tabComponents |
|
watermarkComponent |
|
Dockview API
The Dockview API is exposed both at the onReady
event and on each panel through props.containerApi
.
Through this API you can control general features of the component and access all added panels.
const MyComponent = (props: IDockviewPanelProps<{ title: string }>) => {
// props.containerApi...
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
const onReady = (event: DockviewReadyEvent) => {
// event.api...
};
All of these methods are available through the api
property of DockviewComponent
and the containerApi
property of IDockviewPanel
.
activeGroup | Active group object.
|
---|---|
activePanel | Active panel object.
|
groups | All group objects.
|
height | Height of the component.
|
id | The unique identifier for this instance. Used to manage scope of Drag'n'Drop events.
|
maximumHeight | Maximum height of the component.
|
maximumWidth | Maximum width of the component.
|
minimumHeight | Minimum height of the component.
|
minimumWidth | Minimum width of the component.
|
onDidActiveGroupChange | Invoked when the active group changes. May be undefined if no group is active.
|
onDidActivePanelChange | Invoked when the active panel changes. May be undefined if no panel is active.
|
onDidAddGroup | Invoked when a group is added. May be called multiple times when moving groups.
|
onDidAddPanel | Invoked when a panel is added. May be called multiple times when moving panels.
|
onDidDrop | Invoked when a Drag'n'Drop event occurs that the component was unable to handle. Exposed for custom Drag'n'Drop functionality.
|
onDidLayoutChange | Invoked when any layout change occures, an aggregation of many events.
|
onDidLayoutFromJSON | Invoked after a layout is deserialzied using the fromJSON method.
|
onDidRemoveGroup | Invoked when a group is removed. May be called multiple times when moving groups.
|
onDidRemovePanel | Invoked when a panel is removed. May be called multiple times when moving panels.
|
onWillDragGroup | Invoked before a group is dragged. Exposed for custom Drag'n'Drop functionality.
|
onWillDragPanel | Invoked before a panel is dragged. Exposed for custom Drag'n'Drop functionality.
|
panels | All panel objects.
|
size | Total number of groups.
|
totalPanels | Total number of panels.
|
width | Width of the component.
|
addFloatingGroup | Add a floating group
|
addGroup | Add a group and return the created object.
|
addPanel | Add a panel and return the created object.
|
clear | Reset the component back to an empty and default state.
|
closeAllGroups | Close all groups and panels.
|
focus | Focus the component. Will try to focus an active panel if one exists.
|
fromJSON | Create a component from a serialized object.
|
getGroup | Get a group object given a string id. May return undefined.
|
getPanel | Get a panel object given a string id. May return undefined .
|
layout | Force resize the component to an exact width and height. Read about auto-resizing before using.
|
moveToNext | Move the focus progmatically to the next panel or group.
|
moveToPrevious | Move the focus progmatically to the previous panel or group.
|
removeGroup | Remove a group and any panels within the group.
|
removePanel | Remove a panel given the panel object.
|
toJSON | Create a serialized object of the current component.
|
Dockview Panel API
const MyComponent = (props: IDockviewPanelProps<{ title: string }>) => {
// props.api...
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
All of these are methods are available through the api
property of IDockviewPanel
.
group |
|
---|---|
height | The panel height in pixels
|
id | The id of the panel that would have been assigned when the panel was created
|
isActive | Whether the panel is the actively selected panel
|
isFocused | Whether the panel holds the current focus
|
isGroupActive |
|
isVisible | Whether the panel is visible
|
onDidActiveChange |
|
onDidActiveGroupChange |
|
onDidDimensionsChange |
|
onDidFocusChange |
|
onDidGroupChange |
|
onDidVisibilityChange |
|
title |
|
width | The panel width in pixels
|
close |
|
moveTo |
|
setActive |
|
setSize |
|
setTitle |
|
updateParameters |
|
Theme
As well as importing the dockview
stylesheet you must provide a class-based theme somewhere in your application. For example.
// Providing a theme directly through the DockviewReact component props
<DockviewReact className="dockview-theme-dark" />
// Providing a theme somewhere in the DOM tree
<div className="dockview-theme-dark">
<div>
{/**... */}
<DockviewReact />
</div>
</div>
You can find more details on theming here.
Layout Persistance
Layouts are loaded and saved via to fromJSON
and toJSON
methods on the Dockview api.
The api also exposes an event onDidLayoutChange
you can listen on to determine when the layout has changed.
Below are some snippets showing how you might load from and save to localStorage.
React.useEffect(() => {
if (!api) {
return;
}
const disposable = api.onDidLayoutChange(() => {
const layout = api.toJSON();
localStorage.setItem(
'dockview_persistance_layout',
JSON.stringify(layout)
);
});
return () => {
disposable.dispose();
};
}, [api]);
const onReady = (event: DockviewReadyEvent) => {
const layoutString = localStorage.getItem('dockview_persistance_layout');
let success = false;
if (layoutString) {
try {
const layout = JSON.parse(layoutString);
event.api.fromJSON(layout);
success = true;
} catch (err) {
//
}
}
if (!success) {
// do something if there is no layout or there was a loading error
}
};
Here is an example using the above code loading from and saving to localStorage. If you refresh the page you should notice your layout is loaded as you left it.
Resizing
Panel Resizing
Each Dockview contains of a number of groups and each group has a number of panels. Logically a user may want to resize a panel, but this translates to resizing the group which contains that panel.
You can set the size of a panel using props.api.setSize(...)
.
You can also set the size of the group associated with the panel using props.api.group.api.setSize(...)
although this isn't recommended
due to the clunky syntax.
// it's mandatory to provide either a height or a width, providing both is optional
props.api.setSize({
height: 100,
width: 200,
});
// you could also resize the panels group, although not recommended it achieved the same result
props.api.group.api.setSize({
height: 100,
width: 200,
});
You can see an example invoking both approaches below.
Container Resizing
The component will automatically resize to it's container.
Watermark
When the dockview is empty you may want to display some fallback content, this is refered to as the watermark
.
By default there the watermark has no content but you can provide as a prop to DockviewReact
a watermarkComponent
which will be rendered when there are no panels or groups.
Drag And Drop
Built-in behaviours
Dockview supports a wide variety of built-in Drag and Drop possibilities. Below are some examples of the operations you can perform.
Drag a tab onto another tab to place it inbetween existing tabs.
Drag a tab to the right of the last tab to place it after the existing tabs.
Drag a group onto an existing group to merge the two groups.
Drag into the left/right/top/bottom target zone of a panel to create a new group in the selected direction.
Drag into the center of a panel to add to that group.
Drag to the edge of the dockview component to create a new group on the selected edge.
Extended behaviours
For interaction with the Drag events directly the component exposes some method to help determine whether external drag events should be interacted with or not.
/**
* called when an ondrop event which does not originate from the dockview libray and
* passes the showDndOverlay condition occurs
**/
const onDidDrop = (event: DockviewDropEvent) => {
const { group } = event;
event.api.addPanel({
id: 'test',
component: 'default',
position: {
referencePanel: group.activePanel.id,
direction: 'within',
},
});
};
/**
* called for drag over events which do not originate from the dockview library
* allowing the developer to decide where the overlay should be shown for a
* particular drag event
**/
const showDndOverlay = (event: DockviewDndOverlayEvent) => {
return true;
};
return (
<DockviewReact
components={components}
onReady={onReady}
className="dockview-theme-abyss"
onDidDrop={onDidDrop}
showDndOverlay={showDndOverlay}
/>
);
Intercepting Drag Events
You can intercept drag events to attach your own metadata using the onWillDragPanel
and onWillDragGroup
api methods.
Third Party Dnd Libraries
This shows a simple example of a third-party library used inside a panel that relies on drag
and drop functionalities. This examples serves to show that dockview
doesn't interfer with
any drag and drop logic for other controls.
Floating Groups
Dockview has built-in support for floating groups. Each floating container can contain a single group with many panels and you can have as many floating containers as needed. You cannot dock multiple groups together in the same floating container.
Floating groups can be interacted with whilst holding the shift
key activating the event.shiftKey
boolean property on KeyboardEvent
events.
Float an existing tab by holding
shift
whilst interacting with the tab
Move a floating tab by holding
shift
whilst moving the cursor or dragging the empty header space
Move an entire floating group by holding
shift
whilst dragging the empty header space
Floating groups can be programatically added through the dockview api
method api.addFloatingGroup(...)
and you can check whether
a group is floating via the group.api.isFloating
property. See examples for full code.
You can control the bounding box of floating groups through the optional floatingGroupBounds
options:
boundedWithinViewport
will force the entire floating group to be bounded within the docks viewport.{minimumHeightWithinViewport?: number, minimumWidthWithinViewport?: number}
sets the respective dimension minimums that must appears within the docks viewport- If no options are provided the defaults of
100px
minimum height and width within the viewport are set.
Panels
Add Panel
Using the dockview API you can access the addPanel
method which returns an instance of the created panel.
The minimum method signature is:
const panel = api.addPanel({
id: 'my_unique_panel_id',
component: 'my_component',
});
where id
is the unique id of the panel and component
is the implenentation which
will be used to render the panel. You will have registered this using the components
prop of the DockviewReactComponent
component.
You can optionally provide a tabComponent
parameters to the addPanel
method which will render the tab using a custom renderer.
You will have registered this using the tabComponents
prop of the DockviewReactComponent
component.
const panel = api.addPanel({
id: 'my_unique_panel_id',
component: 'my_component',
tabComponent: 'my_tab_component',
});
You can pass properties to the panel using the params
key.
You can update these properties through the panels api
object and its updateParameters
method.
const panel = api.addPanel({
id: 'my_unique_panel_id',
component: 'my_component',
params: {
myCustomKey: 'my_custom_value',
},
});
panel.api.updateParameters({
myCustomKey: 'my_custom_value',
myOtherCustomKey: 'my_other_custom_key',
});
Note
updateParameters
does not accept partial parameter updates, you should call it with the entire set of parameters you want the panel to receive.
Finally addPanel
accepts a position
object which tells dockview where to place the panel.
- This object optionally accepts either a
referencePanel
orreferenceGroup
which can be the associated id as a string or the panel/group object reference. - This object accepts a
direction
property which dictates where, relative to the provided reference the new panel will be placed.
If neither a
referencePanel
orreferenceGroup
is provided then thedirection
will be treated as absolute.
If no
direction
is provided the library will place the new panel in a pre-determined position.
const panel = api.addPanel({
id: 'panel_1',
component: 'default',
});
const panel2 = api.addPanel({
id: 'panel_2',
component: 'default',
position: {
referencePanel: panel1,
direction: 'right',
},
});
To add a floating panel you should include the floating
variable which can be either a boolean
or an object defining it's bounds.
These bounds are relative to the dockview component.
const panel1 = api.addPanel({
id: 'panel_2',
component: 'default',
floating: true,
});
const panel2 = api.addPanel({
id: 'panel_2',
component: 'default',
floating: { x: 10, y: 10, width: 300, height: 300 },
});
Update Panel
You can programatically update the params
passed through to the panel through the panal api using api.updateParameters
.
const panel = api.addPanel({
id: 'panel_1',
component: 'default',
params: {
keyA: 'valueA',
},
});
// ...
panel.api.updateParameters({
keyB: 'valueB',
});
// ...
panel.api.updateParameters({
keyA: 'anotherValueA',
});
To delete a parameter you should pass a value of undefined
for the key.
panel.api.updateParameters({
keyA: undefined, // this will delete 'keyA'.
});
Move panel
You can programatically move a panel using the panel api
.
panel.api.moveTo({ group, position, index });
An equivalent method for moving groups is avaliable on the group api
.
const group = panel.api.group;
group.api.moveTo({ group, position });
Remove panel
You can programatically remove a panel using the panel api
.
panel.api.close();
Given a reference to the panel you can also use the component api
to remove it.
const panel = api.getPanel('myPanel');
api.removePanel(panel);
Panel Rendering
By default DockviewReact
only adds to the DOM those panels that are visible,
if a panel is not the active tab and not shown the contents of the hidden panel will be removed from the DOM.
However the React Components associated with each panel are only created once and will always exist for as long as the panel exists, hidden or not.
For example this means that any hooks in those components will run whether the panel is visible or not which may lead to excessive background work depending on the panels implementation.
This is the default behaviour to ensure the greatest flexibility for the user but through the panels props.api
you can listen to the visiblity state of the panel
and write additional logic to optimize your application.
For example if you wanted to unmount the React Components when the panel is not visible you could create a Higher-Order-Component that listens to the panels visiblity state and only renders the panel when visible.
import { IDockviewPanelProps } from 'dockview';
import * as React from 'react';
function RenderWhenVisible(
component: React.FunctionComponent<IDockviewPanelProps>
) {
const HigherOrderComponent = (props: IDockviewPanelProps) => {
const [visible, setVisible] = React.useState<boolean>(
props.api.isVisible
);
React.useEffect(() => {
const disposable = props.api.onDidVisibilityChange((event) =>
setVisible(event.isVisible)
);
return () => {
disposable.dispose();
};
}, [props.api]);
if (!visible) {
return null;
}
return React.createElement(component, props);
};
return HigherOrderComponent;
}
const components = { default: RenderWhenVisible(MyComponent) };
Toggling the checkbox you can see that when you only render those panels which are visible the underling React component is destroyed when it becomes hidden and re-created when it becomes visible.
Headers
Custom Tab Headers
You can provide custom renderers for your tab headers for maximum customization.
A default implementation of DockviewDefaultTab
is provided should you only wish to attach minor
changes and events that do not alter the default behaviour, for example to add a custom context menu event
handler.
The DockviewDefaulTab
component accepts a hideClose
prop if you wish only to hide the close button.
import { IDockviewPanelHeaderProps, DockviewDefaultTab } from 'dockview';
const MyCustomheader = (props: IDockviewPanelHeaderProps) => {
const onContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
alert('context menu');
};
return <DockviewDefaultTab onContextMenu={onContextMenu} {...props} />;
};
You are also free to define a custom renderer entirely from scratch and not make use of the DockviewDefaultTab
component.
To use a custom renderer you can must register a collection of tab components.
const tabComponents = {
myCustomHeader: MyCustomHeader,
};
return <DockviewReact tabComponents={tabComponents} ... />;
api.addPanel({
id: 'panel_1',
component: 'default',
tabComponent: 'myCustomHeader', // <-- your registered renderers
title: 'Panel 1',
});
You can also override the default tab renderer which will be used when no tabComponent
is provided to the addPanel
function.
<DockviewReact defaultTabComponent={MyCustomHeader} ... />;
As a simple example the below attaches a custom event handler for the context menu on all tabs as a default tab renderer
The below example uses a custom tab renderer to reigster a popover when the user right clicked on a tab.
This still makes use of the DockviewDefaultTab
since it's only a minor change.
Default Tab Title
If you are using the default tab renderer you can set the title of a tab when creating it
api.addPanel({
id: 'panel_1',
component: 'my_component',
title: 'my_custom_title', // <-- special param for title
});
You can update the title through the panel api which can be accessed via props.api
if you are inside the panel
component or via api.getPanel('panel1').api
if you are accessing from outside of the panel component.
api.setTitle('my_new_custom_title');
Note this only works when using the default tab implementation.
Custom Tab Title
If you are using a custom tab implementation you should pass variables through as a parameter and render them through your tab components implementation.
api.addPanel({
id: 'panel_2',
component: 'my_component',
tabComponent: 'my_tab',
params: {
myTitle: 'Window 2', // <-- passing a variable to use as a title
},
});
const tabComponents = {
default: (props: IDockviewPanelHeaderProps<{ myTitle: string }>) => {
const title = props.params.myTitle; // <-- accessing my custom varaible
return <div>{/** tab implementation as chosen by developer */}</div>;
},
};
Hidden Headers
You may wish to hide the header section of a group. This can achieved through the hidden
variable on panel.group.header
.
panel.group.header.hidden = true;
Full width tabs
DockviewReactComponent
accepts the prop singleTabMode
. If set singleTabMode=fullwidth
then when there is only one tab in a group this tab will expand
to the entire width of the group. For example:
This can be conmbined with Locked Groups to create an application that feels more like a Window Manager rather than a collection of groups and tabs.
<DockviewReactComponent singleTabMode="fullwidth" {...otherProps} />
Tab Height
Tab height can be controlled through CSS.
Groups
Locked group
Locking a group will disable all drop events for this group ensuring no additional panels can be added to the group through drop events. You can still add groups to a locked panel programatically using the API though.
panel.group.locked = true;
// Or
panel.group.locked = 'no-drop-target';
Use true
to keep drop zones top, right, bottom, left for the group. Use no-drop-target
to disable all drop zones. For you to get a
better understanding of what this means, try and drag the panels in the example below to the locked groups.
Group Controls Panel
DockviewReact
accepts leftHeaderActionsComponent
, rightHeaderActionsComponent
and prefixHeaderActionsComponent
which expect a React component with props IDockviewHeaderActionsProps
.
These controls are rendered to left and right side of the space to the right of the tabs in the header bar as well as before the first tab in the case of the prefix header prop.
const Component: React.FunctionComponent<IDockviewHeaderActionsProps> = () => {
return <div>{'...'}</div>;
};
return <DockviewReact {...props} leftHeaderActionsComponent={Component} rightHeaderActionsComponent={...} />;
As a simple example the below uses the groupControlComponent
to render a small control that indicates whether the group
is active and which panel is active in that group.
const RightHeaderActionsComponent = (props: IDockviewHeaderActionsProps) => {
const isGroupActive = props.isGroupActive;
const activePanel = props.activePanel;
return (
<div className="dockview-groupcontrol-demo">
<span
className="dockview-groupcontrol-demo-group-active"
style={{
background: isGroupActive ? 'green' : 'red',
}}
>
{isGroupActive ? 'Group Active' : 'Group Inactive'}
</span>
<span className="dockview-groupcontrol-demo-active-panel">{`activePanel: ${
activePanel?.id || 'null'
}`}</span>
</div>
);
};
Constraints
You may wish to specify a minimum or maximum height or width for a group which can be done through the group api.
api.group.api.setConstraints(...)
Constraints are currently only supported for groups and not individual panels. If you specific a constraint on a group and move a panel within that group to another group it will no longer be subject to those constraints since those constraints were on the group and not on the individual panel.
iFrames
iFrames required special attention because of a particular behaviour in how iFrames render:
Re-parenting an iFrame will reload the contents of the iFrame or the rephrase this, moving an iFrame within the DOM will cause a reload of its contents.
You can find many examples of discussions on this. Two reputable forums for example are linked here and here.
The problem with iFrames and dockview
is that when you hide or move a panel that panels DOM element may be moved within the DOM or removed from the DOM completely.
If your panel contains an iFrame then that iFrame will reload after being re-positioned within the DOM tree and all state in that iFrame will most likely be lost.
dockview
does not provide a built-in solution to this because it's too specific of a problem to include in the library.
However the below example does show an implementation of a higher-order component HoistedDockviewPanel
that you could use to work around this problems and make iFrames behave in dockview
.
What the higher-order component is doing is to hoist the panels contents into a DOM element that is always present and then position: absolute
that element to match the dimensions of it's linked panel.
The visibility of these hoisted elements is then controlled through some exposed api methods to hide elements that shouldn't be currently shown.
You should open this example in CodeSandbox using the provided link to understand the code and make use of this implemention if required.
Events
A simple example showing events fired by `dockviewz that can be interacted with.
Keyboard Navigation
Keyboard shortcuts
Nested Dockviews
You can safely create multiple dockview instances within one page and nest dockviews within other dockviews.
If you wish to interact with the drop event from one dockview instance in another dockview instance you can implement the showDndOverlay
and onDidDrop
props on DockviewReact
.