Skip to content

Commit

Permalink
wip implement dynamic navs
Browse files Browse the repository at this point in the history
  • Loading branch information
cpsievert committed Mar 3, 2022
1 parent c8c9190 commit e3f139c
Show file tree
Hide file tree
Showing 15 changed files with 788 additions and 807 deletions.
4 changes: 4 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ Create segments of UI content.
ui.navs_pill
ui.navs_pill_card
ui.navs_pill_list
ui.nav_insert
ui.nav_remove
ui.nav_show
ui.nav_hide


UI panels
Expand Down
57 changes: 57 additions & 0 deletions shiny/examples/nav_insert/app.py
Original file line number Diff line number Diff line change
@@ -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.navs_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, debug=True)
48 changes: 48 additions & 0 deletions shiny/examples/nav_show/app.py
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 13 additions & 1 deletion shiny/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"Module",
)

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

from htmltools import TagChildArg

Expand Down Expand Up @@ -113,6 +113,18 @@ def __init__(self, ns: str, parent_session: Session) -> None:
self.input: ModuleInputs = ModuleInputs(ns, parent_session.input)
self.output: ModuleOutputs = ModuleOutputs(ns, parent_session.output)

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
3 changes: 3 additions & 0 deletions shiny/session/_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from ._insert import *
from ._modal import *
from ._navs import *
from ._navs_dynamic import *
from ._notification import *
from ._output import *
from ._page import *
Expand Down
3 changes: 2 additions & 1 deletion shiny/ui/_navs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from .._docstring import add_example
from ._html_dependencies import nav_deps
from .._utils import drop_none


@add_example()
Expand Down Expand Up @@ -678,4 +679,4 @@ def navs_bar(

def _nav_tag(name: str, *args: TagChildArg, **kwargs: JSXTagAttrArg) -> JSXTag:
tag = jsx_tag_create("bslib." + name)
return tag(nav_deps(), *args, **kwargs)
return tag(nav_deps(), *args, **drop_none(kwargs))
193 changes: 193 additions & 0 deletions shiny/ui/_navs_dynamic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
__all__ = (
"nav_insert",
"nav_remove",
"nav_hide",
"nav_show",
)

import sys
from typing import Optional

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

from htmltools import JSXTag

from .._docstring import add_example
from ._input_update import update_navs
from ._navs import navs_hidden
from ..session import Session, require_active_session
from .._utils import run_coro_sync


@add_example()
def nav_insert(
id: str,
nav: JSXTag,
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., ``navs_*()`` object).
nav
The navigation item to insert (typically a :func:`shiny.ui.nav` or
: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)

# The currrent JSX implementation of nav items is not smart enough to know how to
# render without a navs container (maybe it could, but I don't think that'd simplify
# things overall), so we wrap in one and also notify the JSX logic to not generate
# active classes in the HTML markup (shiny.js handles that part via the select
# parameter in the message). This way, the shiny.js logic can just render the JSXTag
# verbatim and use the historical HTML code path to insert the nav item.
jsx_tag = navs_hidden(nav, selected=False) # type: ignore

msg = {
"inputId": session.ns(id),
"menuName": None,
"target": target,
"position": position,
"select": select,
"jsxTag": session._process_ui(jsx_tag),
}

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., ``navs_*()`` 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., ``navs_*()`` 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., ``navs_*()`` 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)
5 changes: 4 additions & 1 deletion shiny/www/shared/bslib/dist/navs.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion shiny/www/shared/bslib/dist/navs.min.js.map

Large diffs are not rendered by default.

0 comments on commit e3f139c

Please sign in to comment.