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
:::
::::