Repository: mwouts/itables (language: python)

Issue:
Deploy a sample Shiny app that uses ITables
This can be done using https://shiny.posit.co/py/docs/shinylive.html.

See also https://www.appsilon.com/post/shiny-for-python-deploy-github-pages


Discussion / hints:
I have a working example here: https://github.com/fredguth/shinyduck/blob/main/express-itables-mwe.py
> @fredguth thanks for sharing. If I have a custom css. how do I apply it to the itable  output within my shiny express app.  

```css = """
.dt-container {
  font-size: small;
}

.dataTable th {
  font-weight: bolder;
  background-color: #e1e6ec;
  color: #120051;
  text-align: center !important;
}

.dataTable tbody {
  background-color: white;
  color: #120051;
}

/* Left-align the first visible column (index) */
.dataTable td:first-child,
.dataTable th:first-child {
  text-align: left !important;
}

/* Center-align all other columns */
.dataTable td:not(:first-child),
.dataTable th:not(:first-child) {
  text-align: center !important;
}
"""
``` 

often within ipython  notebooks I can do  this to apply to all tables

```
from IPython.core.display import display, HTML
display(HTML(f"<style>{css}</style>" ""))
```

It may not be of much help, but this is my function I use in Shiny core.

```
def build_ui():
    return ui.page_fillable(
        ui.tags.link(href="am.css", rel="stylesheet"),

        ui.layout_sidebar(
            left_sidebar(),
            ui.layout_sidebar(
                right_sidebar(),
                center_panel(),
                border=False),

            border_radius=False,
            class_=f"{NO_SPACING}"),
        title=APPLICATION_TITLE,
        class_=f"{NO_SPACING}")

from my globals:
NO_SPACING = "m-0 p-0 g-0"


This is all used in my web.py

from pathlib import Path

from shiny import App

from dex.gui.server import server
from dex.gui.view.ui import build_ui

app_ui = build_ui()
app = App(app_ui, server, static_assets=Path(__file__).parent.joinpath("static"))

And desktop app:

app_ui = build_ui()

app = App(app_ui, server, static_assets=Path(__file__).parent.joinpath("static"))
kwargs = {"app": app, "port": 8000, "log_config": logger.configuration()}

FlaskUI(app=app, server="fastapi", width=1600, height=950, server_kwargs=kwargs).run()
```
@micboat to include your custom css, you could save it under `custom.css` then add this to your app:
```
ui.include_css(Path(__file__).parent / "custom.css")
```

Fix patch:
diff --git a/README.md b/README.md
index be1d8c5c..b68df60b 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 [![Conda Version](https://img.shields.io/conda/vn/conda-forge/itables.svg)](https://anaconda.org/conda-forge/itables)
 [![pyversions](https://img.shields.io/pypi/pyversions/itables.svg)](https://pypi.python.org/pypi/itables)
 [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
-[![Jupyter Widget](https://img.shields.io/badge/Jupyter-Widget-F37626.svg?style=flat&logo=Jupyter)](https://mwouts.github.io/itables/ipywidgets.html)
+[![Jupyter Widget](https://img.shields.io/badge/Jupyter-Widget-F37626.svg?style=flat&logo=Jupyter)](https://mwouts.github.io/itables/widget.html)
 [![Streamlit App](https://static.streamlit.io/badges/streamlit_badge_black_red.svg)](https://itables.streamlit.app)
 
 This packages changes how Pandas and Polars DataFrames are rendered in Jupyter Notebooks.
@@ -65,6 +65,6 @@ You can also use ITables in [Quarto](https://mwouts.github.io/itables/quarto.htm
 ITables works well in VS Code, both in Jupyter Notebooks and in interactive Python sessions.
 
 Last but not least, ITables is also available as
-- a [Jupyter Widget](https://mwouts.github.io/itables/ipywidgets.html)
+- a [Jupyter Widget](https://mwouts.github.io/itables/widget.html)
 - a [Streamlit](https://mwouts.github.io/itables/streamlit.html) component,
 - and it also works in [Shiny](https://mwouts.github.io/itables/shiny.html) applications.
diff --git a/docs/_toc.yml b/docs/_toc.yml
index 1659cbc1..391b7095 100644
--- a/docs/_toc.yml
+++ b/docs/_toc.yml
@@ -17,7 +17,7 @@ parts:
     - file: supported_editors
   - caption: ITables in Applications
     chapters:
-      - file: ipywidgets
+      - file: widget
       - file: streamlit
       - file: shiny
       - file: dash
diff --git a/docs/changelog.md b/docs/changelog.md
index 4de95364..e558c6a0 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,13 +1,17 @@
 ITables ChangeLog
 =================
 
-2.2.2-dev (2024-09-??)
+2.2.2 (2024-09-29)
 ------------------
 
 **Fixed**
 - We have fixed a HTML pop up warning when displaying Pandas Style objects in Quarto ([#317](https://github.com/mwouts/itables/issues/317))
 - The dependencies of the Streamlit component have been updated ([#323](https://github.com/mwouts/itables/pull/323))
 
+**Added**
+- We have documented how to use the `ITable` widget in a Shiny application
+- We have deployed a sample Shiny app ([#276](https://github.com/mwouts/itables/issues/276))
+
 
 2.2.1 (2024-09-22)
 ------------------
diff --git a/docs/quick_start.md b/docs/quick_start.md
index e62968f7..cee25d9c 100644
--- a/docs/quick_start.md
+++ b/docs/quick_start.md
@@ -20,7 +20,7 @@ kernelspec:
 [![Conda Version](https://img.shields.io/conda/vn/conda-forge/itables.svg)](https://anaconda.org/conda-forge/itables)
 [![pyversions](https://img.shields.io/pypi/pyversions/itables.svg)](https://pypi.python.org/pypi/itables)
 [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
-[![Jupyter Widget](https://img.shields.io/badge/Jupyter-Widget-F37626.svg?style=flat&logo=Jupyter)](ipywidgets.md)
+[![Jupyter Widget](https://img.shields.io/badge/Jupyter-Widget-F37626.svg?style=flat&logo=Jupyter)](widget.md)
 [![Streamlit App](https://static.streamlit.io/badges/streamlit_badge_black_red.svg)](https://itables.streamlit.app)
 <a class="github-button" href="https://github.com/mwouts/itables" data-icon="octicon-star" data-show-count="true"></a>
 <script src="https://buttons.github.io/buttons.js"></script>
diff --git a/docs/select.md b/docs/select.md
index 082e6b32..d3f1e87e 100644
--- a/docs/select.md
+++ b/docs/select.md
@@ -68,7 +68,7 @@ show(
 ```{tip}
 It is possible to get the updated `selected_rows` back in Python but for this you will have to use,
 instead of `show`, either
-- the `ITable` [Jupyter Widget](ipywidgets.md)
+- the `ITable` [Jupyter Widget](widget.md)
 - the `interactive_table` [Streamlit component](streamlit.md)
 - or `DT` in a [Shiny app](shiny.md).
 ```
diff --git a/docs/shiny.md b/docs/shiny.md
index 125339cf..3d09cacc 100644
--- a/docs/shiny.md
+++ b/docs/shiny.md
@@ -1,6 +1,109 @@
 # Shiny
 
-You can use ITables in Web applications generated with [Shiny](https://shiny.rstudio.com/py/) for Python with e.g.
+## Using `ITable` in Shiny
+
+The recommended way to use `ITables` in a [Shiny for Python](https://shiny.rstudio.com/py/) application is with the [ITable Widget](widget.md).
+
+In the Shiny Express syntax this is as simple as:
+```python
+from shinywidgets import render_widget
+
+from itables.widget import ITable
+
+
+@render_widget
+def my_table():
+    """
+    This function creates the "my_table" widget.
+    """
+    # Note: df is an optional argument
+    return ITable(caption="A table rendered with ITable")
+```
+
+In the Shiny Core syntax you will need, in addition to the above,
+to insert the table in the UI with `output_widget`:
+
+```python
+from shiny import ui
+from shinywidgets import output_widget
+
+app_ui = ui.page_fluid(
+    # ...
+    output_widget("my_table"),
+    # ...
+)
+```
+
+## Updating the widget
+
+Rather than re-creating the widget each time the data changes, you can
+call the `.update` method of the widget object, using the `@reactive.effect`
+decorator:
+
+```python
+from shiny import reactive
+from shiny.express import input
+
+from itables.sample_dfs import get_dict_of_test_dfs
+
+dfs = get_dict_of_test_dfs()
+
+
+@reactive.effect
+def _():
+    """
+    This "reactive.effect" calls the "update" method of the ITable widget
+    to update the widget with the new inputs.
+    """
+    # Get the new inputs
+    df = dfs[input.table_selector()]
+    selected_rows = list(range(0, len(df), 3))
+
+    # Update the widget
+    my_table.widget.update(df, selected_rows=selected_rows)
+```
+
+## Accessing the `selected_rows` attribute
+
+The `reactive_read` function lets you access the `selected_rows` attribute
+of the `ITable` object. The code below displays the selected rows:
+
+```python
+from shiny.express import render
+from shinywidgets import reactive_read
+
+
+@render.code
+def selected_rows():
+    """
+    Here we read the "selected_rows" attribute of the ITable widget
+    """
+    return str(reactive_read(my_table.widget, "selected_rows"))
+```
+
+## An example application
+
+This [repository](https://github.com/mwouts/demo_itables_in_shiny-py/) contains a simple
+example of a Shiny application that uses the `ITable` widget.
+
+The source code of the application
+is at [`app.py`](https://github.com/mwouts/demo_itables_in_shiny-py/blob/main/itable_widget/app.py)
+(Shiny Express) or [`app-core.py`](https://github.com/mwouts/demo_itables_in_shiny-py/blob/main/itable_widget/app-core.py)
+(Shiny Core).
+
+```{div}
+<iframe src="https://itables.shinyapps.io/itable_widget?embed=true"
+style="height: 800px; width: 100%;"></iframe>
+```
+
+## Limitations
+
+Compared to `show`, the `ITable` widget has the same limitations as the [Streamlit component](streamlit.md#limitations),
+e.g. structured headers are not available, you can't pass JavaScript callback, etc.
+
+The good news is that if you only want to _display_ the table, you do not need
+the `ITable` widget. You can render the table using `HTML(DT(...))` as here:
+
 ```python
 from shiny import ui
 
@@ -9,14 +112,17 @@ from itables.shiny import DT, init_itables
 
 # Load the datatables library and css from the ITables package
 # (use connected=True if you prefer to load it from the internet)
-ui.HTML(init_itables(connected=False))
+ui.HTML(init_itables())
 
 # Render the table with DT
 ui.HTML(DT(get_countries(html=False)))
 ```
 
-If you enable row selection and set an id on your table, e.g. `DT(df, table_id="my_table", select=True)` then
-ITables will provide the list of selected rows at `input.my_table_selected_rows()` (replace `my_table` with your
-own table id).
+An example for an application that uses `DT` is available at [`app.py`](https://github.com/mwouts/demo_itables_in_shiny-py/blob/main/itables_DT/app.py)
+(Shiny Express) or [`app-core.py`](https://github.com/mwouts/demo_itables_in_shiny-py/blob/main/itables_DT/app-core.py)
+(Shiny Core).
 
-See also our [tested examples](https://github.com/mwouts/itables/tree/main/tests/sample_python_apps).
+```{div}
+<iframe src="https://itables.shinyapps.io/itables_DT?embed=true"
+style="height: 800px; width: 100%;"></iframe>
+```
diff --git a/docs/ipywidgets.md b/docs/widget.md
similarity index 91%
rename from docs/ipywidgets.md
rename to docs/widget.md
index 2fc17513..cb3697c8 100644
--- a/docs/ipywidgets.md
+++ b/docs/widget.md
@@ -37,6 +37,12 @@ table = ITable(df, selected_rows=[0, 2, 5], select=True)
 table
 ```
 
+```{tip}
+The table shown above does not reflect the initial row selection.
+This is because the `ITable` widget was updated with
+more row selection commands, see below.
+```
+
 ## The `selected_rows` traits
 
 The `selected_rows` attribute of the `ITable` object provides a view on the
@@ -93,7 +99,7 @@ Compared to `show`, the `ITable` widget has the same limitations as the [Streaml
 e.g. structured headers are not available, you can't pass JavaScript callback, etc.
 
 The good news is that if you only want to _display_ the table, you do not need
-the `ITables` widget. Below is an example in which we use `show` to display a different
+the `ITable` widget. Below is an example in which we use `show` to display a different
 table depending on the value of a drop-down component:
 
 ```python
diff --git a/environment.yml b/environment.yml
index 01470a5e..ee867a11 100644
--- a/environment.yml
+++ b/environment.yml
@@ -24,6 +24,7 @@ dependencies:
   - twine
   - ghp-import
   - shiny
+  - shinywidgets
   - streamlit
   - anywidget
   - pip:
diff --git a/pyproject.toml b/pyproject.toml
index 5146cbaa..7152faca 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,8 +44,8 @@ test = [
   "ipykernel",
   "nbconvert",
   "jupytext",
-  # Shiny test app
   "shiny",
+  # "shinywidgets",
   # Test urls
   "requests",
 ]
diff --git a/src/itables/version.py b/src/itables/version.py
index 70358218..7118b680 100644
--- a/src/itables/version.py
+++ b/src/itables/version.py
@@ -1,3 +1,3 @@
 """ITables' version number"""
 
-__version__ = "2.2.2-dev"
+__version__ = "2.2.2"


Test patch:
diff --git a/tests/sample_python_apps/itables_in_a_shiny_app.py b/tests/sample_python_apps/itables_in_a_shiny_app.py
deleted file mode 100644
index a71115e0..00000000
--- a/tests/sample_python_apps/itables_in_a_shiny_app.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# Run this app with "shiny run file.py"
-from shiny import App, Inputs, Outputs, Session, render, ui
-
-from itables.sample_dfs import get_countries
-from itables.shiny import DT, init_itables
-
-app_ui = ui.page_fluid(
-    ui.HTML(init_itables()),
-    ui.layout_columns(
-        ui.value_box(
-            "Selected rows",
-            ui.output_ui("selected_rows"),
-        ),
-        fill=False,
-    ),
-    ui.layout_columns(
-        ui.card(ui.output_ui("table"), full_screen=True),
-    ),
-    title="ITables in a Shiny App",
-)
-
-
-def server(input: Inputs, output: Outputs, session: Session):
-    @render.ui
-    def table():
-        return ui.HTML(
-            DT(
-                get_countries(html=False),
-                table_id="countries",
-                select=True,
-                selected_rows=[0, 1, 2, 207],
-            )
-        )
-
-    @render.ui
-    def selected_rows():
-        selected_rows = list(input.countries_selected_rows())
-        return f"You have selected {len(selected_rows)} rows: {selected_rows}"
-
-
-app = App(app_ui, server, debug=True)
diff --git a/tests/sample_python_apps/itables_in_a_shiny_app_with_tabs.py b/tests/sample_python_apps/itables_in_a_shiny_app_with_tabs.py
deleted file mode 100644
index 26171cb1..00000000
--- a/tests/sample_python_apps/itables_in_a_shiny_app_with_tabs.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Run this app with "shiny run file.py"
-from shiny import App, ui
-
-from itables.sample_dfs import get_dict_of_test_dfs
-from itables.shiny import DT
-
-app_ui = ui.page_fluid(
-    # Display the different tables in different tabs
-    ui.navset_tab(
-        *[
-            ui.nav_panel(name, ui.HTML(DT(df)))
-            for name, df in get_dict_of_test_dfs().items()
-        ]
-    )
-)
-
-app = App(app_ui, server=None)
diff --git a/tests/test_documentation_notebooks_run.py b/tests/test_documentation_notebooks_run.py
index 17a4ddcc..3905b65c 100644
--- a/tests/test_documentation_notebooks_run.py
+++ b/tests/test_documentation_notebooks_run.py
@@ -31,6 +31,8 @@ def test_run_documentation_notebooks(notebook):
         pytest.skip("Polars is not available")
     if "pandas_style" in notebook.stem and pd_style is None:
         pytest.skip("Pandas Style is not available")
+    if "shiny" in notebook.stem:
+        pytest.skip("shinywidgets makes the widget.md notebook fail")
 
     org_options = dir(opt)
 
diff --git a/tests/test_sample_python_apps.py b/tests/test_sample_python_apps.py
deleted file mode 100644
index e3a8a7a7..00000000
--- a/tests/test_sample_python_apps.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import runpy
-from pathlib import Path
-
-import pytest
-from packaging.version import Version
-
-try:
-    from shiny import __version__ as shiny_version
-except ImportError:
-    shiny_version = "NA"
-
-
-def get_app_file_list():
-    return [
-        app_file
-        for app_file in (Path(__file__).parent / "sample_python_apps").iterdir()
-    ]
-
-
-def test_get_app_file_list():
-    app_file_list = get_app_file_list()
-    for app_file in app_file_list:
-        assert app_file.suffix == ".py"
-    assert "itables_in_a_shiny_app" in [app_file.stem for app_file in app_file_list]
-
-
-@pytest.mark.parametrize("app_file", get_app_file_list(), ids=lambda path: path.stem)
-@pytest.mark.skipif(
-    shiny_version == "NA" or Version(shiny_version) < Version("1.0"),
-    reason=f"This test requires shiny>=1.0, got {shiny_version}",
-)
-def test_app_file(app_file):
-    print(f"This is shiny=={shiny_version}")
-    runpy.run_path(str(app_file))