Skip to content

Commit e5b2542

Browse files
committed
Implement dynamic navs (i.e., nav_insert(), nav_remove(), nav_show(), nav_hide())
1 parent e09bd1e commit e5b2542

File tree

7 files changed

+320
-1
lines changed

7 files changed

+320
-1
lines changed

docs/source/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ Create segments of UI content.
136136
ui.navs_pill
137137
ui.navs_pill_card
138138
ui.navs_pill_list
139+
ui.nav_insert
140+
ui.nav_remove
141+
ui.nav_show
142+
ui.nav_hide
139143

140144

141145
UI panels

shiny/_modules.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
__all__ = ("Module",)
22

3-
from typing import Any, Callable, Optional
3+
from typing import Any, Callable, Optional, Dict
44

55
from htmltools import TagChildArg
66

@@ -117,6 +117,15 @@ def __init__(self, ns: str, parent_session: Session) -> None:
117117
def __getattr__(self, attr: str) -> Any:
118118
return getattr(self._parent, attr)
119119

120+
def send_input_message(self, id: str, message: Dict[str, object]) -> None:
121+
return super().send_input_message(self.ns(id), message)
122+
123+
def ns(self, id: Optional[str] = None) -> str:
124+
if id is None:
125+
return self._ns
126+
else:
127+
return self._ns + "-" + id
128+
120129

121130
@add_example()
122131
class Module:

shiny/examples/nav_insert/app.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from shiny import *
2+
3+
app_ui = ui.page_fluid(
4+
ui.layout_sidebar(
5+
ui.panel_sidebar(
6+
ui.input_action_button("add", "Add 'Dynamic' tab"),
7+
ui.input_action_button("removeFoo", "Remove 'Foo' tabs"),
8+
ui.input_action_button("addFoo", "Add New 'Foo' tab"),
9+
),
10+
ui.panel_main(
11+
ui.navs_tab(
12+
ui.nav("Hello", "This is the hello tab"),
13+
ui.nav("Foo", "This is the Foo tab", value="Foo"),
14+
ui.nav_menu(
15+
"Static",
16+
ui.nav("Static 1", "Static 1", value="s1"),
17+
ui.nav("Static 2", "Static 2", value="s2"),
18+
value="Menu",
19+
),
20+
id="tabs",
21+
),
22+
),
23+
)
24+
)
25+
26+
27+
def server(input: Inputs, output: Outputs, session: Session):
28+
@reactive.Effect()
29+
@event(input.add)
30+
def _():
31+
id = "Dynamic-" + str(input.add())
32+
ui.nav_insert(
33+
"tabs",
34+
ui.nav(id, id),
35+
target="s2",
36+
position="before",
37+
)
38+
39+
@reactive.Effect()
40+
@event(input.removeFoo)
41+
def _():
42+
ui.nav_remove("tabs", target="Foo")
43+
44+
@reactive.Effect()
45+
@event(input.addFoo)
46+
def _():
47+
n = str(input.addFoo())
48+
ui.nav_insert(
49+
"tabs",
50+
ui.nav("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"),
51+
target="Menu",
52+
position="before",
53+
select=True,
54+
)
55+
56+
57+
app = App(app_ui, server, debug=True)

shiny/examples/nav_show/app.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from shiny import *
2+
3+
app_ui = ui.page_navbar(
4+
ui.nav(
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.nav("Foo", "This is the foo tab"),
12+
ui.nav("Bar", "This is the bar tab"),
13+
ui.nav_menu(
14+
"More",
15+
ui.nav("Table", "Table page"),
16+
ui.nav("About", "About page"),
17+
"------",
18+
"Even more!",
19+
ui.nav("Email", "Email page"),
20+
),
21+
title="Navbar page",
22+
id="tabs",
23+
)
24+
25+
26+
def server(input: Inputs, output: Outputs, session: Session):
27+
@reactive.Effect()
28+
@event(input.hideTab)
29+
def _():
30+
ui.nav_hide("tabs", target="Foo")
31+
32+
@reactive.Effect()
33+
@event(input.showTab)
34+
def _():
35+
ui.nav_show("tabs", target="Foo")
36+
37+
@reactive.Effect()
38+
@event(input.hideMenu)
39+
def _():
40+
ui.nav_hide("tabs", target="More")
41+
42+
@reactive.Effect()
43+
@event(input.showMenu)
44+
def _():
45+
ui.nav_show("tabs", target="More")
46+
47+
48+
app = App(app_ui, server)

shiny/session/_session.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,9 @@ def _process_ui(self, ui: TagChildArg) -> RenderedDeps:
670670

671671
return {"deps": deps, "html": res["html"]}
672672

673+
def ns(self, id: Optional[str] = None) -> Optional[str]:
674+
return id
675+
673676

674677
# ======================================================================================
675678
# Inputs

shiny/ui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from ._markdown import *
2121
from ._modal import *
2222
from ._navs import *
23+
from ._navs_dynamic import *
2324
from ._notification import *
2425
from ._output import *
2526
from ._page import *

shiny/ui/_navs_dynamic.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
__all__ = (
2+
"nav_insert",
3+
"nav_remove",
4+
"nav_hide",
5+
"nav_show",
6+
)
7+
8+
import sys
9+
from typing import Optional, Union
10+
11+
if sys.version_info >= (3, 8):
12+
from typing import Literal
13+
else:
14+
from typing_extensions import Literal
15+
16+
from .._docstring import add_example
17+
from ._input_update import update_navs
18+
from ._navs import menu_string_as_nav
19+
from ..types import NavsArg
20+
from ..session import Session, require_active_session
21+
from .._utils import run_coro_sync
22+
23+
24+
@add_example()
25+
def nav_insert(
26+
id: str,
27+
nav: Union[NavsArg, str],
28+
target: Optional[str] = None,
29+
position: Literal["after", "before"] = "after",
30+
select: bool = False,
31+
session: Optional[Session] = None,
32+
) -> None:
33+
"""
34+
Insert a new nav item into a navigation container.
35+
36+
Parameters
37+
----------
38+
id
39+
The ``id`` of the relevant navigation container (i.e., ``navs_*()`` object).
40+
nav
41+
The navigation item to insert (typically a :func:`~shiny.ui.nav` or
42+
:func:`~shiny.ui.nav_menu`). A :func:`~shiny.ui.nav_menu` isn't allowed when the
43+
``target`` references an :func:`~shiny.ui.nav_menu` (or an item within it). A
44+
string is only allowed when the ``target`` references a
45+
:func:`~shiny.ui.nav_menu`.
46+
target
47+
The ``value`` of an existing :func:`shiny.ui.nav` item, next to which tab will
48+
be added.
49+
position
50+
The position of the new nav item relative to the target nav item.
51+
select
52+
Whether the nav item should be selected upon insertion.
53+
session
54+
A :class:`~shiny.Session` instance. If not provided, it is inferred via
55+
:func:`~shiny.session.get_current_session`.
56+
57+
See Also
58+
--------
59+
~nav_remove
60+
~nav_show
61+
~nav_hide
62+
~shiny.ui.nav
63+
"""
64+
65+
session = require_active_session(session)
66+
67+
# N.B. this is only sensible if the target is a menu, but we don't know that,
68+
# which could cause confusion of we decide to support top-level strings at some
69+
# in the future.
70+
if isinstance(nav, str):
71+
nav = menu_string_as_nav(nav)
72+
73+
# N.B. shiny.js' is smart enough to know how to add active classes and href/id attrs
74+
li_tag, div_tag = nav.resolve(selected=None)
75+
76+
msg = {
77+
"inputId": session.ns(id),
78+
"liTag": session._process_ui(li_tag),
79+
"divTag": session._process_ui(div_tag),
80+
"menuName": None,
81+
"target": target,
82+
"position": position,
83+
"select": select,
84+
}
85+
86+
def callback() -> None:
87+
run_coro_sync(session._send_message({"shiny-insert-tab": msg}))
88+
89+
session.on_flush(callback, once=True)
90+
91+
92+
def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None:
93+
"""
94+
Remove a nav item from a navigation container.
95+
96+
Parameters
97+
----------
98+
id
99+
The ``id`` of the relevant navigation container (i.e., ``navs_*()`` object).
100+
target
101+
The ``value`` of an existing :func:`shiny.ui.nav` item to remove.
102+
session
103+
A :class:`~shiny.Session` instance. If not provided, it is inferred via
104+
:func:`~shiny.session.get_current_session`.
105+
106+
See Also
107+
--------
108+
~nav_insert
109+
~nav_show
110+
~nav_hide
111+
~shiny.ui.nav
112+
"""
113+
114+
session = require_active_session(session)
115+
116+
msg = {"inputId": session.ns(id), "target": target}
117+
118+
def callback() -> None:
119+
run_coro_sync(session._send_message({"shiny-remove-tab": msg}))
120+
121+
session.on_flush(callback, once=True)
122+
123+
124+
def nav_show(
125+
id: str, target: str, select: bool = False, session: Optional[Session] = None
126+
) -> None:
127+
"""
128+
Show a navigation item
129+
130+
Parameters
131+
----------
132+
id
133+
The ``id`` of the relevant navigation container (i.e., ``navs_*()`` object).
134+
target
135+
The ``value`` of an existing :func:`shiny.ui.nav` item to show.
136+
select
137+
Whether the nav item's content should also be shown.
138+
session
139+
A :class:`~shiny.Session` instance. If not provided, it is inferred via
140+
:func:`~shiny.session.get_current_session`.
141+
142+
Note
143+
----
144+
For ``nav_show()`` to be relevant/useful, a :func:`shiny.ui.nav` item must
145+
have been hidden using :func:`~nav_hide`.
146+
147+
See Also
148+
--------
149+
~nav_hide
150+
~nav_insert
151+
~nav_remove
152+
~shiny.ui.nav
153+
"""
154+
155+
session = require_active_session(session)
156+
157+
if select:
158+
update_navs(id, selected=target)
159+
160+
msg = {"inputId": session.ns(id), "target": target, "type": "show"}
161+
162+
def callback() -> None:
163+
run_coro_sync(session._send_message({"shiny-change-tab-visibility": msg}))
164+
165+
session.on_flush(callback, once=True)
166+
167+
168+
def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None:
169+
"""
170+
Hide a navigation item
171+
172+
Parameters
173+
----------
174+
id
175+
The ``id`` of the relevant navigation container (i.e., ``navs_*()`` object).
176+
target
177+
The ``value`` of an existing :func:`shiny.ui.nav` item to hide.
178+
session
179+
A :class:`~shiny.Session` instance. If not provided, it is inferred via
180+
:func:`~shiny.session.get_current_session`.
181+
182+
See Also
183+
--------
184+
~nav_show
185+
~nav_insert
186+
~nav_remove
187+
~shiny.ui.nav
188+
"""
189+
190+
session = require_active_session(session)
191+
192+
msg = {"inputId": session.ns(id), "target": target, "type": "hide"}
193+
194+
def callback() -> None:
195+
run_coro_sync(session._send_message({"shiny-change-tab-visibility": msg}))
196+
197+
session.on_flush(callback, once=True)

0 commit comments

Comments
 (0)