Skip to content

Commit 084e969

Browse files
cpsievertjcheng5elnelson575
authored
feat: Implement dynamic navs (#90)
Co-authored-by: Joe Cheng <[email protected]> Co-authored-by: E Nelson <[email protected]> Co-authored-by: Liz Nelson <[email protected]>
1 parent e58e9f6 commit 084e969

File tree

15 files changed

+738
-0
lines changed

15 files changed

+738
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### New features
1111

12+
* Added `ui.insert_nav_panel()`, `ui.remove_nav_panel()`, and `ui.update_nav_panel()` to support dynamic navigation. (#90)
13+
1214
* Added support for python 3.13. (#1711)
1315

1416
* `ui.sidebar()` is now interactively resizable. (#2020)

docs/_quartodoc-core.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ quartodoc:
8484
- ui.navset_pill_list
8585
- ui.navset_hidden
8686
- ui.navbar_options
87+
- ui.insert_nav_panel
88+
- ui.remove_nav_panel
89+
- ui.update_nav_panel
8790
- title: UI panels
8891
desc: Visually group together a section of UI components.
8992
contents:

docs/_quartodoc-express.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ quartodoc:
7878
- express.ui.navset_pill_list
7979
- express.ui.navset_hidden
8080
- express.ui.navbar_options
81+
- express.ui.insert_nav_panel
82+
- express.ui.remove_nav_panel
83+
- express.ui.update_nav_panel
8184
- title: Chat interface
8285
desc: Build a chatbot interface
8386
contents:
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from shiny import App, Inputs, Outputs, Session, reactive, ui
2+
3+
app_ui = ui.page_sidebar(
4+
ui.sidebar(
5+
ui.input_action_button("add", "Add 'Dynamic' tab"),
6+
ui.input_action_button("update_foo", "Add/Remove 'Foo' tab"),
7+
),
8+
ui.navset_tab(
9+
ui.nav_panel("Hello", "This is the hello tab", value="Hello"),
10+
ui.nav_panel("Foo", "This is the Foo tab", value="Foo"),
11+
ui.nav_menu(
12+
"Static",
13+
ui.nav_panel("Static 1", "Static 1", value="s1"),
14+
ui.nav_panel("Static 2", "Static 2", value="s2"),
15+
value="Menu",
16+
),
17+
id="tabs",
18+
),
19+
)
20+
21+
22+
def server(input: Inputs, output: Outputs, session: Session):
23+
24+
@reactive.effect
25+
@reactive.event(input.update_foo)
26+
def _():
27+
if input.update_foo() % 2 == 0:
28+
ui.insert_nav_panel(
29+
"tabs",
30+
ui.nav_panel("Foo", "Foo is back now", value="Foo"),
31+
target="Menu",
32+
position="before",
33+
select=True,
34+
)
35+
else:
36+
ui.remove_nav_panel("tabs", target="Foo")
37+
38+
@reactive.effect
39+
@reactive.event(input.add)
40+
def _():
41+
id = "Dynamic-" + str(input.add())
42+
ui.insert_nav_panel(
43+
"tabs",
44+
ui.nav_panel(id, id, value=id),
45+
target="s2",
46+
position="before",
47+
)
48+
49+
ui.notification_show(f"Added tab to menu: {id}")
50+
51+
52+
app = App(app_ui, server)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from shiny import reactive
2+
from shiny.express import input, ui
3+
4+
with ui.sidebar():
5+
ui.input_action_button("add", "Add 'Dynamic' tab")
6+
ui.input_action_button("update_foo", "Add/Remove 'Foo' tab")
7+
8+
9+
with ui.navset_tab(id="tabs"):
10+
with ui.nav_panel("Hello", value="Hello"):
11+
"This is the hello tab"
12+
with ui.nav_panel("Foo", value="Foo"):
13+
"This is the Foo tab"
14+
with ui.nav_menu("Static", value="Menu"):
15+
with ui.nav_panel("Static 1", value="s1"):
16+
"Static 1"
17+
with ui.nav_panel("Static 2", value="s2"):
18+
"Static 2"
19+
20+
21+
@reactive.effect
22+
@reactive.event(input.update_foo)
23+
def _():
24+
if input.update_foo() % 2 == 0:
25+
ui.insert_nav_panel(
26+
"tabs",
27+
"Foo",
28+
"Foo is back now",
29+
value="Foo",
30+
target="Menu",
31+
position="before",
32+
select=True,
33+
)
34+
else:
35+
ui.remove_nav_panel("tabs", target="Foo")
36+
37+
38+
@reactive.effect
39+
@reactive.event(input.add)
40+
def _():
41+
id = "Dynamic-" + str(input.add())
42+
ui.insert_nav_panel("tabs", title=id, value=id, target="s2", position="before")
43+
ui.notification_show(f"Added tab to menu: {id}")
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from shiny import App, Inputs, Outputs, Session, reactive, ui
2+
3+
app_ui = ui.page_sidebar(
4+
ui.sidebar(
5+
"Home",
6+
ui.input_action_button("hideTab", "Hide 'Foo' tab"),
7+
ui.input_action_button("showTab", "Show 'Foo' tab"),
8+
ui.input_action_button("hideMenu", "Hide 'More' nav_menu"),
9+
ui.input_action_button("showMenu", "Show 'More' nav_menu"),
10+
),
11+
ui.navset_tab(
12+
ui.nav_panel("Foo", "This is the foo tab", value="Foo"),
13+
ui.nav_panel("Bar", "This is the bar tab", value="Bar"),
14+
ui.nav_menu(
15+
"More",
16+
ui.nav_panel("Table", "Table page"),
17+
ui.nav_panel("About", "About page"),
18+
"------",
19+
"Even more!",
20+
ui.nav_panel("Email", "Email page"),
21+
value="More",
22+
),
23+
id="tabs",
24+
),
25+
title="Navbar page",
26+
id="sidebar",
27+
)
28+
29+
30+
def server(input: Inputs, output: Outputs, session: Session):
31+
@reactive.effect
32+
@reactive.event(input.hideTab)
33+
def _():
34+
ui.update_nav_panel("tabs", target="Foo", method="hide")
35+
36+
@reactive.effect
37+
@reactive.event(input.showTab)
38+
def _():
39+
ui.update_nav_panel("tabs", target="Foo", method="show")
40+
41+
@reactive.effect
42+
@reactive.event(input.hideMenu)
43+
def _():
44+
ui.update_nav_panel("tabs", target="More", method="hide")
45+
46+
@reactive.effect
47+
@reactive.event(input.showMenu)
48+
def _():
49+
ui.update_nav_panel("tabs", target="More", method="show")
50+
51+
52+
app = App(app_ui, server)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from shiny import reactive
2+
from shiny.express import input, ui
3+
4+
with ui.layout_sidebar():
5+
with ui.sidebar(title="Navbar page", id="sidebar"):
6+
"Home"
7+
ui.input_action_button("hideTab", "Hide 'Foo' tab")
8+
ui.input_action_button("showTab", "Show 'Foo' tab")
9+
ui.input_action_button("hideMenu", "Hide 'More' nav_menu")
10+
ui.input_action_button("showMenu", "Show 'More' nav_menu")
11+
12+
with ui.navset_tab(id="tabs"):
13+
with ui.nav_panel("Foo", value="Foo"):
14+
"This is the foo tab"
15+
with ui.nav_panel("Bar", value="Bar"):
16+
"This is the bar tab"
17+
with ui.nav_menu(title="More", value="More"):
18+
with ui.nav_panel("Table"):
19+
"Table page"
20+
with ui.nav_panel("About"):
21+
"About page"
22+
"------"
23+
"Even more!"
24+
with ui.nav_panel("Email"):
25+
"Email page"
26+
27+
@reactive.effect
28+
@reactive.event(input.hideTab)
29+
def _():
30+
ui.update_nav_panel("tabs", target="Foo", method="hide")
31+
32+
@reactive.effect
33+
@reactive.event(input.showTab)
34+
def _():
35+
ui.update_nav_panel("tabs", target="Foo", method="show")
36+
37+
@reactive.effect
38+
@reactive.event(input.hideMenu)
39+
def _():
40+
ui.update_nav_panel("tabs", target="More", method="hide")
41+
42+
@reactive.effect
43+
@reactive.event(input.showMenu)
44+
def _():
45+
ui.update_nav_panel("tabs", target="More", method="show")

shiny/express/ui/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@
109109
notification_remove,
110110
nav_spacer,
111111
navbar_options,
112+
remove_nav_panel,
113+
update_nav_panel,
112114
Progress,
113115
Theme,
114116
value_box_theme,
@@ -161,6 +163,10 @@
161163
hold,
162164
)
163165

166+
from ._insert import (
167+
insert_nav_panel,
168+
)
169+
164170
__all__ = (
165171
# Imports from htmltools
166172
"TagList",
@@ -289,6 +295,9 @@
289295
"navset_hidden",
290296
"navset_pill",
291297
"navset_pill_list",
298+
"update_nav_panel",
299+
"insert_nav_panel",
300+
"remove_nav_panel",
292301
"navset_tab",
293302
"navset_underline",
294303
"navbar_options",

shiny/express/ui/_insert.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""
2+
Shims for `ui.insert_*()`, `ui.update_*()`, etc. functions that lead to a more ergonomic
3+
Express API.
4+
These functions tend to have one issue in common: if they were re-exported verbatim from
5+
Core to Express, they would want to take RecallContextManager(s) as input, which leads
6+
to a somewhat awkward API. That's because, you'd have to know to use something like
7+
@ui.hold() pass the UI as a value without displaying it.
8+
"""
9+
10+
from typing import Literal, Optional
11+
12+
from htmltools import TagChild
13+
14+
from ..._docstring import add_example
15+
from ...session import Session
16+
17+
18+
@add_example()
19+
def insert_nav_panel(
20+
id: str,
21+
title: TagChild,
22+
*args: TagChild,
23+
value: Optional[str] = None,
24+
icon: TagChild = None,
25+
target: Optional[str] = None,
26+
position: Literal["after", "before"] = "after",
27+
select: bool = False,
28+
session: Optional[Session] = None,
29+
) -> None:
30+
"""
31+
Create a new nav panel in an existing navset.
32+
33+
Parameters
34+
----------
35+
id
36+
The id of the navset container to insert into.
37+
title
38+
A title for the inserted nav panel. Can be a character string or UI elements (i.e., tags).
39+
*args
40+
UI elements for the inserted nav panel.
41+
value
42+
The value of the panel. Use this value to determine whether the panel is active
43+
(when an `id` is provided to the nav container) or to programmatically
44+
select the item (e.g., :func:`~shiny.ui.update_navs`). You can also
45+
provide the value to the `selected` argument of the navigation container
46+
(e.g., :func:`~shiny.ui.navset_tab`).
47+
icon
48+
An icon to appear inline with the title.
49+
target
50+
The `value` of an existing :func:`shiny.ui.nav_panel`, next to which tab will
51+
be added. Can also be `None`; see `position`.
52+
position
53+
The position of the new nav panel relative to the target. If
54+
`target=None`, then `"before"` means the new panel should be inserted at
55+
the head of the navlist, and `"after"` is the end.
56+
select
57+
Whether the nav panel should be selected upon insertion.
58+
session
59+
A :class:`~shiny.Session` instance. If not provided, it is inferred via
60+
:func:`~shiny.session.get_current_session`.
61+
62+
Note
63+
----
64+
Unlike :func:`~shiny.ui.insert_nav_panel`, this function does not support inserting
65+
of a heading/divider into an existing :func:`~shiny.ui.nav_menu`. To do so, use
66+
:func:`~shiny.ui.insert_nav_panel` instead of this Express variant (i.e.,
67+
`shiny.ui.insert_nav_panel("id", "Header")`).
68+
"""
69+
70+
from ...ui import insert_nav_panel, nav_panel
71+
72+
panel = nav_panel(
73+
title,
74+
*args,
75+
value=value,
76+
icon=icon,
77+
)
78+
79+
insert_nav_panel(
80+
id=id,
81+
nav_panel=panel,
82+
target=target,
83+
position=position,
84+
select=select,
85+
session=session,
86+
)

shiny/ui/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@
128128
navset_tab,
129129
navset_underline,
130130
)
131+
from ._navs_dynamic import (
132+
update_nav_panel,
133+
insert_nav_panel,
134+
remove_nav_panel,
135+
)
131136
from ._notification import notification_remove, notification_show
132137
from ._output import (
133138
output_code,
@@ -297,6 +302,9 @@
297302
"navset_pill_list",
298303
"navset_hidden",
299304
"navset_bar",
305+
"insert_nav_panel",
306+
"remove_nav_panel",
307+
"update_nav_panel",
300308
"navbar_options",
301309
# _notification
302310
"notification_show",

0 commit comments

Comments
 (0)