Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement dynamic navs #90

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
2 changes: 1 addition & 1 deletion shiny/session/_session.py
Expand Up @@ -561,7 +561,7 @@ def send_input_message(self, id: str, message: dict[str, object]) -> None:
message
The message to send.
"""
msg: dict[str, object] = {"id": id, "message": message}
msg: dict[str, object] = {"id": self.ns(id), "message": message}
self._outbound_message_queues["input_messages"].append(msg)
self._request_flush()

Expand Down
10 changes: 10 additions & 0 deletions shiny/ui/__init__.py
Expand Up @@ -66,6 +66,12 @@
navset_hidden,
navset_bar,
)
from ._navs_dynamic import (
nav_hide,
nav_insert,
nav_remove,
nav_show,
)
from ._notification import notification_show, notification_remove
from ._output import (
output_plot,
Expand Down Expand Up @@ -179,6 +185,10 @@
"navset_pill_list",
"navset_hidden",
"navset_bar",
"nav_hide",
"nav_insert",
"nav_remove",
"nav_show",
"notification_show",
"notification_remove",
"output_plot",
Expand Down
203 changes: 203 additions & 0 deletions shiny/ui/_navs_dynamic.py
@@ -0,0 +1,203 @@
__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 .._namespaces import resolve_id
from .._utils import run_coro_sync
from ..session import Session, require_active_session
from ..types import NavSetArg
from ._input_update import update_navs
from ._navs import menu_string_as_nav


@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. Can also be ``None``; see ``position``.
position
The position of the new nav item relative to the target nav item. If
``target=None``, then ``"before"`` means the new nav item should be inserted at
the head of the navlist, and ``"after"`` is the end.
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, context=dict(tabsetid="tsid", index="id")
)

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

id = resolve_id(id)
if select:
update_navs(id, selected=target)

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