Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

textInput(), textAreaInput(), numericInput(), passwordInput(): allow updating value on blur #4183

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

wch
Copy link
Collaborator

@wch wch commented Feb 3, 2025

This PR updates textInput(), textAreaInput(), numericInput(), and passwordInput(), so that:

  • When updateOn="blur" is used, the input value will be updated only when the user presses Enter, or the input loses focus.

Example app:

library(shiny)

ui <- fluidPage(
  textInput("txt", "Text", "Hello", updateOn="blur"),
  textAreaInput("txtarea", "Text Area", updateOn="blur"),
  numericInput("num", "Numeric", 1, updateOn="blur"),
  passwordInput("pwd", "Password", updateOn="blur"),
  verbatimTextOutput("value")
)

server <- function(input, output) {
  output$value <- renderText({
    paste(input$txt, "----", input$txtarea, "----", input$num, "----", input$pwd, sep = "\n")
  })
}

shinyApp(ui, server)

Notes:

  • For textAreaInput(), it only updates on blur; when the user presses Enter, it adds a newline character. Maybe it should also update when the user presses ctrl- or cmd-Enter?
    • Update [garrick]: After 8977a82, we now explicitly add a keydown event handler for Enter or Cmd/Ctrl + Enter (textarea inputs), rather than relying on the change event.
  • For numericInput(), the value still updates when the up/down arrow are pressed. I think that's probably because those keypresses trigger a change event.
    • Update [garrick]: I fixed this in 0f0e4e5

@wch wch force-pushed the text-input-enter branch from 7031a42 to 1219174 Compare February 3, 2025 18:44
@wch wch changed the title textInput(): allow updating value on blur and allow setting debounce delay textInput(): allow updating value on blur Feb 3, 2025
@wch wch changed the title textInput(): allow updating value on blur textInput(), numericInput(), passwordInput(): allow updating value on blur Feb 3, 2025
@wch wch force-pushed the text-input-enter branch from e58f790 to a06d61a Compare February 3, 2025 21:30
@wch wch changed the title textInput(), numericInput(), passwordInput(): allow updating value on blur textInput(), textAreaInput(), numericInput(), passwordInput(): allow updating value on blur Feb 3, 2025
@gadenbuie gadenbuie added this to the Next Release milestone Feb 19, 2025
Comment on lines 66 to 72
$el.on(
"change.textInputBinding",
// event: Event
function () {
callback(false);
}
);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For NumberInputBinding, this change event can get triggered:

  • programmatically, via updateNumericInput() on the server
  • by clicking on the up/down arrows, or pressing up/down keys on the keyboard.

When we enable the option, we'll want the callback to be invoked when receiving the server message, but not when the up/down arrow event causes it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I handled this in 0f0e4e5 by adding extra data to the change event emitted by the .receiveMessage() method, whose presence signals that the origin of the event was a server-side message. Then, when updateOn === "blur", we ignore change events that aren't server driven.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that's worth considering is the possibility that someone would trigger a change event on a programmatically via JS, like if they use shinyjs. Requiring fromServer: true could make that stop working.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point; I'll think about this some more

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! I came up with an elegant solution (in d322976): rather than using the event data, we'll just ignore any change events that happen when the element has focus. It's unlikely that programmatic changes will happen while an element has focus, so in most cases the update is immediate. But if the changed input currently has focus, we'll just update on blur like we otherwise would.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh great! That’s much nicer than the other solutions.

@gadenbuie
Copy link
Member

gadenbuie commented Feb 20, 2025

@wch This PR is ready to go! I've implemented all the changes we discussed. Here's a small app based on your example that shows the difference between updateOn="change" and updateOn="blur" and also includes buttons for testing server-side updates. (I intend to follow up and add this app to shinycoreci, but I've tested it manually in Chrome/Firefox/Safari and on iOS.)

Could you give this PR a quick review?

Example app
library(shiny)

text_input_ui <- function(updateOn = "change") {
  ns <- NS(updateOn)

  tagList(
    h2(sprintf('updateOn="%s"', updateOn)),
    textInput(ns("txt"), "Text", "Hello", updateOn = updateOn),
    textAreaInput(ns("txtarea"), "Text Area", updateOn = updateOn),
    numericInput(ns("num"), "Numeric", 1, updateOn = updateOn),
    passwordInput(ns("pwd"), "Password", updateOn = updateOn),
    verbatimTextOutput(ns("value")),
    actionButton(ns("update_text"), "Update Text"),
    actionButton(ns("update_text_area"), "Update Text Area"),
    actionButton(ns("update_number"), "Update Number"),
    actionButton(ns("update_pwd"), "Update Password"),
  )
}

text_input_server <- function(id) {
  moduleServer(id, function(input, output, session) {
    output$value <- renderText({
      paste(
        "---- Text ----",
        input$txt,
        "---- Text Area ----",
        input$txtarea,
        "---- Numeric ----",
        input$num,
        "---- Password ----",
        input$pwd,
        sep = "\n"
      )
    })

    observeEvent(input$update_number, {
      updateNumericInput(session, "num", value = floor(runif(1, 0, 100)))
    })

    observeEvent(input$update_text_area, {
      updateTextAreaInput(
        session,
        "txtarea",
        value = paste(sample(LETTERS, 5), collapse = "\n")
      )
    })

    observeEvent(input$update_text, {
      updateTextInput(
        session,
        "txt",
        value = paste(sample(letters, 12), collapse = " ")
      )
    })

    observeEvent(input$update_pwd, {
      updateTextInput(
        session,
        "pwd",
        value = paste(sample(letters, 8), collapse = "")
      )
    })
  })
}

ui <- fluidPage(
  fluidRow(
    column(6, class = "col-sm-12", text_input_ui("change")),
    column(6, class = "col-sm-12", text_input_ui("blur"))
  )
)

server <- function(input, output, session) {
  text_input_server("change")
  text_input_server("blur")
}

shinyApp(ui, server)

Comment on lines +17 to +20
#' update the input immediately whenever the value changes. Use `"blur"`to
#' delay the input update until the input loses focus (the user moves away
#' from the input), or when Enter is pressed (or Cmd/Ctrl + Enter for
#' [textAreaInput()]).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if some people will want to have a third option, "enter"? In that scenario, we'd allow multiple values for updateOn, so you could say updateOn = c("blur", "enter").

That said, I prefer the implementation as it is now; I don't think in practice in Shiny apps it's a great idea to require pressing Enter in these inputs for the value to change, or if you do you should use an button (or we extract the shinychat text input into a standalone component).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants