Skip to content

Commit

Permalink
Implement dynamic navs (i.e., nav_insert(), nav_remove(), nav_show(),…
Browse files Browse the repository at this point in the history
… nav_hide())
  • Loading branch information
cpsievert committed Apr 27, 2022
1 parent 6d5357a commit affed80
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 1 deletion.
4 changes: 4 additions & 0 deletions docs/source/index.rst
Expand Up @@ -136,6 +136,10 @@ Create segments of UI content.
ui.navset_pill
ui.navset_pill_card
ui.navset_pill_list
ui.nav_insert
ui.nav_remove
ui.nav_show
ui.nav_hide


UI panels
Expand Down
11 changes: 10 additions & 1 deletion shiny/_modules.py
@@ -1,6 +1,6 @@
__all__ = ("Module",)

from typing import Any, Callable, Optional
from typing import Any, Callable, Optional, Dict

from htmltools import TagChildArg

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

def send_input_message(self, id: str, message: Dict[str, object]) -> None:
return super().send_input_message(self.ns(id), message)

def ns(self, id: Optional[str] = None) -> str:
if id is None:
return self._ns
else:
return self._ns + "-" + id


@add_example()
class Module:
Expand Down
57 changes: 57 additions & 0 deletions shiny/examples/nav_insert/app.py
@@ -0,0 +1,57 @@
from shiny import *

app_ui = ui.page_fluid(
ui.layout_sidebar(
ui.panel_sidebar(
ui.input_action_button("add", "Add 'Dynamic' tab"),
ui.input_action_button("removeFoo", "Remove 'Foo' tabs"),
ui.input_action_button("addFoo", "Add New 'Foo' tab"),
),
ui.panel_main(
ui.navset_tab(
ui.nav("Hello", "This is the hello tab"),
ui.nav("Foo", "This is the Foo tab", value="Foo"),
ui.nav_menu(
"Static",
ui.nav("Static 1", "Static 1", value="s1"),
ui.nav("Static 2", "Static 2", value="s2"),
value="Menu",
),
id="tabs",
),
),
)
)


def server(input: Inputs, output: Outputs, session: Session):
@reactive.Effect()
@event(input.add)
def _():
id = "Dynamic-" + str(input.add())
ui.nav_insert(
"tabs",
ui.nav(id, id),
target="s2",
position="before",
)

@reactive.Effect()
@event(input.removeFoo)
def _():
ui.nav_remove("tabs", target="Foo")

@reactive.Effect()
@event(input.addFoo)
def _():
n = str(input.addFoo())
ui.nav_insert(
"tabs",
ui.nav("Foo-" + n, "This is the new Foo-" + n + " tab", value="Foo"),
target="Menu",
position="before",
select=True,
)


app = App(app_ui, server)
48 changes: 48 additions & 0 deletions shiny/examples/nav_show/app.py
@@ -0,0 +1,48 @@
from shiny import *

app_ui = ui.page_navbar(
ui.nav(
"Home",
ui.input_action_button("hideTab", "Hide 'Foo' tab"),
ui.input_action_button("showTab", "Show 'Foo' tab"),
ui.input_action_button("hideMenu", "Hide 'More' nav_menu"),
ui.input_action_button("showMenu", "Show 'More' nav_menu"),
),
ui.nav("Foo", "This is the foo tab"),
ui.nav("Bar", "This is the bar tab"),
ui.nav_menu(
"More",
ui.nav("Table", "Table page"),
ui.nav("About", "About page"),
"------",
"Even more!",
ui.nav("Email", "Email page"),
),
title="Navbar page",
id="tabs",
)


def server(input: Inputs, output: Outputs, session: Session):
@reactive.Effect()
@event(input.hideTab)
def _():
ui.nav_hide("tabs", target="Foo")

@reactive.Effect()
@event(input.showTab)
def _():
ui.nav_show("tabs", target="Foo")

@reactive.Effect()
@event(input.hideMenu)
def _():
ui.nav_hide("tabs", target="More")

@reactive.Effect()
@event(input.showMenu)
def _():
ui.nav_show("tabs", target="More")


app = App(app_ui, server)
3 changes: 3 additions & 0 deletions shiny/session/_session.py
Expand Up @@ -670,6 +670,9 @@ def _process_ui(self, ui: TagChildArg) -> RenderedDeps:

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

def ns(self, id: Optional[str] = None) -> Optional[str]:
return id


# ======================================================================================
# Inputs
Expand Down
1 change: 1 addition & 0 deletions shiny/ui/__init__.py
Expand Up @@ -20,6 +20,7 @@
from ._markdown import *
from ._modal import *
from ._navs import *
from ._navs_dynamic import *
from ._notification import *
from ._output import *
from ._page import *
Expand Down
197 changes: 197 additions & 0 deletions shiny/ui/_navs_dynamic.py
@@ -0,0 +1,197 @@
__all__ = (
"nav_insert",
"nav_remove",
"nav_hide",
"nav_show",
)

import sys
from typing import Optional, Union

if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal

from .._docstring import add_example
from ._input_update import update_navs
from ._navs import menu_string_as_nav
from ..types import NavSetArg
from ..session import Session, require_active_session
from .._utils import run_coro_sync


@add_example()
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.
position
The position of the new nav item relative to the target nav item.
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`.
See Also
--------
~nav_remove
~nav_show
~nav_hide
~shiny.ui.nav
"""

session = require_active_session(session)

# N.B. this is only sensible if the target is a menu, but we don't know that,
# which could cause confusion of we decide to support top-level strings at some
# in the future.
if isinstance(nav, str):
nav = menu_string_as_nav(nav)

# N.B. shiny.js' is smart enough to know how to add active classes and href/id attrs
li_tag, div_tag = nav.resolve(selected=None)

msg = {
"inputId": session.ns(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)


def nav_remove(id: str, target: str, session: Optional[Session] = None) -> None:
"""
Remove a nav item from a navigation container.
Parameters
----------
id
The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object).
target
The ``value`` of an existing :func:`shiny.ui.nav` item to remove.
session
A :class:`~shiny.Session` instance. If not provided, it is inferred via
:func:`~shiny.session.get_current_session`.
See Also
--------
~nav_insert
~nav_show
~nav_hide
~shiny.ui.nav
"""

session = require_active_session(session)

msg = {"inputId": session.ns(id), "target": target}

def callback() -> None:
run_coro_sync(session._send_message({"shiny-remove-tab": msg}))

session.on_flush(callback, once=True)


def nav_show(
id: str, target: str, select: bool = False, session: Optional[Session] = None
) -> None:
"""
Show a navigation item
Parameters
----------
id
The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object).
target
The ``value`` of an existing :func:`shiny.ui.nav` item to show.
select
Whether the nav item's content should also be shown.
session
A :class:`~shiny.Session` instance. If not provided, it is inferred via
:func:`~shiny.session.get_current_session`.
Note
----
For ``nav_show()`` to be relevant/useful, a :func:`shiny.ui.nav` item must
have been hidden using :func:`~nav_hide`.
See Also
--------
~nav_hide
~nav_insert
~nav_remove
~shiny.ui.nav
"""

session = require_active_session(session)

if select:
update_navs(id, selected=target)

msg = {"inputId": session.ns(id), "target": target, "type": "show"}

def callback() -> None:
run_coro_sync(session._send_message({"shiny-change-tab-visibility": msg}))

session.on_flush(callback, once=True)


def nav_hide(id: str, target: str, session: Optional[Session] = None) -> None:
"""
Hide a navigation item
Parameters
----------
id
The ``id`` of the relevant navigation container (i.e., ``navset_*()`` object).
target
The ``value`` of an existing :func:`shiny.ui.nav` item to hide.
session
A :class:`~shiny.Session` instance. If not provided, it is inferred via
:func:`~shiny.session.get_current_session`.
See Also
--------
~nav_show
~nav_insert
~nav_remove
~shiny.ui.nav
"""

session = require_active_session(session)

msg = {"inputId": session.ns(id), "target": target, "type": "hide"}

def callback() -> None:
run_coro_sync(session._send_message({"shiny-change-tab-visibility": msg}))

session.on_flush(callback, once=True)

0 comments on commit affed80

Please sign in to comment.