diff --git a/shiny/api-examples/Module/app-core.py b/shiny/api-examples/Module/app-core.py index e5fe83d20..3c4e8d5f5 100644 --- a/shiny/api-examples/Module/app-core.py +++ b/shiny/api-examples/Module/app-core.py @@ -9,7 +9,7 @@ def counter_ui(label: str = "Increment counter") -> ui.TagChild: return ui.card( ui.h2("This is " + label), ui.input_action_button(id="button", label=label), - ui.output_text_verbatim(id="out"), + ui.output_text(id="out"), ) diff --git a/shiny/api-examples/Module/app-express.py b/shiny/api-examples/Module/app-express.py new file mode 100644 index 000000000..57e16dbfe --- /dev/null +++ b/shiny/api-examples/Module/app-express.py @@ -0,0 +1,30 @@ +from shiny import reactive +from shiny.express import module, render, ui + + +# ============================================================ +# Counter module +# ============================================================ +@module +def counter(input, output, session, label, starting_value: int = 0): + count = reactive.value(starting_value) + with ui.card(): + ui.h2(f"This is {label}") + ui.input_action_button("button", f"{label}") + + @render.text + def out(): + return f"Click count is {count()}" + + @reactive.effect + @reactive.event(input.button) + def _(): + count.set(count() + 1) + + +# ============================================================================= +# App that uses module +# ============================================================================= +counter("counter1", "Counter 1", starting_value=0) +ui.hr() +counter("counter2", "Counter 2", starting_value=0) diff --git a/shiny/api-examples/express_module/app-express.py b/shiny/api-examples/express_module/app-express.py deleted file mode 100644 index 75550445d..000000000 --- a/shiny/api-examples/express_module/app-express.py +++ /dev/null @@ -1,25 +0,0 @@ -from shiny import reactive -from shiny.express import module, render, ui - - -@module -def counter(input, output, session, starting_value: int = 0): - count = reactive.value(starting_value) - - ui.input_action_button("btn", "Increment") - - with ui.div(): - - @render.express - def current_count(): - count() - - @reactive.effect - @reactive.event(input.btn) - def increment(): - count.set(count() + 1) - - -counter("one") -ui.hr() -counter("two") diff --git a/shiny/express/_module.py b/shiny/express/_module.py index 9036e287a..779156874 100644 --- a/shiny/express/_module.py +++ b/shiny/express/_module.py @@ -15,7 +15,7 @@ __all__ = ("module",) -@add_example(ex_dir="../api-examples/express_module") +@add_example(ex_dir="../api-examples/Module") def module( fn: Callable[Concatenate[Inputs, Outputs, Session, P], R], ) -> Callable[Concatenate[Id, P], R]: diff --git a/shiny/module.py b/shiny/module.py index a47e52e5e..3ebbbc4a2 100644 --- a/shiny/module.py +++ b/shiny/module.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Callable, TypeVar -from ._docstring import no_example +from ._docstring import add_example from ._namespaces import ( Id, ResolvedId, @@ -24,8 +24,36 @@ _: Id # type: ignore -@no_example() +@add_example(ex_dir="api-examples/Module") def ui(fn: Callable[P, R]) -> Callable[Concatenate[str, P], R]: + """ + Decorator for defining a Shiny module UI function. + + This decorator allows you to write the UI portion of a Shiny module. + When your decorated `ui` function is called with an `id`, + the UI elements defined within will automatically be namespaced using that `id`. + This enables reuse of UI components and consistent input/output handling + when paired with a :func:`shiny.module.server` function. + + Parameters + ---------- + fn + A function that returns a Shiny UI element or layout (e.g., a `ui.panel_*` component). + This function should **not** accept an `id` parameter itself; the decorator injects it. + + Returns + ------- + : + A function that takes a `str` `id` as its first argument, followed by any additional + parameters accepted by `fn`. When called, it returns UI elements with input/output + IDs automatically namespaced using the provided module `id`. + + See Also + -------- + * Shiny Modules documentation: + * :func:`shiny.module.server` + """ + def wrapper(id: Id, *args: P.args, **kwargs: P.kwargs) -> R: with namespace_context(id): return fn(*args, **kwargs) @@ -33,16 +61,50 @@ def wrapper(id: Id, *args: P.args, **kwargs: P.kwargs) -> R: return wrapper -@no_example() +@add_example(ex_dir="api-examples/Module") def server( fn: Callable[Concatenate[Inputs, Outputs, Session, P], R], ) -> Callable[Concatenate[str, P], R]: + """ + Decorator for defining a Shiny module server function. + + This decorator is used to encapsulate the server logic for a Shiny module. + It automatically creates a namespaced child `Session` using the provided module `id`, + and passes the appropriate `input`, `output`, and `session` objects to your server function. + + This ensures that the server logic is scoped correctly for each module instance and + allows for reuse of logic across multiple instances of the same module. + + Parameters + ---------- + fn + A server function that takes `input`, `output`, and `session` as its first + three arguments, followed by any additional arguments defined by the user. + + Returns + ------- + : + A function that takes a module `id` (as a string) as its first argument, + followed by any arguments expected by `fn`. When called, it will register + the module's server logic in a namespaced context. + + See Also + -------- + * Shiny Modules documentation: + * :func:`shiny.module.ui` + """ from .session import require_active_session, session_context def wrapper(id: Id, *args: P.args, **kwargs: P.kwargs) -> R: sess = require_active_session(None) child_sess = sess.make_scope(id) with session_context(child_sess): - return fn(child_sess.input, child_sess.output, child_sess, *args, **kwargs) + return fn( + child_sess.input, + child_sess.output, + child_sess, + *args, + **kwargs, + ) return wrapper