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.
::::{tab-set}
:::{tab-item} JSComponent
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/[email protected]'
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()
:::
:::{tab-item} ReactComponent
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/[email protected]'
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()
:::
:::{tab-item} AnyWidgetComponent
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/[email protected]'
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.
::::{tab-set}
:::{tab-item} JSComponent
split_js.right=pn.pane.Markdown("Hi. I'm a `Markdown` pane replacing the `CodeEditor` widget!", sizing_mode="stretch_both")
:::
:::{tab-item} ReactComponent
split_react.right=pn.pane.Markdown("Hi. I'm a `Markdown` pane replacing the `CodeEditor` widget!", sizing_mode="stretch_both")
:::
:::{tab-item} AnyWidgetComponent
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:
::::{tab-set}
:::{tab-item} JSComponent
split_js.right=pn.widgets.CodeEditor(
value="Right",
sizing_mode="stretch_both",
margin=0,
theme="monokai",
language="python",
)
:::
:::{tab-item} ReactComponent
split_react.right=pn.widgets.CodeEditor(
value="Right",
sizing_mode="stretch_both",
margin=0,
theme="monokai",
language="python",
)
:::
:::{tab-item} AnyWidgetComponent
split_anywidget.right=pn.widgets.CodeEditor(
value="Right",
sizing_mode="stretch_both",
margin=0,
theme="monokai",
language="python",
)
:::
::::
Now, let's change it back:
::::{tab-set}
:::{tab-item} JSComponent
split_js.right=pn.widgets.CodeEditor(
value="Right",
sizing_mode="stretch_both",
margin=0,
theme="monokai",
language="python",
)
:::
:::{tab-item} ReactComponent
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.
::::{tab-set}
:::{tab-item} JSComponent
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/[email protected]'
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. :::
:::{tab-item} ReactComponent
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/[email protected]'
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. :::
:::{tab-item} AnyWidgetComponent
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/[email protected]'
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:
::::{tab-set}
:::{tab-item} JSComponent
grid_js.append(
pn.widgets.CodeEditor(
value="Another one bites the dust\n" * 10,
theme="monokai",
sizing_mode="stretch_both",
)
)
:::
:::{tab-item} ReactComponent
grid_react.append(
pn.widgets.CodeEditor(
value="Another one bites the dust\n" * 10,
theme="monokai",
sizing_mode="stretch_both",
)
)
:::
:::{tab-item} AnyWidgetComponent
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:
::::{tab-set}
:::{tab-item} JSComponent
:::
:::{tab-item} ReactComponent
:::
:::{tab-item} AnyWidgetComponent
:::
::::