Skip to content

Commit 942604f

Browse files
committed
wip implement dynamic navs
1 parent ba5e120 commit 942604f

File tree

15 files changed

+785
-807
lines changed

15 files changed

+785
-807
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.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from .._docstring import add_example
2727
from ._html_dependencies import nav_deps
28+
from .._utils import drop_none
2829

2930

3031
@add_example()
@@ -678,4 +679,4 @@ def navs_bar(
678679

679680
def _nav_tag(name: str, *args: TagChildArg, **kwargs: JSXTagAttrArg) -> JSXTag:
680681
tag = jsx_tag_create("bslib." + name)
681-
return tag(nav_deps(), *args, **kwargs)
682+
return tag(nav_deps(), *args, **drop_none(kwargs))

shiny/ui/_navs_dynamic.py

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

shiny/www/shared/bslib/dist/navs.min.js

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shiny/www/shared/bslib/dist/navs.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)