Create Custom Layouts using ESM Components#

In this guide, we will demonstrate how to build custom, reusable layouts using JSComponent, ReactComponent or AnyWidgetComponent.

Layout Two Objects#

This example will show you how to create a split layout containing two objects. We will be using the Split.js library.

import panel as pn

from panel.custom import Child, JSComponent

CSS = """
.split {
    display: flex;
    flex-direction: row;
    height: 100%;
    width: 100%;
}

.gutter {
    background-color: #eee;
    background-repeat: no-repeat;
    background-position: 50%;
}

.gutter.gutter-horizontal {
    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
    cursor: col-resize;
}
"""


class SplitJS(JSComponent):

    left = Child()
    right = Child()

    _esm = """
    import Split from 'https://esm.sh/split.js@1.6.5'

    export function render({ model }) {
      const splitDiv = document.createElement('div');
      splitDiv.className = 'split';

      const split0 = document.createElement('div');
      splitDiv.appendChild(split0);

      const split1 = document.createElement('div');
      splitDiv.appendChild(split1);

      const split = Split([split0, split1])

      model.on('remove', () => split.destroy())

      split0.append(model.get_child("left"))
      split1.append(model.get_child("right"))
      return splitDiv
    }"""

    _stylesheets = [CSS]


pn.extension("codeeditor")

split_js = SplitJS(
    left=pn.widgets.CodeEditor(
        value="Left!",
        sizing_mode="stretch_both",
        margin=0,
        theme="monokai",
        language="python",
    ),
    right=pn.widgets.CodeEditor(
        value="Right",
        sizing_mode="stretch_both",
        margin=0,
        theme="monokai",
        language="python",
    ),
    height=500,
    sizing_mode="stretch_width",
)
split_js.servable()
import panel as pn

from panel.custom import Child, ReactComponent

CSS = """
.split {
    display: flex;
    flex-direction: row;
    height: 100%;
    width: 100%;
}

.gutter {
    background-color: #eee;
    background-repeat: no-repeat;
    background-position: 50%;
}

.gutter.gutter-horizontal {
    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
    cursor: col-resize;
}
"""


class SplitReact(ReactComponent):

    left = Child()
    right = Child()

    _esm = """
    import Split from 'https://esm.sh/react-split@2.0.14'

    export function render({ model }) {
      return (
        <Split className="split">
          {model.get_child("left")}
          {model.get_child("right")}
        </Split>
      )
    }
    """

    _stylesheets = [CSS]


pn.extension("codeeditor")

split_react = SplitReact(
    left=pn.widgets.CodeEditor(
        value="Left!",
        sizing_mode="stretch_both",
        margin=0,
        theme="monokai",
        language="python",
    ),
    right=pn.widgets.CodeEditor(
        value="Right",
        sizing_mode="stretch_both",
        margin=0,
        theme="monokai",
        language="python",
    ),
    height=500,
    sizing_mode="stretch_width",
)
split_react.servable()
import panel as pn

from panel.custom import Child, AnyWidgetComponent

CSS = """
.split {
    display: flex;
    flex-direction: row;
    height: 100%;
    width: 100%;
}

.gutter {
    background-color: #eee;
    background-repeat: no-repeat;
    background-position: 50%;
}

.gutter.gutter-horizontal {
    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
    cursor: col-resize;
}
"""


class SplitAnyWidget(AnyWidgetComponent):

    left = Child()
    right = Child()

    _esm = """
    import Split from 'https://esm.sh/split.js@1.6.5'

    function render({ model, el }) {
      const splitDiv = document.createElement('div');
      splitDiv.className = 'split';

      const split0 = document.createElement('div');
      splitDiv.appendChild(split0);

      const split1 = document.createElement('div');
      splitDiv.appendChild(split1);

      const split = Split([split0, split1])

      model.on('remove', () => split.destroy())

      split0.append(model.get_child("left"))
      split1.append(model.get_child("right"))

      el.appendChild(splitDiv)
    }

    export default {render}
    """

    _stylesheets = [CSS]


pn.extension("codeeditor")

split_anywidget = SplitAnyWidget(
    left=pn.widgets.CodeEditor(
        value="Left!",
        sizing_mode="stretch_both",
        margin=0,
        theme="monokai",
        language="python",
    ),
    right=pn.widgets.CodeEditor(
        value="Right",
        sizing_mode="stretch_both",
        margin=0,
        theme="monokai",
        language="python",
    ),
    height=500,
    sizing_mode="stretch_width",
)
split_anywidget.servable()

Let’s verify that the layout will automatically update when the object is changed.

split_js.right=pn.pane.Markdown("Hi. I'm a `Markdown` pane replacing the `CodeEditor` widget!", sizing_mode="stretch_both")
split_react.right=pn.pane.Markdown("Hi. I'm a `Markdown` pane replacing the `CodeEditor` widget!", sizing_mode="stretch_both")
split_anywidget.right=pn.pane.Markdown("Hi. I'm a `Markdown` pane replacing the `CodeEditor` widget!", sizing_mode="stretch_both")

Now, let’s change it back:

split_js.right=pn.widgets.CodeEditor(
    value="Right",
    sizing_mode="stretch_both",
    margin=0,
    theme="monokai",
    language="python",
)
split_react.right=pn.widgets.CodeEditor(
    value="Right",
    sizing_mode="stretch_both",
    margin=0,
    theme="monokai",
    language="python",
)
split_anywidget.right=pn.widgets.CodeEditor(
    value="Right",
    sizing_mode="stretch_both",
    margin=0,
    theme="monokai",
    language="python",
)

Now, let’s change it back:

split_js.right=pn.widgets.CodeEditor(
    value="Right",
    sizing_mode="stretch_both",
    margin=0,
    theme="monokai",
    language="python",
)
split_react.right=pn.widgets.CodeEditor(
    value="Right",
    sizing_mode="stretch_both",
    margin=0,
    theme="monokai",
    language="python",
)

Layout a List of Objects#

A Panel Column or Row works as a list of objects. It is list-like. In this section, we will show you how to create your own list-like layout using Panel’s NamedListLike class.

import panel as pn
import param

from panel.custom import JSComponent

from panel.layout.base import ListLike

CSS = """
.gutter {
    background-color: #eee;
    background-repeat: no-repeat;
    background-position: 50%;
}
.gutter.gutter-vertical {
    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
    cursor: row-resize;
}
"""


class GridJS(ListLike, JSComponent):

    _esm = """
    import Split from 'https://esm.sh/split.js@1.6.5'

    export function render({ model}) {
      const objects = model.get_child("objects")

      const splitDiv = document.createElement('div');
      splitDiv.className = 'split';
      splitDiv.style.height = `calc(100% - ${(objects.length - 1) * 10}px)`;

      let splits = [];

      objects.forEach((object, index) => {
        const split = document.createElement('div');
        splits.push(split)

        splitDiv.appendChild(split);
        split.appendChild(object);
      })

      Split(splits, {direction: 'vertical'})

      return splitDiv
    }"""

    _stylesheets = [CSS]


pn.extension("codeeditor")

grid_js = GridJS(
    pn.widgets.CodeEditor(
        value="I love beatboxing\n" * 10, theme="monokai", sizing_mode="stretch_both"
    ),
    pn.panel(
        "https://upload.wikimedia.org/wikipedia/commons/d/d3/Beatboxset1_pepouni.ogg",
        sizing_mode="stretch_width",
        height=100,
    ),
    pn.widgets.CodeEditor(
        value="Yes, I do!\n" * 10, theme="monokai", sizing_mode="stretch_both"
    ),
    styles={"border": "2px solid lightgray"},
    height=800,
    width=500,
    sizing_mode="fixed",
).servable()

You must list ListLike, JSComponent in exactly that order when you define the class! Reversing the order to JSComponent, ListLike will not work.

import panel as pn
import param

from panel.custom import ReactComponent
from panel.layout.base import ListLike

CSS = """
.gutter {
    background-color: #eee;
    background-repeat: no-repeat;
    background-position: 50%;
}
.gutter.gutter-vertical {
    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');


 cursor: row-resize;
}
"""


class GridReact(ListLike, ReactComponent):

    _esm = """
    import Split from 'https://esm.sh/react-split@2.0.14'

    export function render({ model}) {
      const objects = model.get_child("objects")
      const calculatedHeight = `calc( 100% - ${(objects.length - 1) * 10}px )`;

      return (
        <Split
            className="split"
            direction="vertical"
            style={{ height: "100%" }}
        >{...objects}</Split>
      )
    }"""

    _stylesheets = [CSS]


pn.extension("codeeditor")

grid_react = GridReact(
    pn.widgets.CodeEditor(
        value="I love beatboxing\n" * 10, theme="monokai", sizing_mode="stretch_both"
    ),
    pn.panel(
        "https://upload.wikimedia.org/wikipedia/commons/d/d3/Beatboxset1_pepouni.ogg",
        sizing_mode="stretch_width",
        height=100,
    ),
    pn.widgets.CodeEditor(
        value="Yes, I do!\n" * 10, theme="monokai", sizing_mode="stretch_both"
    ),
    styles={"border": "2px solid lightgray"},
    height=800,
    width=500,
    sizing_mode="fixed",
)
grid_react.servable()

You must list ListLike, ReactComponent in exactly that order when you define the class! Reversing the order to ReactComponent, ListLike will not work.

import panel as pn
import param

from panel.custom import AnyWidgetComponent

from panel.layout.base import ListLike

CSS = """
.gutter {
    background-color: #eee;
    background-repeat: no-repeat;
    background-position: 50%;
}
.gutter.gutter-vertical {
    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
    cursor: row-resize;
}
"""


class GridAnyWidget(ListLike, AnyWidgetComponent):

    _esm = """
    import Split from 'https://esm.sh/split.js@1.6.5'

    function render({ model, el}) {
      const objects = model.get_child("objects")

      const splitDiv = document.createElement('div');
      splitDiv.className = 'split';
      splitDiv.style.height = `calc(100% - ${(objects.length - 1) * 10}px)`;

      let splits = [];

      objects.forEach((object, index) => {
        const split = document.createElement('div');
        splits.push(split)

        splitDiv.appendChild(split);
        split.appendChild(object);
      })

      Split(splits, {direction: 'vertical'})

      el.appendChild(splitDiv);
    }
    export default {render}
    """

    _stylesheets = [CSS]


pn.extension("codeeditor")

grid_anywidget = GridAnyWidget(
    pn.widgets.CodeEditor(
        value="I love beatboxing\n" * 10, theme="monokai", sizing_mode="stretch_both"
    ),
    pn.panel(
        "https://upload.wikimedia.org/wikipedia/commons/d/d3/Beatboxset1_pepouni.ogg",
        sizing_mode="stretch_width",
        height=100,
    ),
    pn.widgets.CodeEditor(
        value="Yes, I do!\n" * 10, theme="monokai", sizing_mode="stretch_both"
    ),
    styles={"border": "2px solid lightgray"},
    height=800,
    width=500,
    sizing_mode="fixed",
).servable()

You must list ListLike, AnyWidgetComponent in exactly that order when you define the class! Reversing the order to AnyWidgetComponent, ListLike will not work.

You can now use [...] indexing and methods like .append, .insert, pop, etc., as you would expect:

grid_js.append(
    pn.widgets.CodeEditor(
        value="Another one bites the dust\n" * 10,
        theme="monokai",
        sizing_mode="stretch_both",
    )
)
grid_react.append(
    pn.widgets.CodeEditor(
        value="Another one bites the dust\n" * 10,
        theme="monokai",
        sizing_mode="stretch_both",
    )
)
grid_anywidget.append(
    pn.widgets.CodeEditor(
        value="Another one bites the dust\n" * 10,
        theme="monokai",
        sizing_mode="stretch_both",
    )
)

Let’s remove it again:

grid_js.pop(-1)
grid_react.pop(-1)
grid_anywidget.pop(-1)