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.
import { DockviewReact } from 'dockview';
Property | Type | Optional | Default | Description |
---|---|---|---|---|
onReady | (event: SplitviewReadyEvent) => void | No | ||
components | object | No | ||
tabComponents | object | Yes | ||
watermarkComponent | object | Yes | ||
hideBorders | boolean | Yes | false | |
className | string | Yes | '' | |
disableAutoResizing | boolean | Yes | false | See Auto Resizing |
onDidDrop | Event | Yes | false | |
showDndOverlay | Event | Yes | false | |
defaultTabComponent | object | Yes | ||
groupControlComponent | object | Yes | ||
singleTabMode | 'fullwidth' | 'default' | Yes | 'default' |
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...
};
Property | Type | Description |
---|---|---|
height | number | Component pixel height |
width | number | Component pixel width |
minimumHeight | number | |
maximumHeight | number | |
maximumWidth | number | |
maximumWidth | number | |
length | number | Number of panels |
size | number | Number of Groups |
panels | IDockviewPanel[] | |
groups | GroupPanel[] | |
activePanel | IDockviewPanel \| undefined | |
activeGroup | IDockviewPanel \| undefined | |
onDidLayoutChange | Event<void> | |
onDidLayoutFromJSON | Event<void> | |
onDidAddGroup | Event<GroupPanel> | |
onDidRemoveGroup | Event<GroupPanel> | |
onDidActiveGroupChange | Event<GroupPanel \| undefined> | |
onDidAddPanel | Event<IDockviewPanel> | |
onDidRemovePanel | Event<IDockviewPanel> | |
onDidActivePanelChange | Event<IDockviewPanel \| undefined> | |
onDidDrop | Event<DockviewDropEvent | |
addPanel | addPanel(options: AddPanelOptions): IDockviewPanel | |
getPanel | (id: string) \| IDockviewPanel \| undefined | |
addGroup | (options? AddGroupOptions): void | |
closeAllGroups | (): void | |
removeGroup | (group: GroupPanel): void | |
getGroup | (id: string): GroupPanel \| undefined | |
updateOptions | (options:SplitviewComponentUpdateOptions): void | |
focus | (): void | |
layout | (width: number, height:number): void | Auto Resizing |
fromJSON | (data: SerializedDockview): void | Serialization |
toJSON | (): SerializedDockview | Serialization |
clear | (): void | Clears the current layout |
Dockview Panel API
const MyComponent = (props: IDockviewPanelProps<{ title: string }>) => {
// props.api...
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
Property | Type | Description |
---|---|---|
id | string | Panel id |
isFocused | boolean | Is panel focused |
isActive | boolean | Is panel active |
width | number | Panel width |
height | number | Panel height |
onDidDimensionsChange | Event<PanelDimensionChangeEvent> | |
onDidFocusChange | Event<FocusEvent> | |
onDidVisibilityChange | Event<VisibilityEvent> | |
onDidActiveChange | Event<ActiveEvent> | |
setActive | (): void | |
onDidConstraintsChange | onDidConstraintsChange: Event<PanelConstraintChangeEvent> | |
setConstraints | (value: PanelConstraintChangeEvent2): void; | |
setSize | (event: SizeEvent): void | |
group | `GroupPanel | undefined` |
isGroupActive | boolean | |
title | string | |
suppressClosable | boolean | |
close | (): void | |
setTitle | (title: string): void |
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}
/>
);
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.
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
then the provideddirection
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',
},
});
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'.
});
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.
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;
Group Controls Panel
DockviewReact
accepts a prop groupControlComponent
which expects a React component whos props are IDockviewGroupControlProps
.
This control will be rendered inside the header bar on the right hand side for each group of tabs.
const Component: React.FunctionComponent<IDockviewGroupControlProps> = () => {
return <div>{'...'}</div>;
};
return <DockviewReact {...props} groupControlComponent={Component} />;
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 GroupControlComponent = (props: IDockviewGroupControlProps) => {
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.
Advanced Examples
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
.
Window-like mananger with tabs
Vanilla JS
Note: This section is experimental and support for Vanilla JS is a work in progress.
The dockview
package contains ReactJS
wrappers for the core library.
The core library is published as an independant package under the name dockview-core
which you can install standalone.
When using
dockview
there is no need to also installdockview-core
.dockview-core
is a dependency ofdockview
and automatically installed during the installation process ofdockview
vianpm install dockview
.