User Feedback

Validation

Validating input

This requires a pacakge shiny_validate, which is a python implementation of R package shinyvalidate.

action-feedback/input-validation/app.py
from shiny import App, ui, render, reactive, req
from shiny_validate import InputValidator

app_ui = ui.page_fluid(
    ui.input_numeric("n", "n", value=10),
    ui.output_text("half"),
)

def server(input, output, session):
    iv = InputValidator()

    iv.add_rule("n", lambda x: "Please select an even number" if x%2 != 0 else None)
    iv.enable()

    @render.text
    def half():
        even = input.n()%2 == 0
        req(even)
        return input.n()/2

app = App(app_ui, server)
Caution

Depending on {shiny_validate} version, it may reinstall {shiny} with different version. Please choose a right version that is compatible with your shiny installation or consider pip install --no-deps when installing {shiny_validate}.

Tip

When there are mutliple controls and/or multiple rules, you can use iv.is_valid() to check whether all the input validation rules currently pass.

Cancelling execution with req()

examples/action-feedback/cancelling-execution/app.py
from shiny import App, ui, render, req

app_ui = ui.page_fluid(
    ui.input_select("language", "Language", choices=["", "English", "Maori"]),
    ui.input_text("name", "Name"),
    ui.output_text("greeting"),
)

def server(input, output, session):
    greetings = {
        'English': "Hello",
        'Maori': "Ki ora",
    }

    @render.text
    def greeting():
        req(input.language(), input.name())
        return f"{greetings[input.language()]} {input.name()}!"
    

app = App(app_ui, server)

req() and validation

Below is my attempt to implement an app that behave the same to the Mastering Shiny book example:

  • When no input is provided, does not raise an error and just does not show any table
  • When invalid data name is provided, show an error message, but keep a table output to be a table associated with the last valid input.
examples/action-feedback/dataset-name/app.py
from shiny import App, ui, render, reactive, req
from shiny_validate import InputValidator
from pydataset import data
import numpy as np

datasets = set(data()['dataset_id'])

app_ui = ui.page_fluid(
    ui.input_text("dataset", "Dataset name"),
    ui.output_table("table"),
)

def server(input, output, session):
    iv = InputValidator()
    iv.add_rule("dataset", 
        lambda x: "Unknown dataset" \
            if len(x) > 0 and not x in datasets \
            else None)
    iv.enable()

    @reactive.calc
    def load():
        req(input.dataset())
        req(iv.is_valid(), cancel_output=True)
        return data(input.dataset())
    
    @render.table
    def table():
        return load().head()

app = App(app_ui, server)

Below is slightly different implementation by using check submodule of {shiny_validate}.

examples/action-feedback/dataset-name/app-check.py
from shiny import App, ui, render, reactive, req
from shiny_validate import InputValidator, check
from pydataset import data
import numpy as np

datasets = set(data()['dataset_id'])

app_ui = ui.page_fluid(
    ui.input_text("dataset", "Dataset name"),
    ui.output_table("table"),
)

def server(input, output, session):
    iv = InputValidator()
    iv.add_rule("dataset", 
                check.compose_rules(
                    check.required("Empty dataset name"),
                    check.in_set(datasets, "Unknown dataset")),
                )
                
    iv.enable()

    @reactive.calc
    def load():
        req(input.dataset())
        req(iv.is_valid(), cancel_output=True)
        return data(input.dataset())
    
    @render.table
    def table():
        return load().head()

app = App(app_ui, server)
  • check.required() is to set the input as required and throw an error message when input has not been provided.
  • check.in_set() is to check whether the input is an element of given set. It is important that the type of set argument should be set.
  • check.compose_rules() is to add multiple rules to a single input control.

Validate output

I could not find Shiny for Python’s equivalent of Shiny for R’s validate() function. Implementation below is per Gorden Shotwell’s recommendation on this thread.

examples/action-feedback/output-validation/app.py
from shiny import App, ui, render, reactive, req
import numpy as np

app_ui = ui.page_fluid(
    ui.input_numeric("x", "x", value=0),
    ui.input_select("trans", "transformation", choices=["square", "log", "square-root"]),
    ui.output_ui("out_container"),
)

def server(input, output, session):
    @render.ui
    def out_container():
        if input.x() < 0 and input.trans() in ["log", "square-root"]:
            return ui.markdown("**x can not be negative for this transformation**")
        else:
            return ui.output_text("out")
    
    @render.text
    def out():
        req(not (input.x() < 0 and input.trans() in ["log", "square-root"]))
        match input.trans():
            case "square":
                res = input.x()**2
            case "square-root":
                res = np.sqrt(input.x())
            case "log":
                res = np.log(input.x())

        return res

app = App(app_ui, server)
Warning

I could not find Shiny for Python’s equivalent of Shiny for R’s validate() function. Implementation below is per Gorden Shotwell’s recommendation on this thread. I hope Shiny for Python provides validate() equvalent.

Notifications

Transient notification

ui.notification_show() displays a message in the corner of the screen.

examples/action-feedback/transient-notification/app.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
from time import sleep

app_ui = ui.page_fluid(
    ui.input_action_button("goodnight", "Good night"),
)

def server(input: Inputs, output: Outputs, session: Session):
    @reactive.effect
    @reactive.event(input.goodnight)
    def _():
        ui.notification_show("So long")
        sleep(1)        
        ui.notification_show("Farewell", type="message")
        sleep(1)        
        ui.notification_show("Auf Wiedersehen", type="warning")
        sleep(1)        
        ui.notification_show("Adieu", type="error")

app = App(app_ui, server)
Note

Shiny for Python website shows an example with asynchronous programming. I do not clearly understand what would be differences, but here is an asynchronous programming version implementation:

examples/action-feedback/transient-notification/app-async.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
from asyncio import sleep

app_ui = ui.page_fluid(
    ui.input_action_button("goodnight", "Good night"),
)

def server(input: Inputs, output: Outputs, session: Session):
    @reactive.effect
    @reactive.event(input.goodnight)
    async def _():
        ui.notification_show("So long")
        await sleep(1)
        ui.notification_show("Farewell", type="message")
        await sleep(1)
        ui.notification_show("Auf Wiedersehen", type="warning")
        await sleep(1)
        ui.notification_show("Adieu", type="error")

app = App(app_ui, server)

Removing on completion

examples/action-feedback/removing-notification/app.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
from shiny.types import FileInfo
import pandas as pd

app_ui = ui.page_fluid(
    ui.input_file("file", "Add CSV file", accept=".csv"),
    ui.output_table("table")
)


def server(input: Inputs, output: Outputs, session: Session):
    @reactive.calc
    def data():
        id = ui.notification_show("Reading data...",
            duration=None, close_button=False)
        
        file: list[FileInfo] | None = input.file()
        if file is None:
            res = pd.DataFrame()
        else:
            res = pd.read_csv(file[0]["datapath"])

        ui.notification_remove(id)
        
        return res
        
    @render.table
    def table():
        return data().head()


app = App(app_ui, server)
Warning

I could not find Shiny for Python’s equivalent of Shiny for R’s on.exit() function. The app implementation above may be unreliable in a case when the reactive code chunk returns an error (e.g. when pd.read_csv() causes an error).

Progressive updates

examples/action-feedback/progressive-update/app.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
from pydataset import data
from time import sleep

mtcars = data('mtcars')

app_ui = ui.page_fluid(
    ui.output_table("table"),
)


def server(input: Inputs, output: Outputs, session: Session):
    def notify(msg, id=None):
        return ui.notification_show(msg, id=id, duration=None, close_button=False)

    @reactive.calc
    def data():
        id = notify("Reading data...")
        sleep(1)

        notify("Reticulating splines...", id=id)
        sleep(1)

        notify("Herding llamas...", id=id)
        sleep(1)

        notify("Orthogonalizing matrices...", id=id)
        sleep(1)

        ui.notification_remove(id)
        return mtcars

    @render.table
    def table():
        return data().head()


app = App(app_ui, server)

Progress bars

Shiny

examples/action-feedback/builtin-progress-bar/app.py
from shiny import App, ui, render, reactive
from time import sleep
import random

app_ui = ui.page_fluid(
    ui.input_numeric("steps", "How many steps?", 10),
    ui.input_action_button("go", "go"),
    ui.output_text("result"),
)

def server(input, output, session):
    @reactive.calc
    @reactive.event(input.go)
    def data():
        with ui.Progress() as p:
            p.set(message="Computing random number")
            for i in range(1, input.steps()):
                p.inc(1/input.steps())
                sleep(0.5)
        
        return random.uniform(0, 1)
    
    @render.text
    def result():
        return round(data(), 2)

app = App(app_ui, server)
Note

The following code provides equally behaving progress bar:

with ui.Progress(min=1, max=input.steps()) as p:
    p.set(message="Computing random number")
        for i in range(1, input.steps()):
            p.set(i)
            sleep(0.5)
  • You can pass min and max arguments when initializing the progress bar to set the starting point and end of the progress bar. If you do not provide, they set to be min=0 and max=1 by default.
  • Instead of inc() that increment the progress bar by amount argument, you can use set() that update progress to be value argument.

Waiter

There is a python package py-waiter that provides python implementation of waiter package from John Coene. As of January 19, 2024, py-waiter only exists in github.

Let me further explore this package later, probably once the package is registered to PyPI.

Spinners

The same situation to waiter.

Confirming and undoing

Explicit confirmation

examples/action-feedback/explicit-confirmation/app.py
from shiny import App, ui, reactive

modal_confirm = ui.modal(
    "Are you sure you want to continue?",
    title="Delete files",
    footer=ui.TagList(
        ui.input_action_button("cancel", "Cancel"),
        ui.input_action_button("ok", "Delete", class_="btn btn-danger"),
    )
)

app_ui = ui.page_fluid(
    ui.input_action_button("delete", "Delete all files?"),
)

def server(input, output, session):
    @reactive.effect
    @reactive.event(input.delete)
    def delete():
        ui.modal_show(modal_confirm)
    
    @reactive.effect
    @reactive.event(input.ok)
    def ok():
        ui.notification_show("Files deleted")
        ui.modal_remove()
    
    @reactive.effect
    @reactive.event(input.cancel)
    def cancel():
        ui.modal_remove()


app = App(app_ui, server)
Note

For "cancel" button on modal dialog footer, because there is no associated tasks other than closing the modal dialog, you may want to consider using ui.modal_button() within the UI while removing server-side behavior for the button event:

examples/action-feedback/explicit-confirmation/app-modal-button.py
from shiny import App, ui, reactive

modal_confirm = ui.modal(
    "Are you sure you want to continue?",
    title="Delete files",
    footer=ui.TagList(
        ui.modal_button("Cancel"),
        ui.input_action_button("ok", "Delete", class_="btn btn-danger"),
    )
)

app_ui = ui.page_fluid(
    ui.input_action_button("delete", "Delete all files?"),
)

def server(input, output, session):
    @reactive.effect
    @reactive.event(input.delete)
    def delete():
        ui.modal_show(modal_confirm)
    
    @reactive.effect
    @reactive.event(input.ok)
    def ok():
        ui.notification_show("Files deleted")
        ui.modal_remove()

app = App(app_ui, server)

Undoing an Action

Caution

I could not reproduct the app in python. Here are two topics that I do not know how to implemet in Shiny for Python. - Let observe run only once, which is equivalent to once = TRUE in observeEvent() in R. - Destory existing observer from outside the scope that the observer exists.