Repository: mwouts/itables

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:
github-actions[bot]: Thank you for making this pull request.

Did you know? You can try it on Binder: [![Binder:lab](https://img.shields.io/badge/binder-jupyterlab-0172B2.svg)](https://mybinder.org/v2/gh/mwouts/itables/document_itable_widget_in_shiny?urlpath=lab/tree/docs/quick_start.md).

Also, the version of ITables developed in this PR can be installed with `pip`:
```
pip install git+https://github.com/mwouts/itables.git@document_itable_widget_in_shiny
```
(this requires `nodejs`, see more at [Developing ITables](https://mwouts.github.io/itables/developing.html))
<!-- thollander/actions-comment-pull-request "binder_link" -->
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/mwouts/itables/pull/324?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 91.14%. Comparing base [(`a03d71c`)](https://app.codecov.io/gh/mwouts/itables/commit/a03d71c61aa7a509031d2bbdce94dd7a6642e1d2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts) to head [(`ac57c4e`)](https://app.codecov.io/gh/mwouts/itables/commit/ac57c4e9b09e076b553eaaf1449adf65cce4084c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #324      +/-   ##
==========================================
- Coverage   93.94%   91.14%   -2.81%     
==========================================
  Files          28       23       -5     
  Lines        1338     1152     -186     
==========================================
- Hits         1257     1050     -207     
- Misses         81      102      +21     
```

| [Flag](https://app.codecov.io/gh/mwouts/itables/pull/324/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts) | Coverage Δ | |
|---|---|---|
| [](https://app.codecov.io/gh/mwouts/itables/pull/324/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts) | `91.14% <ø> (-2.81%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/mwouts/itables/pull/324?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts).


Patch:
diff --git a/README.md b/README.md
index be1d8c5..b68df60 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 1659cbc..391b709 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 4de9536..e558c6a 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 e62968f..cee25d9 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 082e6b3..d3f1e87 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 125339c..3d09cac 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 2fc1751..cb3697c 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 01470a5..ee867a1 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 5146cba..7152fac 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 7035821..7118b68 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 a71115e..0000000
--- 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 26171cb..0000000
--- 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 17a4ddc..3905b65 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 e3a8a7a..0000000
--- 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))