Shiny: [Feature Request] Adding onKeyPress event to textInput

Created on 8 Apr 2019  Â·  8Comments  Â·  Source: rstudio/shiny

Hey,

One feature I receive from clients when building a Shiny App is the ability to "validate" through pressing the Enter key when using a textInput().

Example: You use textInput() as a Search Form, with a button to validate the search just below. I've been asked several time to implement a validation of search through pressing the Enter button here, which I always do locally via jQuery but I feel like there should be a more systemic way to do it.

I think it would be nice to have a native keypress event here, the same way we have a click event on plotOutput()

My suggestion:

pkgload::load_all()
textInput <- function(inputId, label, value = "", width = NULL,
                      placeholder = NULL) {

  value <- restoreInput(id = inputId, default = value)

  div(class = "form-group shiny-input-container",
      style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
      label %AND% tags$label(label, `for` = inputId),
      tags$input(
        id = inputId, 
        type="text", 
        class="form-control", 
        value=value,
        placeholder = placeholder, 
        onKeyPress = sprintf("Shiny.setInputValue('%s_keypress', event.key)", inputId)
        )
  )
}

library(shiny)
ui <- function(request){
  tagList(
    textInput("plop","plop"),
    br(),
    textOutput("text")
  )

}

server <- function(input, output, session){
  observeEvent( input$plop_keypress, {
    print(input$plop_keypress)
    if (input$plop_keypress == "Enter"){
      output$text <- renderText({
        "You've validated"
      })
    }
  })

}

shinyApp(ui, server)

As you can see, the key is printed to the console, and we can create conditional events on pressing the 'Enter' button.

Tested on :

  • Chrome
  • Firefox
  • Safari
    On a Mac OS

Things to look at :

  • Is the pressed key returned the same way on each OS /Browser?
  • The "click" events input names are defined by the user in plotOutput(), maybe this can be the same here. My personal preference is inputId_keypress though, but I also could understand the need for consistency :)

If ever you think this is a good idea, I'll be glad to do a PR.

Colin

Type

All 8 comments

This would also be nice for other inputs like numericInput, passwordInput or textAreainput.

Seconded - easy form validation in Shiny via R is the biggest gap that I see in the ecosystem.

Here's a suggestion for numericInput() and passwordInput()

  • numericInput
pkgload::load_all()
numericInput <- function(
  inputId, 
  label, 
  value, 
  min = NA, 
  max = NA, 
  step = NA,
  width = NULL
  ) {

  value <- restoreInput(id = inputId, default = value)

  # build input tag
  inputTag <- tags$input(id = inputId, type = "number", class="form-control",
                         value = formatNoSci(value))
  if (!is.na(min))
    inputTag$attribs$min = min
  if (!is.na(max))
    inputTag$attribs$max = max
  if (!is.na(step))
    inputTag$attribs$step = step

  div(
    class = "form-group shiny-input-container",
    style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
    label %AND% tags$label(label, `for` = inputId),
    inputTag, 
    onKeyPress = sprintf("Shiny.setInputValue('%s_keypress', event.key)", inputId)
  )
}

library(shiny)
ui <- function(request){
  tagList(
    numericInput("plop","plop", min = 0, max = 10, value = 5),
    br(),
    textOutput("text")
  )

}

server <- function(input, output, session){
  observeEvent( input$plop_keypress, {
    print(input$plop_keypress)
    if (input$plop_keypress == "Enter"){
      output$text <- renderText({
        input$plop
      })
    }
  })

}

shinyApp(ui, server)
  • passwordInput()
pkgload::load_all()
passwordInput <- function(
  inputId, 
  label, 
  value = "", 
  width = NULL,
  placeholder = NULL
  ) {
  div(class = "form-group shiny-input-container",
      style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
      label %AND% tags$label(label, `for` = inputId),
      tags$input(
        id = inputId,
        type="password", 
        class="form-control", 
        value=value,
        placeholder = placeholder,
        onKeyPress = sprintf("Shiny.setInputValue('%s_keypress', event.key)", inputId)
        )
  )
}
library(shiny)
ui <- function(request){
  tagList(
    passwordInput("plop","plop"),
    br(),
    textOutput("text")
  )

}

server <- function(input, output, session){
  observeEvent( input$plop_keypress, {
    print(input$plop_keypress)
    if (input$plop_keypress == "Enter"){
      if (input$plop == "password")
      output$text <- renderText({
        "Unlocked!"
      })
    }
  })

}

shinyApp(ui, server)

I'm not sure of a context where it would be useful in a textAreaInput() — the main use case for this being the key press of 'Enter', and textAreaInput() uses Enter to write a new line. But if ever:

pkgload::load_all()
textAreaInput <- function(inputId, label, value = "", width = NULL, height = NULL,
                          cols = NULL, rows = NULL, placeholder = NULL, resize = NULL) {

  value <- restoreInput(id = inputId, default = value)

  if (!is.null(resize)) {
    resize <- match.arg(resize, c("both", "none", "vertical", "horizontal"))
  }

  style <- paste(
    if (!is.null(width))  paste0("width: ",  validateCssUnit(width),  ";"),
    if (!is.null(height)) paste0("height: ", validateCssUnit(height), ";"),
    if (!is.null(resize)) paste0("resize: ", resize, ";")
  )

  # Workaround for tag attribute=character(0) bug:
  #   https://github.com/rstudio/htmltools/issues/65
  if (length(style) == 0) style <- NULL

  div(class = "form-group shiny-input-container",
      label %AND% tags$label(label, `for` = inputId),
      tags$textarea(
        id = inputId,
        class = "form-control",
        placeholder = placeholder,
        style = style,
        rows = rows,
        cols = cols,
        value,
        onKeyPress = sprintf("Shiny.setInputValue('%s_keypress', event.key)", inputId)
      )
  )
}

library(shiny)
ui <- function(request){
  tagList(
    textAreaInput("plop","plop"),
    br(),
    textOutput("text")
  )

}

server <- function(input, output, session){
  observeEvent( input$plop_keypress, {
    print(input$plop_keypress)
    if (input$plop_keypress == "Enter"){
      if (input$plop == "password")
      output$text <- renderText({
        "Unlocked!"
      })
    }
  })

}

shinyApp(ui, server)

Just noticed a pblm: if you press "Enter" several times, you'll only have a Shiny event once (as the value doesn't change).

Can be solved with adding the time with sprintf("Shiny.setInputValue('%s_keypress', {value : event.key, time : new Date()})", inputId):

pkgload::load_all()
textInput <- function(inputId, label, value = "", width = NULL,
                      placeholder = NULL) {

  value <- restoreInput(id = inputId, default = value)

  div(class = "form-group shiny-input-container",
      style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
      label %AND% tags$label(label, `for` = inputId),
      tags$input(
        id = inputId, 
        type="text", 
        class="form-control", 
        value=value,
        placeholder = placeholder, 
        onKeyPress = sprintf("Shiny.setInputValue('%s_keypress', {value : event.key, time : new Date()})", inputId)
      )
  )
}

library(shiny)
ui <- function(request){
  tagList(
    textInput("plop","plop"),
    br(),
    textOutput("text")
  )

}

server <- function(input, output, session){
  observeEvent( input$plop_keypress, {
    print(input$plop_keypress)
    if (input$plop_keypress$value == "Enter"){
      output$text <- renderText({
        "You've validated"
      })
    }
  })

}

shinyApp(ui, server)

I agree for the textAreaInput, it would probably make more sense to trigger the observeEvent with CTRL+Enter for example or another combination.

@ColinFay you can also do Shiny.setInputValue('keypress', {value : event.key}, {priority: 'event'})

@cpsievert That's indeed better :)

Otherwise, there's function searchInput that does exactly that in shinyWidgets package.

Was this page helpful?
0 / 5 - 0 ratings