posit-dev / py-shiny

Shiny for Python
https://shiny.posit.co/py/
MIT License
1.29k stars 78 forks source link

ui.input_numeric allows typed values outside of min/max #953

Open shebdon opened 10 months ago

shebdon commented 10 months ago

ui.input_numeric does not currently catch/correct typed values outside of the min/max range (aka min/max range arguments are ineffective for typed user input).

This can be reproduced using the example here: https://shiny.posit.co/py/api/ui.input_numeric.html when the user types in 1000, the server responds with 1000, even though the max is set to 100.

The expected functionality can be simulated with the app below. When the users enters an out-of-bounds value, it is imediately replaced with the nearest within-bounds value. With modules, one can build their own control system, though this feels like unintented behavior of the ui.input_numeric function.

Thanks,

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

app_ui = ui.page_fluid(
    ui.output_ui("numeric"),
    ui.output_text_verbatim("value"),
)

def server(input, output, session):
    @output
    @render.ui
    def numeric():
        return ui.input_numeric("obs", "Observations:", 10, min=1, max=100)

    @output
    @render.text
    def value():
        return input.obs()

    @reactive.Effect
    def _():
        if not isinstance(input.obs(), int): #this just keeps an empty box from crashing the app
            return
        if input.obs() > 100 :
            ui.update_numeric("obs", label="Observations:", value=100, min = 1, max = 100)
        if input.obs() < 1 :
            ui.update_numeric("obs", label="Observations:", value=1, min = 1, max = 100)   

app = App(app_ui, server)
shebdon commented 10 months ago

Here's a module and usage for teh workaround.

from shiny import module, ui, reactive, render
@module.ui
def controlled_numeric_ui():
    return ui.output_ui("numeric")

@module.server
def controlled_numeric_server(input, output, session, my_id:str = "controlled_number", my_label:str = "Can't type out-of-range number", my_value:float = 1, my_min:float = 1, my_max:float=100):

    @output
    @render.ui
    def numeric():
        return ui.input_numeric("controlled_numeric", label=my_label, value=my_value, min=my_min, max=my_max)

    @reactive.Effect
    def _():
        if not isinstance(input.controlled_numeric(), int):
            return
        if input.controlled_numeric() > my_max :
            ui.update_numeric("controlled_numeric", label=my_label, value=my_max, min = my_min, max = my_min)
        if input.controlled_numeric() < my_min :
            ui.update_numeric("controlled_numeric", label=my_label, value=my_min, min = my_min, max = my_min)

    return input.controlled_numeric

And here's a usage example:

from shiny import App, Inputs, Outputs, Session, ui, render
from numeric_module import controlled_numeric_server, controlled_numeric_ui

app_ui = ui.page_fluid(
        controlled_numeric_ui("numeric_module"),
        ui.output_text_verbatim("value")
    )

def server(input: Inputs, output: Outputs, session: Session):
    controlled_value = controlled_numeric_server("numeric_module",my_id="controlled_value", my_label="Forced to be in range", my_value=10, my_min=1, my_max=100)

    @output
    @render.text
    def value():
        return controlled_value()

app = App(app_ui, server)