FlexLayout

A multi-tab layout manager

README

FlexLayout

GitHub
npm npm

FlexLayout is a layout manager that arranges React components in multiple tab sets, tabs can be resized and moved.

FlexLayout Demo Screenshot


Try it now using JSFiddle



FlexLayout's only dependencies are React and uuid.

Features:
splitters
tabs
tab dragging and ordering
tabset dragging (move all the tabs in a tabset in one operation)
dock to tabset or edge of frame
maximize tabset (double click tabset header or use icon)
tab overflow (show menu when tabs overflow, scroll tabs using mouse wheel)
  border tabsets
  popout tabs into new browser windows
submodels, allow layouts inside layouts
tab renaming (double click tab text to rename)
theming - light, underline, gray and dark
touch events - works on mobile devices (iPad, Android)
  add tabs using drag, indirect drag, add to active tabset, add to tabset by id
  preferred pixel size tabsets (try to keep their size when window resizes)
  headed tabsets
tab and tabset attributes: enableHeader, enableTabStrip, enableDock, enableDrop...
customizable tabs and tabset header rendering
typescript type declarations included

Installation


FlexLayout is in the npm repository. Simply install React and FlexLayout from npm:

  1. ```
  2. npm install react
  3. npm install react-dom
  4. npm install flexlayout-react
  5. ```

Import React and FlexLayout in your modules:

  1. ```
  2. import * as React from "react";
  3. import { createRoot } from "react-dom/client";
  4. import * as FlexLayout from "flexlayout-react";
  5. ```

Include the light, underline, gray or dark style in your html:

  1. ```
  2. <link rel="stylesheet" href="node_modules/flexlayout-react/style/light.css" />
  3. ```

Usage


The `` component renders the tabsets and splitters, it takes the following props:


Required props:



PropDescription
--------------------------------
modelthe
factorya

Additional optional props

The model is tree of Node objects that define the structure of the layout.

The factory is a function that takes a Node object and returns a React component that should be hosted by a tab in the layout.

The model can be created using the Model.fromJson(jsonObject) static method, and can be saved using the model.toJson() method.

  1. ``` js
  2. this.state = {model: FlexLayout.Model.fromJson(json)};

  3. render() {
  4. <FlexLayout.Layout model={this.state.model} factory={factory}/>
  5. }
  6. ```

Example Configuration:


  1. ``` js
  2. var json = {
  3.     global: {},
  4.     borders: [],
  5.     layout: {
  6.         type: "row",
  7.         weight: 100,
  8.         children: [
  9.             {
  10.                 type: "tabset",
  11.                 weight: 50,
  12.                 children: [
  13.                     {
  14.                         type: "tab",
  15.                         name: "One",
  16.                         component: "button",
  17.                     }
  18.                 ]
  19.             },
  20.             {
  21.                 type: "tabset",
  22.                 weight: 50,
  23.                 children: [
  24.                     {
  25.                         type: "tab",
  26.                         name: "Two",
  27.                         component: "button",
  28.                     }
  29.                 ]
  30.             }
  31.         ]
  32.     }
  33. };
  34. ```

Example Code


  1. ```
  2. import * as React from "react";
  3. import { createRoot } from "react-dom/client";
  4. import * as FlexLayout from "flexlayout-react";

  5. class Main extends React.Component {

  6.     constructor(props) {
  7.         super(props);
  8.         this.state = {model: FlexLayout.Model.fromJson(json)};
  9.     }

  10.     factory = (node) => {
  11.         var component = node.getComponent();
  12.         if (component === "button") {
  13.             return <button>{node.getName()}</button>;
  14.         }
  15.     }

  16.     render() {
  17.         return (
  18.             <FlexLayout.Layout model={this.state.model} factory={this.factory}/>
  19.         )
  20.     }
  21. }

  22. const root = createRoot(document.getElementById("container"));
  23. root.render(<Main/>);
  24. ```  

The above code would render two tabsets horizontally each containing a single tab that hosts a button component. The tabs could be moved and resized by dragging and dropping. Additional grids could be added to the layout by sending actions to the model.

Try it now using JSFiddle

A simple Typescript example can be found here:

https://github.com/nealus/FlexLayout_cra_example

The model is built up using 4 types of 'node':

row - rows contains a list of tabsets and child rows, the top level 'row' will render horizontally (unless the global attribute rootOrientationVertical is set)
, child 'rows' will render in the opposite orientation to their parent.

tabset - tabsets contain a list of tabs and the index of the selected tab

tab - tabs specify the name of the component that they should host (that will be loaded via the factory) and the text of the actual tab.

border - borders contain a list of tabs and the index of the selected tab, they can only be used in the borders
top level element.

The main layout is defined with rows within rows that contain tabsets that themselves contain tabs.

The model json contains 3 top level elements:

global - where global options are defined
layout - where the main row/tabset/tabs layout hierarchy is defined
borders - (optional) where up to 4 borders are defined ("top", "bottom", "left", "right").

Weights on rows and tabsets specify the relative weight of these nodes within the parent row, the actual values do not matter just their relative values (ie two tabsets of weights 30,70 would render the same if they had weights of 3,7).

NOTE: the easiest way to create your initial layout JSON is to use the demo app, modify one of the
existing layouts by dragging/dropping and adding nodes then press the 'Show Layout JSON in console' button to print the JSON to the browser developer console.


example borders section:
  1. ```
  2.     borders: [
  3.          {
  4.             type: "border",
  5.             location: "left",
  6.             children: [
  7.                 {
  8.                     type: "tab",
  9.                     enableClose: false,
  10.                     name: "Navigation",
  11.                     component: "grid",
  12.                 }
  13.             ]
  14.         },
  15.         {
  16.             type: "border",
  17.             location: "right",
  18.             children: [
  19.                 {
  20.                     type: "tab",
  21.                     enableClose: false,
  22.                     name: "Options",
  23.                     component: "grid",
  24.                 }
  25.             ]
  26.         },
  27.         {
  28.             type: "border",
  29.             location: "bottom",
  30.             children: [
  31.                 {
  32.                     type: "tab",
  33.                     enableClose: false,
  34.                     name: "Activity Blotter",
  35.                     component: "grid",
  36.                 },
  37.                 {
  38.                     type: "tab",
  39.                     enableClose: false,
  40.                     name: "Execution Blotter",
  41.                     component: "grid",
  42.                 }
  43.             ]
  44.         }
  45.     ]
  46. ```

To control where nodes can be dropped you can add a callback function to the model:

  1. ```
  2. model.setOnAllowDrop(this.allowDrop);
  3. ```

example:
  1. ```
  2.     allowDrop(dragNode, dropInfo) {
  3.         let dropNode = dropInfo.node;

  4.         // prevent non-border tabs dropping into borders
  5.         if (dropNode.getType() == "border" && (dragNode.getParent() == null || dragNode.getParent().getType() != "border"))
  6.             return false;

  7.         // prevent border tabs dropping into main layout
  8.         if (dropNode.getType() != "border" && (dragNode.getParent() != null && dragNode.getParent().getType() == "border"))
  9.             return false;

  10.         return true;
  11.     }
  12. ```

By changing global or node attributes you can change the layout appearance and functionality, for example:

Setting tabSetEnableTabStrip:false in the global options would change the layout into a multi-splitter (without
tabs or drag and drop).

  1. ```
  2. global: {tabSetEnableTabStrip:false},
  3. ```

Floating Tabs (Popouts)


Tabs can be rendered into external browser windows (for use in multi-monitor setups)
by configuring them with the enableFloat attribute. When this attribute is present
an additional icon is shown in the tab header bar allowing the tab to be popped out
into an external window.

For popouts to work there needs to be an additional html page 'popout.html' hosted
at the same location as the main page (copy the one from examples/demo). The popout.html is the host page for the
popped out tab, the styles from the main page will be copied into it at runtime.

Because popouts are rendering into a different document to the main layout any code in the popped out
tab that uses the global document or window objects will not work correctly (for example custom popup menus),
they need to instead use the document/window of the popout. To get the document/window of the popout use the
following method on one of the elements rendered in the popout (for example a ref or target in an event handler):

  1. ```
  2.     const currentDocument = this.selfRef.current.ownerDocument;
  3.     const currentWindow = currentDocument.defaultView!;
  4. ```

In the above code selfRef is a React ref to the toplevel element in the tab being rendered.

Note: some libraries support popout windows by allowing you to specify the document to use,
for example see the getDocument() callback in agGrid at https://www.ag-grid.com/javascript-grid-callbacks/

Optional Props



PropDescription
--------------------------------
fontthe
iconsobject
onActionfunction
onRenderTabfunction
onRenderTabSetfunction
onModelChangefunction
onExternalDragfunction
classNameMapperfunction
i18nMapperfunction
supportsPopoutif
popoutURLURL
realtimeResizeboolean
onTabDragfunction
onRenderDragRectcallback
onRenderFloatingTabPlaceholdercallback
onContextMenucallback
onAuxMouseClickcallback
onShowOverflowMenucallback
onTabSetPlaceHoldercallback
iconFactorya
titleFactorya


Global Config attributes


Attributes allowed in the 'global' element


AttributeDefaultDescription
-------------|:-------------:|
splitterSize8width
splitterExtra0additional
legacyOverflowMenufalseuse
enableEdgeDocktrue|
tabEnableClosetrueallow
tabCloseType1see
tabEnableDragtrueallow
tabEnableRenametrueallow
tabEnableFloatfalseenable
tabClassNamenull|
tabIconnull|
tabEnableRenderOnDemandtruewhether
tabDragSpeed0.3CSS
tabBorderWidth-1width
tabBorderHeight-1height
tabSetEnableDeleteWhenEmptytrue|
tabSetEnableDroptrueallow
tabSetEnableDragtrueallow
tabSetEnableDividetrueallow
tabSetEnableMaximizetrueallow
tabSetEnableClosefalseallow
tabSetAutoSelectTabtruewhether
tabSetClassNameTabStripnullheight
tabSetClassNameHeadernull|
tabSetEnableTabStriptrueenable
tabSetHeaderHeight0height
tabSetTabStripHeight0height
borderBarSize0size
borderEnableAutoHidefalsehide
borderEnableDroptrueallow
borderAutoSelectTabWhenOpentruewhether
borderAutoSelectTabWhenClosedfalsewhether
borderClassNamenull|
borderSize200initial
borderMinSize0minimum
tabSetMinHeight0minimum
tabSetMinWidth0minimum
tabSetTabLocationtopshow
rootOrientationVerticalfalsethe


Row Attributes


Attributes allowed in nodes of type 'row'.

AttributeDefaultDescription
-------------|:-------------:|
typerow|
weight100|
widthnullpreferred
heightnullpreferred
children*required*a

Tab Attributes


Attributes allowed in nodes of type 'tab'.

Inherited defaults will take their value from the associated global attributes (see above).


AttributeDefaultDescription
-------------|:-------------:|
typetab|
name*required*name
altName*optional*if
component*required*string
confignulla
idauto|
helpText*optional*An
enableClose*inherited*allow
closeType*inherited*see
enableDrag*inherited*allow
enableRename*inherited*allow
enableFloat*inherited*enable
floatingfalse|
className*inherited*|
icon*inherited*|
enableRenderOnDemand*inherited*whether
borderWidth*inherited*width
borderHeight*inherited*height

Tab nodes have a getExtraData() method that initially returns an empty object, this is the place to
add extra data to a tab node that will not be saved.


TabSet Attributes


Attributes allowed in nodes of type 'tabset'.

Inherited defaults will take their value from the associated global attributes (see above).

Note: tabsets can be dynamically created as tabs are moved and deleted when all their tabs are removed (unless enableDeleteWhenEmpty is false).

AttributeDefaultDescription
-------------|:-------------:|
typetabset|
weight100relative
widthnullpreferred
heightnullpreferred
namenullnamed
confignulla
selected0index
maximizedfalsewhether
enableClosefalseallow
idauto|
children*required*a
enableDeleteWhenEmpty*inherited*|
enableDrop*inherited*allow
enableDrag*inherited*allow
enableDivide*inherited*allow
enableMaximize*inherited*allow
autoSelectTab*inherited*whether
classNameTabStrip*inherited*|
classNameHeader*inherited*|
enableTabStrip*inherited*enable
headerHeight*inherited*|
tabStripHeight*inherited*height
tabLocation*inherited*show
minHeight*inherited*minimum
minWidth*inherited*minimum

Border Attributes


Attributes allowed in nodes of type 'border'.

Inherited defaults will take their value from the associated global attributes (see above).


AttributeDefaultDescription
-------------|:-------------:|
typeborder|
size*inherited*size
minSize*inherited*|
selected-1index
idautoborder_
confignulla
showtrueshow/hide
enableAutoHidefalsehide
children*required*a
barSize*inherited*size
enableDrop*inherited*|
autoSelectTabWhenOpen*inherited*whether
autoSelectTabWhenClosed*inherited*whether
className*inherited*|


Model Actions


All changes to the model are applied through actions.
You can intercept actions resulting from GUI changes before they are applied by
implementing the onAction callback property of the Layout.
You can also apply actions directly using the Model.doAction() method.
This method takes a single argument, created by one of the following action
generators (typically accessed as `FlexLayout.Actions.`):

ActionDescription
------------------|
Actions.addNode(newNodeJson,add
Actions.moveNode(fromNodeId,move
Actions.deleteTab(tabNodeId)delete
Actions.renameTab(tabNodeId,rename
Actions.selectTab(tabNodeId)select
Actions.setActiveTabset(tabsetNodeId)set
Actions.adjustSplit(splitterNodeId,adjust
Actions.adjustBorderSplit(borderNodeId,updates
Actions.maximizeToggle(tabsetNodeId)toggles
Actions.updateModelAttributes(attributes)updates
Actions.updateNodeAttributes(nodeId,updates
Actions.floatTab(nodeId)popout
Actions.unFloatTab(nodeId)restore

Examples


  1. ``` js
  2. model.doAction(FlexLayout.Actions.updateModelAttributes({
  3.     splitterSize:40,
  4.     tabSetHeaderHeight:40,
  5.     tabSetTabStripHeight:40
  6. }));
  7. ```

The above example would increase the size of the splitters, tabset headers and tabs, this could be used to make
adjusting the layout easier on a small device.

  1. ``` js
  2. model.doAction(FlexLayout.Actions.addNode(
  3.     {type:"tab", component:"grid", name:"a grid", id:"5"},
  4.     "1", FlexLayout.DockLocation.CENTER, 0));
  5. ```

This example adds a new grid component to the center of tabset with id "1" and at the 0'th tab position (use value -1 to add to the end of the tabs).
Note: you can get the id of a node (e.g., the node returned by the addNode
action) using the method node.getId().
If an id wasn't assigned when the node was created, then one will be created for you of the form `#` (e.g. `#0c459064-8dee-444e-8636-eb9ab910fb27`).


Layout Component Methods to Create New Tabs


Methods on the Layout Component for adding tabs, the tabs are specified by their layout json.

Example:

  1. ```
  2. this.layoutRef.current.addTabToTabSet("NAVIGATION", {type:"tab", component:"grid", name:"a grid"});
  3. ```
This would add a new grid component to the tabset with id "NAVIGATION" (where this.layoutRef is a ref to the Layout element, see https://reactjs.org/docs/refs-and-the-dom.html ).


LayoutDescription
------------------|
addTabToTabSet(tabsetId,adds
addTabToActiveTabSet(json)adds
addTabWithDragAndDrop(dragText,adds
addTabWithDragAndDropIndirect(dragText,adds
moveTabWithDragAndDrop(Move

Tab Node Events


You can handle events on nodes by adding a listener, this would typically be done
in the components constructor() method.

Example:
  1. ```
  2.     constructor(props) {
  3.         super(props);
  4.         let config = this.props.node.getConfig();

  5.         // save state in flexlayout node tree
  6.         this.props.node.setEventListener("save", (p) => {
  7.              config.subject = this.subject;
  8.         };
  9.     }

  10. ```

EventparametersDescription
-------------|:-------------:|
resize|
close|
visibility|
save|


Running the Examples and Building the Project


First install dependencies:

  1. ```
  2. yarn install
  3. ```

Compile the project and run the examples:
  1. ```
  2. yarn start
  3. ```

Open your browser at http://localhost:8080/examples/ to show the examples directory, click on the examples to run them.

The 'yarn start' command will watch for changes to flexlayout and example source, so you can make changes to the code
and then refresh the browser to see the result.

To run the tests in the Cypress interactive runner use:

  1. ```
  2. yarn cypress
  3. ```

FlexLayout Cypress tests


To build the npm distribution run 'yarn build', this will create the artifacts in the dist dir.

Alternative Layout Managers


NameRepository
-------------|:-------------|
rc-dockhttps://github.com/ticlo/rc-dock
luminohttps://github.com/jupyterlab/lumino
golden-layouthttps://github.com/golden-layout/golden-layout
react-mosaichttps://github.com/nomcopter/react-mosaic