Skip to content

Commit

Permalink
Implement/improve nav() API by implementing in Python instead of Reac…
Browse files Browse the repository at this point in the history
…t/JSX (#136)

* Switch from React/JSX to Python for constructing nav() markup

* code review

* No more need for bs3compat.js (or downloading JSX implementation)

* Add unit tests and fix some bugs

* Use a Protocol for typing navigation items inside consumers

This way others can create their own custom nav item classes

* No need to import pytest

* add comment; fix type

* Use a more general context object to pass info between parent/child relationships

* Rename navs_*() to navset_*(); add a dedicated class for containers with a tagify() method (so we can add other useful methods in the future); add a tagify() method to Nav/NavMenu as well that raises informative error

* Remove nav_content() from the API

* Rename nav_item() to nav_control()

* Rename directory

* Bump version to signify breaking changes

* Leverage inheritance to stor additional layout parameters
  • Loading branch information
cpsievert committed Apr 28, 2022
1 parent 4b81a12 commit d2e3fb9
Show file tree
Hide file tree
Showing 22 changed files with 895 additions and 3,363 deletions.
12 changes: 6 additions & 6 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,14 @@ Create segments of UI content.
:toctree: reference/

ui.nav
ui.nav_item
ui.nav_control
ui.nav_spacer
ui.nav_menu
ui.navs_tab
ui.navs_tab_card
ui.navs_pill
ui.navs_pill_card
ui.navs_pill_list
ui.navset_tab
ui.navset_tab_card
ui.navset_pill
ui.navset_pill_card
ui.navset_pill_list


UI panels
Expand Down
2 changes: 1 addition & 1 deletion examples/event/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
print the number of clicks in the console twice.
"""
),
ui.navs_tab_card(
ui.navset_tab_card(
ui.nav(
"Sync",
ui.input_action_button("btn", "Click me"),
Expand Down
2 changes: 1 addition & 1 deletion examples/inputs-update/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
multiple=True,
),
),
ui.navs_tab(
ui.navset_tab(
ui.nav("panel1", h2("This is the first panel.")),
ui.nav("panel2", h2("This is the second panel.")),
id="inTabset",
Expand Down
2 changes: 1 addition & 1 deletion examples/inputs/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
),
ui.panel_main(
ui.output_plot("plot"),
ui.navs_tab_card(
ui.navset_tab_card(
# TODO: output_plot() within a tab not working?
ui.nav("Inputs", ui.output_ui("inputs"), icon=icon_svg("code")),
ui.nav(
Expand Down
4 changes: 2 additions & 2 deletions parity.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ output: html_document

## Tabs

`tabsetPanel()` -> `navs_tab()`/`navs_pill()`
`navlistPanel()` -> `navs_pill_list()`
`tabsetPanel()` -> `navset_tab()`/`navset_pill()`
`navlistPanel()` -> `navset_pill_list()`
`tabPanel()` -> `nav()`
`navbarMenu()` -> `nav_menu()`
`tabPanelBody()` -> `navs_content()`
Expand Down
17 changes: 6 additions & 11 deletions scripts/htmlDependencies.R
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,9 @@ withr::with_options(
lapply(deps, copyDependencyToDir, "shiny/www/shared")
)

# For JSX based nav() implementation
bslib <- file.path(www, "shared", "bslib")
dir.create(bslib)
withr::with_tempdir({
cmd <- paste("git clone --depth 1 --branch jsx https://github.com/rstudio/bslib")
system(cmd)
file.copy(
"bslib/inst/navs/dist",
bslib, recursive = TRUE
)
})
# This additional bs3compat HTMLDependency() only holds
# the JS shim for tab panel logic, which we don't need
# since we're generating BS5+ tab markup. Note, however,
# we still do have bs3compat's CSS on the page, which
# comes in via the bootstrap HTMLDependency()
unlink("shiny/www/shared/bs3compat/", recursive = TRUE)
2 changes: 1 addition & 1 deletion shiny/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""A package for building reactive web applications."""

__version__ = "0.2.0.9001"
__version__ = "0.2.0.9002"

from ._shinyenv import is_pyodide as _is_pyodide

Expand Down
37 changes: 30 additions & 7 deletions shiny/_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import contextlib
import functools
import importlib
import inspect
import os
import random
import secrets
import sys
import tempfile

from typing import (
Callable,
Awaitable,
Expand All @@ -8,13 +18,6 @@
Any,
cast,
)
import functools
import os
import sys
import tempfile
import importlib
import inspect
import secrets

if sys.version_info >= (3, 10):
from typing import TypeGuard
Expand All @@ -33,6 +36,26 @@ def rand_hex(bytes: int) -> str:
return format_str.format(secrets.randbits(bytes * 8))


def private_random_int(min: int, max: int) -> str:
with private_seed():
return str(random.randint(min, max))


@contextlib.contextmanager
def private_seed():
state = random.getstate()
global own_random_state
try:
if own_random_state is not None:
random.setstate(own_random_state)
yield
finally:
own_random_state = random.getstate()
random.setstate(state)


own_random_state = None

# ==============================================================================
# Async-related functions
# ==============================================================================
Expand Down
40 changes: 24 additions & 16 deletions shiny/examples/nav/app.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from typing import List

from shiny import *
from htmltools import JSXTag, h4
from shiny.types import NavSetArg
from shiny.ui import h4
from fontawesome import icon_svg as icon


def nav_items(prefix: str) -> List[JSXTag]:
def nav_controls(prefix: str) -> List[NavSetArg]:
return [
ui.nav("a", prefix + ": tab a content"),
ui.nav("b", prefix + ": tab b content"),
ui.nav_item(
ui.nav_control(
ui.a(
icon("github"),
"Shiny",
Expand All @@ -21,7 +22,10 @@ def nav_items(prefix: str) -> List[JSXTag]:
ui.nav_menu(
"Other links",
ui.nav("c", prefix + ": tab c content"),
ui.nav_item(
"----",
"Plain text",
"----",
ui.nav_control(
ui.a(
icon("r-project"),
"RStudio",
Expand All @@ -35,28 +39,32 @@ def nav_items(prefix: str) -> List[JSXTag]:


app_ui = ui.page_navbar(
*nav_items("page_navbar"),
*nav_controls("page_navbar"),
title="page_navbar()",
bg="#0062cc",
inverse=True,
id="navbar_id",
footer=ui.div(
{"style": "width:80%;margin: 0 auto"},
ui.h4("navs_tab()"),
ui.navs_tab(*nav_items("navs_tab()")),
h4("navs_pill()"),
ui.navs_pill(*nav_items("navs_pill()")),
h4("navs_tab_card()"),
ui.navs_tab_card(*nav_items("navs_tab_card()")),
h4("navs_pill_card()"),
ui.navs_pill_card(*nav_items("navs_pill_card()")),
h4("navs_pill_list()"),
ui.navs_pill_list(*nav_items("navs_pill_list()")),
h4("navset_tab()"),
# ui.nav_menu("F", ui.nav("G", "g")),
ui.navset_tab(*nav_controls("navset_tab()")),
h4("navset_pill()"),
ui.navset_pill(*nav_controls("navset_pill()")),
h4("navset_tab_card()"),
ui.navset_tab_card(*nav_controls("navset_tab_card()")),
h4("navset_pill_card()"),
ui.navset_pill_card(*nav_controls("navset_pill_card()")),
h4("navset_pill_list()"),
ui.navset_pill_list(*nav_controls("navset_pill_list()")),
)
)


def server(input: Inputs, output: Outputs, session: Session):
pass
@reactive.Effect()
def _():
print("Current navbar page: ", input.navbar_id())


app = App(app_ui, server)
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
ui.input_radio_buttons("controller", "Controller", ["1", "2", "3"], "1")
),
ui.panel_main(
ui.navs_hidden(
ui.nav_content("panel1", "Panel 1 content"),
ui.nav_content("panel2", "Panel 2 content"),
ui.nav_content("panel3", "Panel 3 content"),
ui.navset_hidden(
ui.nav(None, "Panel 1 content", value="panel1"),
ui.nav(None, "Panel 2 content", value="panel2"),
ui.nav(None, "Panel 3 content", value="panel3"),
id="hidden_tabs",
)
)
),
)
)


def server(input: Inputs, output: Outputs, session: Session):
@reactive.Effect()
@event(input.controller)
Expand Down
2 changes: 1 addition & 1 deletion shiny/examples/update_navs/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
ui.layout_sidebar(
ui.panel_sidebar(ui.input_slider("controller", "Controller", 1, 3, 1)),
ui.panel_main(
ui.navs_tab_card(
ui.navset_tab_card(
ui.nav("Panel 1", "Panel 1 content", value="panel1"),
ui.nav("Panel 2", "Panel 2 content", value="panel2"),
ui.nav("Panel 3", "Panel 3 content", value="panel3"),
Expand Down
41 changes: 40 additions & 1 deletion shiny/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)

import sys
from typing import Union, Optional
from typing import Union, Optional, Tuple, Dict, Any

# Even though TypedDict is available in Python 3.8, because it's used with NotRequired,
# they should both come from the same typing module.
Expand All @@ -23,6 +23,13 @@
else:
from typing_extensions import NotRequired, TypedDict

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

from htmltools import TagChildArg

from ._docstring import add_example

# Sentinel value - indicates a missing value in a function call.
Expand Down Expand Up @@ -138,3 +145,35 @@ class SilentCancelOutputException(Exception):

class ActionButtonValue(int):
pass


class NavSetArg(Protocol):
"""
An value suitable for passing to a navigation container (e.g.,
:func:`~shiny.ui.navset_tab`).
"""

def resolve(
self, selected: Optional[str], context: Dict[str, Any] = {}
) -> Tuple[TagChildArg, TagChildArg]:
"""
Resolve information provided by the navigation container.
Parameters
----------
selected
The value of the navigation item to be shown on page load.
context
Additional context supplied by the navigation container.
"""
...

def get_value(self) -> Optional[str]:
"""
Get the value of this navigation item (if any).
This value is only used to determine what navigation item should be shown
by default when none is specified (i.e., the first navigation item that
returns a value is used to determine the container's ``selected`` value).
"""
...
28 changes: 2 additions & 26 deletions shiny/ui/_html_dependencies.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import List, Union
from typing import List

from htmltools import HTML, HTMLDependency

from ..html_dependencies import jquery_deps


def bootstrap_deps(bs3compat: bool = True) -> List[HTMLDependency]:
def bootstrap_deps() -> List[HTMLDependency]:
dep = HTMLDependency(
name="bootstrap",
version="5.0.1",
Expand All @@ -14,33 +14,9 @@ def bootstrap_deps(bs3compat: bool = True) -> List[HTMLDependency]:
stylesheet={"href": "bootstrap.min.css"},
)
deps = [jquery_deps(), dep]
if bs3compat:
deps.append(bs3compat_deps())
return deps


# TODO: if we want to support glyphicons we'll need to bundle font files, too
def bs3compat_deps() -> HTMLDependency:
return HTMLDependency(
name="bs3-compat",
version="1.0",
source={"package": "shiny", "subdir": "www/shared/bs3compat/"},
script=[{"src": "transition.js"}, {"src": "tabs.js"}, {"src": "bs3compat.js"}],
)


def nav_deps(
include_bootstrap: bool = True,
) -> Union[HTMLDependency, List[HTMLDependency]]:
dep = HTMLDependency(
name="bslib-navs",
version="1.0",
source={"package": "shiny", "subdir": "www/shared/bslib/dist/"},
script={"src": "navs.min.js"},
)
return [dep, *bootstrap_deps()] if include_bootstrap else dep


def ionrangeslider_deps() -> List[HTMLDependency]:
return [
HTMLDependency(
Expand Down
4 changes: 2 additions & 2 deletions shiny/ui/_input_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,8 +657,8 @@ def update_navs(
See Also
-------
~shiny.ui.navs_tab
~shiny.ui.navs_pill
~shiny.ui.navset_tab
~shiny.ui.navset_pill
~shiny.ui.page_navbar
"""

Expand Down

0 comments on commit d2e3fb9

Please sign in to comment.