diff --git a/doc/developer_guide/index.md b/doc/developer_guide/index.md index b5890dc406..2e5dad6006 100644 --- a/doc/developer_guide/index.md +++ b/doc/developer_guide/index.md @@ -82,7 +82,7 @@ All available tasks can be found by running `pixi task list`, the following sect ### Editable install -It can be advantageous to install the Panel in [editable mode](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs): +It can be advantageous to install Panel in [editable mode](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs): ```bash pixi run install @@ -97,6 +97,8 @@ Currently, this needs to be run for each environment. So, if you want to install pixi run -e test-ui install ``` +You can find the list of environments in the **pixi.toml** file. + ::: ## Linting diff --git a/doc/explanation/components/reactive_html_components.md b/doc/explanation/components/reactive_html_components.md index 4658d5511b..a74db5ef99 100644 --- a/doc/explanation/components/reactive_html_components.md +++ b/doc/explanation/components/reactive_html_components.md @@ -12,7 +12,7 @@ This page will walk you through using the `ReactiveHTML` class to craft custom c A *`ReactiveHTML` component* is essentially a class that you create by inheriting from the `ReactiveHTML` class. Within this custom class, you are required to define the `_template` attribute using HTML, which serves as the *design blueprint* for your custom component. You can use Javascript *template variables* `${...}` as well as Python [Jinja2](https://jinja.palletsprojects.com) syntax to make the template *dynamic*. -Here is a basic `SlideShow` component +Here is a basic `Slideshow` component ```{pyodide} import param diff --git a/doc/how_to/custom_components/esm/callbacks.md b/doc/how_to/custom_components/esm/callbacks.md index cf5247f90d..a2ffa5dac1 100644 --- a/doc/how_to/custom_components/esm/callbacks.md +++ b/doc/how_to/custom_components/esm/callbacks.md @@ -4,7 +4,7 @@ In this guide we will show you how to add callbacks to your ESM components. ## Slideshow with Python callback -This example shows you how to create a `SlideShow` component that uses a Python *callback* function to update the `Slideshow` image when its clicked: +This example shows you how to create a `Slideshow` component that uses a Python *callback* function to update the `Slideshow` image when its clicked: ```{pyodide} import param diff --git a/doc/how_to/custom_components/esm/custom_layout.md b/doc/how_to/custom_components/esm/custom_layout.md index d5ad7bc692..bc88360f70 100644 --- a/doc/how_to/custom_components/esm/custom_layout.md +++ b/doc/how_to/custom_components/esm/custom_layout.md @@ -1,8 +1,6 @@ # Create Custom Layouts using ESM Components -In this guide, we will demonstrate how to build custom, reusable layouts using [`JSComponent`](../../reference/panes/JSComponent.md) or [`ReactComponent`](../../reference/panes/ReactComponent.md). - -Please note that it is currently not possible to create layouts using the [`AnyWidgetComponent`](../../reference/panes/AnyWidgetComponent.md), as the underlying [`AnyWidget`](https://anywidget.dev/) API does not support this. +In this guide, we will demonstrate how to build custom, reusable layouts using [`JSComponent`](../../reference/panes/JSComponent.md), [`ReactComponent`](../../reference/panes/ReactComponent.md) or [`AnyWidgetComponent`](../../reference/panes/AnyWidgetComponent.md). ## Layout Two Objects @@ -89,11 +87,9 @@ split_js = SplitJS( ) split_js.servable() ``` - ::: :::{tab-item} `ReactComponent` - ```{pyodide} import panel as pn @@ -165,6 +161,90 @@ split_react.servable() ``` ::: +:::{tab-item} `AnyWidgetComponent` +```{pyodide} +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. @@ -172,6 +252,7 @@ Let's verify that the layout will automatically update when the `object` is chan ::::{tab-set} :::{tab-item} `JSComponent` + ```{pyodide} split_js.right=pn.pane.Markdown("Hi. I'm a `Markdown` pane replacing the `CodeEditor` widget!", sizing_mode="stretch_both") ``` @@ -183,6 +264,14 @@ split_react.right=pn.pane.Markdown("Hi. I'm a `Markdown` pane replacing the `Cod ``` ::: +::: + +:::{tab-item} `AnyWidgetComponent` +```{pyodide} +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: @@ -213,6 +302,18 @@ split_react.right=pn.widgets.CodeEditor( ``` ::: +:::{tab-item} `AnyWidgetComponent` +```{pyodide} +split_anywidget.right=pn.widgets.CodeEditor( + value="Right", + sizing_mode="stretch_both", + margin=0, + theme="monokai", + language="python", +) +``` +::: + :::: ## Layout a List of Objects @@ -362,20 +463,95 @@ grid_react = GridReact( ) 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` +```{pyodide} +import panel as pn +import param -:::{note} -You must list `ListLike, ReactComponent` in exactly that order when you define the class! Reversing the order to `ReactComponent, ListLike` will not work. +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: ::::{tab-set} :::{tab-item} `JSComponent` - ```{pyodide} grid_js.append( pn.widgets.CodeEditor( @@ -399,6 +575,18 @@ grid_react.append( ``` ::: +:::{tab-item} `AnyWidgetComponent` +```{pyodide} +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: @@ -417,4 +605,10 @@ grid_react.pop(-1) ``` ::: +:::{tab-item} `AnyWidgetComponent` +```{pyodide} +grid_anywidget.pop(-1) +``` +::: + :::: diff --git a/doc/how_to/custom_components/esm/custom_widgets.md b/doc/how_to/custom_components/esm/custom_widgets.md index 279b77662a..d3a91e6992 100644 --- a/doc/how_to/custom_components/esm/custom_widgets.md +++ b/doc/how_to/custom_components/esm/custom_widgets.md @@ -97,8 +97,8 @@ export function render({ model }) { const [image] = model.useState("image"); return ( - ) } @@ -129,7 +129,7 @@ export function render({ model }) { button = ImageButton( image="https://panel.holoviz.org/_static/logo_stacked.png", styles={"border": "2px solid lightgray"}, - width=400, height=200 + width=400 ) pn.Column(button, button.param.clicks).servable() ``` @@ -141,6 +141,7 @@ import panel as pn import param from panel.custom import AnyWidgetComponent +from panel.widgets import WidgetBase pn.extension() @@ -208,7 +209,7 @@ pn.Column(button, button.param.clicks).servable() If you don't want the *button* styling, you can change the `