I'm encountering a peculiar issue when developing my Python shiny app. My app currently has the functionality to dynamically generate new tabs with the press of a navset tab called "+". However, after pressing "+", the state (including input and output values) of the previous tabs reset back to empty. Is there a way to preserve the state of any previously existing tabs?
My code is outlined below:
from shiny import App, Inputs, Outputs, Session, module, reactive, render, ui
# Create a Module UI
@module.ui
def textbox_ui(panelNum):
return ui.nav_panel(
f"Tab {panelNum}",
ui.input_text_area(id=f"test_text",
label = "Enter some text"),
ui.output_ui(f"display_text_{panelNum}"),
value = f"Tab_{panelNum}"
)
# Set up module server
@module.server
def textbox_server(input, output, session, panelNum):
@output(id=f"display_text_{panelNum}")
@render.text
def return_text():
return input[f"test_text"]()
# Set up app UI
app_ui = ui.page_fluid(
ui.tags.head(
ui.tags.style(
"""
body {
height: 100vh;
overflow-y: auto !important;
}
"""
)
),
ui.h2('Test App', align='center', fillable=True),
ui.output_ui("tab_UI"),
title = "Test App"
)
# Set up server
def server(input, output, session):
# Set up reactive values
navs = reactive.value(0)
# Add tabs if the user presses "+"
@reactive.effect
@reactive.event(input.shiny_tabs)
def add_tabs():
if input.shiny_tabs() == "+":
navs.set(navs.get() + 1)
@output
@render.ui
def tab_UI():
[textbox_server(str(x), panelNum=x+1) for x in range(navs.get())]
ui.update_navs("shiny_tabs", selected = f"Tab_{navs.get()}")
return ui.navset_tab(
ui.nav_panel("Home",
ui.card(
ui.card_header("Overview"),
ui.p("An example of the outputs clearing")
),
value = "panel0"
),
*[textbox_ui(str(x), panelNum=x+1) for x in range(navs.get())],
ui.nav_panel("+"),
id = "shiny_tabs"
)
app = App(app_ui, server)
The reason for the reset is that the navset_tab
is re-rendered each time a new nav_panel
gets appended. So an approach would be better where we have the navset_tab
outside of the server and then append a nav_panel
on click without re-rendering everything.
The difficulty is on the one hand that ui.insert_ui
does not seem to be suitable for the insert and on the other hand that Shiny for Python currently does not carry functions for dynamic navs, see e.g. posit-dev/py-shiny#089.
However, within the PR#90 is a draft for an insert function nav_insert
which is suitable for this application. I adapted this below and re-wrote your app, we now only insert a new tab if the button is clicked, the rest stays stable.
import sys
from shiny import App, reactive, ui, Session, Inputs, Outputs, module, render
from shiny._utils import run_coro_sync
from shiny._namespaces import resolve_id
from shiny.types import NavSetArg
from shiny.session import require_active_session
from typing import Optional, Union
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal
# adapted from https://github.com/posit-dev/py-shiny/pull/90/files
def nav_insert(
id: str,
nav: Union[NavSetArg, str],
target: Optional[str] = None,
position: Literal["after", "before"] = "after",
select: bool = False,
session: Optional[Session] = None,
) -> None:
"""
Insert a new nav item into a navigation container.
Parameters
----------
id
The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object).
nav
The navigation item to insert (typically a :func:`~shiny.ui.nav` or
:func:`~shiny.ui.nav_menu`). A :func:`~shiny.ui.nav_menu` isn't allowed when the
``target`` references an :func:`~shiny.ui.nav_menu` (or an item within it). A
string is only allowed when the ``target`` references a
:func:`~shiny.ui.nav_menu`.
target
The ``value`` of an existing :func:`shiny.ui.nav` item, next to which tab will
be added. Can also be ``None``; see ``position``.
position
The position of the new nav item relative to the target nav item. If
``target=None``, then ``"before"`` means the new nav item should be inserted at
the head of the navlist, and ``"after"`` is the end.
select
Whether the nav item should be selected upon insertion.
session
A :class:`~shiny.Session` instance. If not provided, it is inferred via
:func:`~shiny.session.get_current_session`.
"""
session = require_active_session(session)
li_tag, div_tag = nav.resolve(
selected=None, context=dict(tabsetid="tsid", index="id")
)
msg = {
"inputId": resolve_id(id),
"liTag": session._process_ui(li_tag),
"divTag": session._process_ui(div_tag),
"menuName": None,
"target": target,
"position": position,
"select": select,
}
def callback() -> None:
run_coro_sync(session._send_message({"shiny-insert-tab": msg}))
session.on_flush(callback, once=True)
@module.ui
def textbox_ui(panelNum):
return ui.nav_panel(
f"Tab {panelNum}",
ui.input_text_area(id=f"test_text_{panelNum}",
label = "Enter some text"),
ui.output_ui(f"display_text_{panelNum}"),
value = f"Tab_{panelNum}"
)
@module.server
def textbox_server(input, output, session, panelNum):
@output(id=f"display_text_{panelNum}")
@render.text
def return_text():
return input[f"test_text_{panelNum}"]()
app_ui = ui.page_fluid(
ui.tags.head(
ui.tags.style(
"""
body {
height: 100vh;
overflow-y: auto !important;
}
"""
)
),
ui.h2('Test App', align='center', fillable=True),
ui.navset_tab(
ui.nav_panel("Home",
ui.card(
ui.card_header("Overview"),
ui.p("An example of the outputs not clearing")
),
value = "Tab_0"
),
ui.nav_panel("+"),
id = "shiny_tabs"
),
title = "Test App"
)
def server(input: Inputs, output: Outputs, session: Session):
navCounter = reactive.value(0)
@reactive.effect
@reactive.event(input.shiny_tabs)
def add_tabs():
if input.shiny_tabs() == "+":
navCounter.set(navCounter.get() + 1)
id = str(navCounter.get())
idPrev = str(navCounter.get() - 1)
nav_insert(
"shiny_tabs",
textbox_ui(id, id),
target=f"Tab_{idPrev}",
position="after",
select=True
)
textbox_server(id, id)
app = App(app_ui, server)