posit-dev / py-shinyswatch

Bootswatch themes for py-shiny

Home Page:https://posit-dev.github.io/py-shinyswatch/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Theme picker?

vnijs opened this issue · comments

py-shinyswatch looks outstanding. Thanks for creating this!

I'm almost surprised no one has asked this question yet since theme pickers seem to be very popular but ... is something like the below possible for shiny-for-python and py-shinyswatch?

library(shiny)
library(bslib)

# Define UI
ui <- fluidPage(
    theme = bs_theme(),

    # Add a select input for theme selection
    selectInput("theme", "Choose a theme:",
        choices = c(
            "default", "cerulean", "cosmo", "flatly",
            "journal", "litera", "lumen", "lux", "materia",
            "minty", "pulse", "sandstone", "simplex",
            "sketchy", "slate", "solar", "spacelab",
            "superhero", "united", "yeti"
        )
    ),

    # Add some content
    h1("Hello, Shiny!"),
    p("This is a sample Shiny app with theme selection.")
)

# Define server logic
server <- function(input, output, session) {
    # Update the theme when the input changes
    observe({
        session$setCurrentTheme(bs_theme(bootswatch = input$theme))
    })
}

# Run the application
shinyApp(ui = ui, server = server)

Yes, but it'll be tricky as the current shinyswatch themes are only html dependencies.

It will at least need a page refresh. I don't know off the top of my head if the ui can be a function in py-shiny. If so, the we can make it work. Not pretty, but it'd work.

Currently py-shiny does not have theme support built in like R shiny

Thanks for the quick comment @schloerke. Page refresh isn't a big deal. Would that involve something like onclick = "window.location.reload();"?

Not sure what you mean by "if the ui can be a function in py-shiny". In the below app_ui is a function. I started on the below before realizing I didn't know a way to push the theme into the header. I also use functions a lot to reuse pieces of ui across apps.

from shiny import App, Inputs, Outputs, Session, ui, reactive

import shinyswatch


def app_ui():
    return ui.page_fluid(
        # Theme code - start
        shinyswatch.theme.sketchy(),
        # Theme code - end
        ui.input_select(
            id="select_theme",
            label="Select a theme:",
            selected="superhero",
            choices={
                "superhero": "Super Hero",
                "darkly": "Darkly",
                "sketchy": "Sketchy",
            },
        ),
    )


def server(input: Inputs, output: Outputs, session: Session):
    @reactive.Effect
    @reactive.event(input.select_theme, ignore_none=True)
    def set_theme():
        return shinyswatch.get_theme(f"{input.select_theme()}")


app = App(app_ui(), server)

Oh! Great!

Cliff notes before a proper approach on Monday...

  • drop-down is selected in browser.
  • server recognizes value and updates global variable that sets the theme name
  • server sends custom message to refresh page
  • on page load, ui function uses the global variable to determine theme.
  • (rinse repeat)

Gave it a try but it keeps resetting to the original value.

from shiny import App, Inputs, Outputs, Session, ui, reactive

import shinyswatch

if "theme" not in globals():
    theme = {"theme": "superhero"}


def app_ui():
    return ui.page_fluid(
        shinyswatch.get_theme(theme.get("theme", "superhero")),
        ui.tags.script(
            """
            Shiny.addCustomMessageHandler('refresh', function(message) {
                window.location.reload();
            });
            """
        ),
        ui.input_select(
            id="select_theme",
            label="Select a theme:",
            selected=theme.get("theme", "superhero"),
            choices=["superhero", "darkly", "sketchy"],
        ),
    )


def server(input: Inputs, output: Outputs, session: Session):
    @reactive.Effect
    @reactive.event(input.select_theme, ignore_none=True)
    async def set_theme():
        if input.select_theme() != theme.get("theme", "superhero"):
            theme["theme"] = input.select_theme()
            await session.send_custom_message("refresh", "")


app = App(app_ui(), server)

app_ui isn't called on browser restart.

from shiny import App, Inputs, Outputs, Session, ui, reactive, render
import shinyswatch


if "theme" not in globals():
    print("Define theme dictionary. Was not in globals()")
    theme = {"theme": "superhero"}


def app_ui():
    print("ui function was called")
    return ui.page_fluid(
        shinyswatch.get_theme(theme.get("theme", "superhero")),
        ui.tags.script(
            """
            Shiny.addCustomMessageHandler('refresh', function(message) {
                window.location.reload();
            });
            """
        ),
        # ui.input_select(
        #     id="select_theme",
        #     label="Select a theme:",
        #     selected=theme.get("theme", "superhero"),
        #     choices=["superhero", "darkly", "sketchy"],
        # ),
        ui.output_ui("ui_select_theme"),
    )


def server(input: Inputs, output: Outputs, session: Session):
    print("server function was called")
    print(theme.get("theme", "superhero"))

    @reactive.Effect
    @reactive.event(input.select_theme, ignore_none=True)
    async def set_theme():
        if input.select_theme() != theme.get("theme", "superhero"):
            theme["theme"] = input.select_theme()
            await session.send_custom_message("refresh", "")
        # ui.update_select("select_theme", selected=theme.get("theme", "superhero"))

    @output(id="ui_select_theme")
    @render.ui
    def ui_select_theme():
        return (
            ui.input_select(
                id="select_theme",
                label="Select a theme:",
                selected=theme.get("theme", "superhero"),
                choices=["superhero", "darkly", "sketchy"],
            ),
        )


app = App(app_ui(), server, debug=False)

app_ui isn't called on browser restart

That's not good. Not much point of having a function, then. 😝 This behavior is definitely a bug in py-shiny.

I thought your solution would have worked (but only glancing through it). But if the ui isn't being recalculated of refresh, then we're already off to a rough start.

Thanks @schloerke. Report as an issue to py-shiny?

If you're up for it! Please reference this one. Thank you!

I missed that app_ui was being called as it was being sent into App. Instead, app_ui should be a function that takes a starlette.requests.Request object for context about the request.

Updated app below:

import shinyswatch
from htmltools import Tag
from starlette.requests import Request as StarletteRequest

from shiny import App, Inputs, Outputs, Session, reactive, ui

if "theme_obj" not in globals():
    print("Define theme dictionary. Was not in globals()")
    theme_obj = {"theme": "superhero"}


def app_ui(request: StarletteRequest) -> Tag:
    print("ui function was called")
    return ui.page_fluid(
        shinyswatch.get_theme(theme_obj.get("theme")),
        ui.tags.script(
            """
            Shiny.addCustomMessageHandler('refresh', function(message) {
                window.location.reload();
            });
            """
        ),
        ui.input_select(
            id="select_theme",
            label="Select a theme:",
            selected=theme_obj.get("theme"),
            choices=["superhero", "darkly", "sketchy"],
        ),
    )


def server(input: Inputs, output: Outputs, session: Session):
    print("server function was called")
    print(theme_obj.get("theme"))

    @reactive.Effect
    @reactive.event(input.select_theme, ignore_none=True)
    async def set_theme():
        if input.select_theme() != theme_obj.get("theme"):
            print("setting theme: ", input.select_theme())
            theme_obj["theme"] = input.select_theme()
            await session.send_custom_message("refresh", "")


app = App(app_ui, server, debug=False)

** working on making shinyswatch.theme_picker(app: App)

Nice! Thanks @schloerke. Should I remove the related issue for py-shiny?

EDIT: Just noticed you already closed that issue.

@vnijs Let me know how #12 works for you!

Remove the theme from your app's UI and wrap your app in shinyswatch.theme_picker(app)

Ex:

# ... at bottom of file: app.py
app = shinyswatch.theme_picker(App(app_ui, server))

Very nice @schloerke! Question: Could the theme dropdown be included in the navbar? Not without adding it back to the UI and forcing a refresh correct?

Not without adding it back to the UI and forcing a refresh correct?

Correct. It would require a "module" type approach that would need both a ui and server function.


I'm ok using the module approach if you find it more familiar. Altering the app object is a new approach compared to R.

Thoughts?

To be honest, I never really used modules in R. Just used functions instead. Would love to see the example. Thanks @schloerke

Yes, it'd basically be shinyswatch.theme_picker_ui(*, default_theme: str, id: str) and shinyswatch.theme_picker_server(*, id: str). Each function would live within the UI / server respectively.