shinyalert icon indicating copy to clipboard operation
shinyalert copied to clipboard

[Bug] Cannot access input variable for shinyalert modal created in `callbackR`

Open JamesBrownSEAMS opened this issue 5 years ago • 13 comments

I have a simple user interface that asks two questions and takes two user defined values as responces. I have implmented this using the latest version of shinyalert using the new features that allow input controls to be defined in the modals. An example of the process is shown here:

library(shiny)
library(shinyalert)

ui <- fluidPage(
  useShinyalert(),
  
  tags$h2("Demo shinyalert"),
  tags$br(),
  fluidRow(
    column(
      width = 6,
      actionButton("shinyalert_popup",
                   "Alert Popup")
    )
  ),
  fluidRow(
    verbatimTextOutput(outputId = "return_value_1"),
    verbatimTextOutput(outputId = "return_value_2")
  )
)

server <- function(input, output, session) {
  
  observeEvent(input$shinyalert_popup, {
    shinyalert(
      title = "Input Number 1",
      text = tagList(
        "Enter a number",
        textInput(inputId = "shinyalert_value_1",
                  label = "")
      ),
      html = TRUE,
      inputId = "shinyalert_modal",
      showConfirmButton = TRUE,
      confirmButtonText = "OK",
      callbackR = function() {
        shinyalert(
          title = "Input Number 2",
          text = tagList(
            "Enter another number",
            textInput(inputId = "shinyalert_value_2",
                      label = "")
          ),
          html = TRUE,
          inputId = "shinyalert_callback",
          showConfirmButton = TRUE,
          confirmButtonText = "OK"
        )
      }
    )
  })
  
  output$return_value_1 <- renderPrint(input$shinyalert_value_1)
  output$return_value_2 <- renderPrint(input$shinyalert_value_2)
}

shinyApp(ui = ui, server = server)

The responce shows that the first input, from the id shinyalert_value_1 is captured and updates in the ui. The second input, from shinyalert_value_2, which is nested in the callbackR parameter shows a NULL when values are typed and after the modal has been closed. This indicates that the input$shinyalert_value_2 is not found in this configuration.

JamesBrownSEAMS avatar Dec 16 '20 15:12 JamesBrownSEAMS

Thanks for reporting @JamesBrownSEAMS

I can confirm this is indeed a bug. It seems to do with the timing, perhaps the new input is being added and unbound too early or something like that, because if I add a 500ms delay before showing the second alert then it works, but a 100ms delay does not. I'm caught up in a lot of projects and not sure when I'd be able to debug this, I would warmly welcome any help.

daattali avatar Dec 17 '20 00:12 daattali

I created a modified example following comments about the delay and used shinyjs to add one for testing:

library(shiny)
library(shinyalert)
library(shinyjs)

ui <- fluidPage(
  useShinyalert(),
  useShinyjs(),
    
  tags$h2("Demo shinyalert"),
  tags$br(),
  fluidRow(
    column(
      width = 6,
      actionButton("shinyalert_popup",
                   "Alert Popup")
    )
  ),
  fluidRow(
    verbatimTextOutput(outputId = "return_value_1"),
    verbatimTextOutput(outputId = "return_value_2"),
    verbatimTextOutput(outputId = "return_value_3")
  )
)

server <- function(input, output, session) {
  
  observeEvent(input$shinyalert_popup, {
    shinyalert(
      title = "Input Number 1",
      text = tagList(
        "Enter a number",
        textInput(inputId = "shinyalert_value_1",
                  label = ""),
        "Enter another number",
        textInput(inputId = "shinyalert_value_2",
                  label = "")
      ),
      html = TRUE,
      inputId = "shinyalert_modal",
      showConfirmButton = TRUE,
      confirmButtonText = "OK",
      callbackR = function() {
        delay(371,
              shinyalert(
                title = "Input Number 3",
                text = tagList(
                  "Enter a final number",
                  textInput(inputId = "shinyalert_value_3",
                            label = "")
                ),
                html = TRUE,
                inputId = "shinyalert_callback",
                showConfirmButton = TRUE,
                confirmButtonText = "OK"
              )
        )
      }
    )
  })
  
  output$return_value_1 <- renderPrint(input$shinyalert_value_1)
  output$return_value_2 <- renderPrint(input$shinyalert_value_2)
  output$return_value_3 <- renderPrint(input$shinyalert_value_3)
}

shinyApp(ui = ui, server = server)

I narrowed the threshold between the input being available or not to 370ms. At a delay of 370ms the input shinyalert_value_3 cannot be accessed, at 371ms it can. Tested this on a couple of machines and results were consistnet in each case. I have done a bit of research but can't find anything that indicates 370ms is a significant value for anything.

JamesBrownSEAMS avatar Dec 17 '20 09:12 JamesBrownSEAMS

Further testing on the delay. Cutoffs for the major browsers:

  • FireFox (initial test browser) 370-371ms
  • Edge 370-371ms
  • IE11 376-377ms
  • Chrome 380-381ms

JamesBrownSEAMS avatar Dec 17 '20 14:12 JamesBrownSEAMS

Thanks for the testing. Though I'd be inclined to ignore the exact values, and instead look into solving the underlying problem rther than add a delay :)

daattali avatar Dec 17 '20 16:12 daattali

Agreed, I wanted to see if there was a consistent delay requirement that might point to some process in the browser (perhaps in JavaScript) that was causing the issue. Alas, I cannot find anything online that indicates there is anything special about these timings.

JamesBrownSEAMS avatar Dec 17 '20 16:12 JamesBrownSEAMS

That's interesting indeed that it's so consistent, I didn't expect that!

daattali avatar Dec 17 '20 17:12 daattali

Indeed, I had expected it to be semi-random within a deteminable range but I reran the tests a number of times under different conditions for the R environment and the numbers are consistent for each browser. This is what is making me think there is something at play that runs in the browser when the modal pops up that impacts the accessibility of the input value. Unfortunatly I have no idea what that might be...

JamesBrownSEAMS avatar Dec 17 '20 17:12 JamesBrownSEAMS

For a textInput, you can use the one of SweetAlert:

      callbackR = function() {
        shinyalert(
          type = "input",
          title = "Input Number 2",
          text = "Enter another number",
          inputId = "shinyalert_value_2",
          showConfirmButton = TRUE,
          confirmButtonText = "OK"
        )
      }

stla avatar Sep 23 '21 11:09 stla

Or you can use the JS callback:

      callbackR = function() {
        shinyalert(
          title = "Input Number 2",
          text = tagList(
            "Enter another number",
            textInput(inputId = "shinyalert_value_2",
                      label = "")
          ),
          callbackJS = 
            "function(){setTimeout(function(){var x = $('#shinyalert_value_2').val(); Shiny.setInputValue('shinyalert_value_2', x)});}",
          html = TRUE,
          inputId = "shinyalert_callback",
          showConfirmButton = TRUE,
          confirmButtonText = "OK"
        )
      }

stla avatar Sep 23 '21 11:09 stla

Other solution, with Shiny unbind/bind:

    shinyalert(
      title = "Input Number 1",
      text = tagList(
        "Enter a number",
        textInput(inputId = "shinyalert_value_1",
                  label = "")
      ),
      html = TRUE,
      inputId = "shinyalert_modal",
      showConfirmButton = TRUE,
      confirmButtonText = "OK",
      callbackJS = "function(){Shiny.unbindAll();}",
      callbackR = function() {
        shinyalert(
          title = "Input Number 2",
          text = tagList(
            "Enter another number",
            textInput(inputId = "shinyalert_value_2",
                      label = "")
          ),
          callbackJS = "function(){Shiny.bindAll();}",
          html = TRUE,
          inputId = "shinyalert_callback",
          showConfirmButton = TRUE,
          confirmButtonText = "OK"
        )
      }
    )

stla avatar Sep 23 '21 11:09 stla

@stla thanks for the input, that's useful! Perhaps unbind/bind is the key to solving this issue

daattali avatar Sep 23 '21 22:09 daattali

Thanks for finding this strange workaround. This issue remains one of the most, if not the most, severe bug in all of my packages. If anyone is able to find the issue and a clean solution I would be very grateful!

daattali avatar May 22 '23 14:05 daattali

@daattali If 100ms doesn't work and 500ms works, its probably related to #48

The problem is how sweetalert closing is defined.

initialize: function() {
  var service = this;
  var originalClose = this.__swal.close;
  this.__swal.close = function() {
    service.isClosing = true;
    originalClose();
    service.currentSwal = null;
    setTimeout(function() {
      service.onClosed();
    }, 400);
  };
}

There's a setTimeout that calls a function 400ms after you call alert.close().

ch0c0l8ra1n avatar Mar 23 '24 11:03 ch0c0l8ra1n

I never noticed that swalservice has this delay. That seems a bit irresponsible, to place that in a publicly used library, it seems like it's bound to create issues. Any idea why it's there, and if it's safe to change it to 0?

daattali avatar Mar 24 '24 05:03 daattali

@daattali The delay is there to help with animations as seen in sweetalert.dev.js which was not provided with shinyalert.

image

I can think of a pretty straightforward solution to handle pending swals that makes minor edits to the underlying library. We can make it so that swalservice waits till old sweet alert is completely closed before displaying the new sweet alert.

ch0c0l8ra1n avatar Mar 29 '24 09:03 ch0c0l8ra1n

I said earlier that I wouldn't want to modify the code of an external library, but if it's a small easy to understand change that has only a positive effect, and will be able to close multiple open issues, then yes please :)

daattali avatar Mar 29 '24 14:03 daattali

@daattali the issue, while related to the setTimeout, turned out to be slightly different than what I had thought. It was a problem of shiny not binding the inputs for swals that were queued by swalservice.js

It's simply fixed by adding a Shiny.bindAll after opening queued swals. #86 should fix this and #48

ch0c0l8ra1n avatar Apr 21 '24 17:04 ch0c0l8ra1n

Fixed in https://github.com/daattali/shinyalert/commit/510bb42fc605a9c9bfef9ff05b361c30310270a3 - thanks to @stla and @ch0c0l8ra1n

daattali avatar Apr 23 '24 15:04 daattali