Skip to content

Commit 94dab30

Browse files
committed
wip implement dynamic navs
1 parent c8c9190 commit 94dab30

File tree

13 files changed

+778
-806
lines changed

13 files changed

+778
-806
lines changed

docs/source/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ Create segments of UI content.
8989
ui.navs_pill
9090
ui.navs_pill_card
9191
ui.navs_pill_list
92+
ui.nav_insert
93+
ui.nav_remove
94+
ui.nav_show
95+
ui.nav_hide
9296

9397

9498
UI panels

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/ui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ._insert import *
2020
from ._modal import *
2121
from ._navs import *
22+
from ._navs_dynamic import *
2223
from ._notification import *
2324
from ._output import *
2425
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: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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+
# TODO: do we need to namespace the id?
65+
# id = session.ns(id)
66+
67+
# The currrent JSX implementation of nav items is not smart enough to
68+
# to render without a navs container (maybe it could?), so we need to
69+
# wrap in one and also notify the JSX logic to not select the nav
70+
# (shiny.js handles that part via the select parameter).
71+
jsx_tag = navs_hidden(nav, selected=False) # type: ignore
72+
73+
msg = {
74+
"inputId": 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+
# TODO: do we need to namespace the id?
112+
# id = session.ns(id)
113+
114+
msg = {"inputId": id, "target": target}
115+
116+
def callback() -> None:
117+
run_coro_sync(session._send_message({"shiny-remove-tab": msg}))
118+
119+
session.on_flush(callback, once=True)
120+
121+
122+
def nav_show(
123+
id: str, target: str, select: bool = False, session: Optional[Session] = None
124+
) -> None:
125+
"""
126+
Show a navigation item
127+
128+
Parameters
129+
----------
130+
id
131+
The ``id`` of the relevant navigation container (i.e., ``navs_*()`` object).
132+
target
133+
The ``value`` of an existing :func:`shiny.ui.nav` item to show.
134+
select
135+
Whether the nav item's content should also be shown.
136+
session
137+
A :class:`~shiny.Session` instance. If not provided, it is inferred via
138+
:func:`~shiny.session.get_current_session`.
139+
140+
Note
141+
----
142+
For ``nav_show()`` to be relevant/useful, a :func:`shiny.ui.nav` item must
143+
have been hidden using :func:`~nav_hide`.
144+
145+
See Also
146+
--------
147+
~nav_hide
148+
~nav_insert
149+
~nav_remove
150+
~shiny.ui.nav
151+
"""
152+
153+
session = require_active_session(session)
154+
155+
if select:
156+
update_navs(id, selected=target)
157+
158+
# TODO: do we need to namespace the id?
159+
# id = session.ns(id)
160+
msg = {"inputId": 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+
# TODO: do we need to namespace the id?
192+
# id = session.ns(id)
193+
194+
msg = {"inputId": id, "target": target, "type": "hide"}
195+
196+
def callback() -> None:
197+
run_coro_sync(session._send_message({"shiny-change-tab-visibility": msg}))
198+
199+
session.on_flush(callback, once=True)

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

Lines changed: 3 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)