-->

Shiny Responds to Enter

2019-01-24 07:13发布

问题:

I have a textInput widget, and now whenever I start typing in the widget, shinyApp tries to evaluate the unfinished content in the textInput widget and results in many errors. I'm aware that adding an action Button "Calculate" would easily solve the problem. However, my app does not have space left for one more button. So, I'd like to know if there's a way that the textInput widget would "listen" to a keyboard event, such as when the user hits "Enter?" Thanks in advance!

回答1:

In your case, the problem is reactive programming and this is the reason that you need something to manage this situation. My recommendation is to use observer pattern or validate function.

  • Observer pattern: shiny implements the observer pattern which is useful to act when an event happens in an object (it can be a click in a button, new value in an input...).

  • Validate function: the functionality of this process is similar to an if/else statement. Indeed, there is need what is the if to check the parameter, if the values are wrong, there will be an error message.

To know how to use observe pattern and the validate function, click on the previous link (in the Shiny website is everything explained).



回答2:

Very good question. Here is an example of the way I use; this app shows a ggplot and the user gives the title of the ggplot in a textbox - but the title changes reacts only when "Return" is pressed:

js <- '
$(document).on("keyup", function(e) {
  if(e.keyCode == 13){
    Shiny.onInputChange("keyPressed", Math.random());
  }
});
'

shinyApp(
  ui = bootstrapPage(

    tags$script(js),

    textInput("title", label = "Title"),

    plotOutput("ggplot")
  ),

  server = function(input, output, session){

    Title <- reactiveVal()

    observeEvent(input[["keyPressed"]], {
      Title(input[["title"]])
    })

    output[["ggplot"]] <- renderPlot({
      ggplot(iris, aes(x=Sepal.Length, y=Sepal.Width)) +
        geom_point() +
        ggtitle(Title())
    })

  }
)

Explanations:

This Javascript code:

$(document).on("keyup", function(e) {
  if(e.keyCode == 13){
    Shiny.onInputChange("keyPressed", Math.random());
  }
});

creates a new Shiny input, namely input$keyPressed which receives a random number when the "Return" key is pressed anywhere.

Then I define a reactive value which takes the value input$title given in the textbox by the user, only when input$keyPressed changes:

Title <- reactiveVal()

observeEvent(input[["keyPressed"]], {
  Title(input[["title"]])
})

And finally I pass this reactive value to ggtitle:

output[["ggplot"]] <- renderPlot({
  ggplot(iris, aes(x=Sepal.Length, y=Sepal.Width)) +
    geom_point() +
    ggtitle(Title())
})


回答3:

Here is an app that I built, and solves a similar problem.

The idea is to have listen to both the keypress and the button, and make sure they work together well. In your case, you should be able to make something even simpler because you don't need the button.

I hope you like it.

library(shiny)
# This is a demo app to test a key binding on an actionButton
# Uncommenting the info item (on both UI and server) will display internal stuff
runApp( 
  list(
    #############################################
    # UI 
    #############################################
    ui = bootstrapPage(
      textInput ("myinput", label = "Write something here"),
      tags$script('
        $(document).on("keydown", function (e) {
        Shiny.onInputChange("lastkeypresscode", e.keyCode);
        });
        '),
      actionButton("GO", "Lancer le matching !"),
      # verbatimTextOutput("info"),
      verbatimTextOutput("results")
    ), 

    #############################################
    # SERVER 
    #############################################
    server = function(input, output, session) {

      # There are state variables for the input text and GO button
      curr.val <- "" # Corresponds to the current displayed input$myinput
      curr.go  <- 0  # Corresponds to the last known GO value (integer)

      lastEvent <- reactive({
        # Is reactive to the following events
        input$GO
        input$lastkeypresscode

        # Decide which action should be taken
        if(input$GO > curr.go) {
          # The user pushed the GO actionButton, so take action
          action <- 1
          curr.go <<- input$GO
        } else if(input$lastkeypresscode == 13) {
          # The user pressed the Enter key, so take action
          action <- 1
        } else {
          # The user did anything else, so do nothing
          action <- 0
        }

        return(action)
      })

      output$results = renderPrint({
        if(lastEvent() == 1) {
          curr.val <<- isolate(input$myinput)
        }
        curr.val
      })

      # output$info = renderText({
      #   paste(curr.val, curr.go, input$lastkeypresscode, sep = ", ")
      # })
    }
  )
)


回答4:

I created a simple app as an example, where the user can write the name of a city and after pressing ENTER it returns latitude and longitude:

library(shiny)
library(ggmap)


runApp( 
  list(
    #############################################
    # UI 
    #############################################
ui = fluidPage( title = "City Search" ,
                position= "static-top",
                tags$script(' $(document).on("keydown", function (e) {
                                                  Shiny.onInputChange("lastkeypresscode", e.keyCode);
                                                  });
                                                  '),
                # Search panel:
                textInput("search_city", "" , placeholder= "City"),
                verbatimTextOutput("results")), 

    #############################################
    # SERVER 
    #############################################
server = function(input, output, session) {

  observe({
    if(!is.null(input$lastkeypresscode)) {
      if(input$lastkeypresscode == 13){
        target_pos = geocode(input$search_city, messaging =FALSE)
        LAT = target_pos$lat
        LONG = target_pos$lon
        if (is.null(input$search_city) || input$search_city == "")
          return()
        output$results = renderPrint({
          sprintf("Longitude: %s ---- Latitude: %s", LONG, LAT)
        })
      }
    }
  })
}
)
)

Note that for catching the ENTER input the code is 13, i.e. input$lastkeypresscode == 13.