23

I'm looking for a way to trigger an event based upon closing of a Shiny modal when the easy-close option is TRUE (so clicking outside the modal closes it). Since there is no ID linked to a modal, I can't seem to catch this event. I tried wrapping the modal in an 'observe' event, but this only triggers the opening but not closing of the modal.


example: I want to trigger an event if this modal closes by clicking outside of it, so not the dismiss button. The code below only triggers when opening...

library(shiny)
ui <- fluidPage(
  fluidRow(
    actionButton(inputId = "enterText", label = "Enter name", align = "left"),
    h1(textOutput("myOutput"))
  )
)

server <- function(input, output, session) {
  myText <- reactiveValues(input = "...")
  myModal = modalDialog(h3("Enter a string, then click outside this modal to close and display the text"),
                        textInput(inputId = "myString", label = "Enter a string: "),
                        title = "Input", easyClose = TRUE, footer = modalButton("Dismiss"))
  test = observe(myModal)

  #Open the modal when button clicked
  observeEvent(input$enterText,{
    showModal(myModal)
    })

  #Observe the modal, should fire when it CLOSES by clicking outside the modal (easy-close)
  observeEvent(test, {
    myText$input = input$myString
    print("observed")
  }, ignoreInit = T)
  
  output$myOutput = renderText(myText$input)

}

shinyApp(ui = ui, server = server)
Ian Campbell
  • 23,484
  • 14
  • 36
  • 57
pieterjanvc
  • 273
  • 2
  • 7
  • You could have one `observeEvent` that opens the modal and than trigger this event. It will trigger this other event before the user closes the modal. But if this event is slow, it might be annoying for the user since he won't be able to close the modal untill this other process has ended... – user5029763 Jul 21 '17 at 20:08
  • Thanks but I don't really understand what you mean. I've updated my code example above, but this only triggers opening and not closing of the modal – pieterjanvc Jul 21 '17 at 20:43
  • Please provide a minimal example https://stackoverflow.com/help/mcve – user5029763 Jul 24 '17 at 13:37
  • HI, thank you for the response. The code above is the minimal example I could come up with. It does not do what it should at this point though: It prints 'OBSERVED' when the modal opens, but I want the action to happen when it closes by using the easy-close (clicking outside the modal to close it). – pieterjanvc Jul 24 '17 at 19:50
  • You want this modal to appear only when your app is initializing? – user5029763 Jul 24 '17 at 19:54
  • No, in my real application the modal is called as a result of a user action (clicking something). They then perform an action (entering data) within the modal and when they close it, I need to manipulate that data. But I want them to be able to close by clicking a button or just outside of the modal and trigger the same action. Thanks so much for helping! – pieterjanvc Jul 24 '17 at 20:37
  • I've tried researching other websites / shiny manual but still no luck ... – pieterjanvc Jul 28 '17 at 19:52
  • Your code is too vague... the reason I asked if the modal was supposed to appear only when initializing is because that's what your code is doing right now. There are no triggers (the user can't even interact with the app)... I can't really tell how to fix your code because I can't understand what it was supposed to be doing.... – user5029763 Jul 28 '17 at 20:39
  • Ok, I made a hopefully better example (see above). If the user clicks the button, the modal opens and you can enter text. I want the textOutput to update this value only when the modal is closed by clicking outside of it (so the easy-close method) – pieterjanvc Jul 28 '17 at 22:55
  • 4
    Maybe [this](https://github.com/ebailey78/shinyBS/issues/39) link would help you. – SBista Aug 02 '17 at 12:06
  • Hi SBista. The link you sent is looking very promising!! I need to play a bit more with the code but at first sight the difference is this is written for the package BSmodal. I use the built-in modal function. I will need to figure out with what I could replace the next code line: $("#myModal").on("hidden.bs.modal", function (event). Still, I hope we are getting somewhere... – pieterjanvc Aug 02 '17 at 22:01
  • did you sort anything out here @pieterjanvc – moman822 Jul 15 '20 at 18:17
  • 1
    @moman822 I left another approach below, which is using only basic shiny. – ismirsehregal Apr 05 '23 at 10:45

3 Answers3

16

I want to provide an easier answer for anyone who doesn't require this to work with easyClose = TRUE, as the OP does -- in that case, this is a much easier solution than SBista's link in the comments.

This simply replaces the modal's default "dismiss" button with an action button of your own, which allows you to implement some additional action when you close the modal. This is set via the footer = argument.

In this example, the modal contains checkboxes, which don't take effect until the modal is closed.

library(shiny)
shinyApp(
  ui <- fluidPage(
    fluidRow(
      ## Button to display modal:
      actionButton(inputId = "display_modal",label = "Display modal"),
      ## Print the choices that were made in the modal:
      h1(textOutput("checked_letters"))
    )
  ),

  server <- function(input, output) {
    ## These values allow the actions made in the modal to be delayed until the
    #  modal is closed
    values = reactiveValues(to_print = "",   ## This is the text that will be displayed
                            modal_closed=F)  ## This prevents the values$to_print output from 
                                             #  updating until the modal is closed

    ## Open the modal when button clicked
    observeEvent(input$display_modal,{
      values$modal_closed <- F
      showModal(modalDialog(
        checkboxGroupInput("checkboxes",label = "Select letters",
                           choices = LETTERS[1:7]),
        ## This footer replaces the default "Dismiss" button,
        #  which is 'footer = modalButton("Dismiss")'
        footer = actionButton("dismiss_modal",label = "Dismiss")
        ))
    })

    ## This event is triggered by the actionButton inside the modalDialog
    #  It closes the modal, and by setting values$modal_closed <- T, it
    #  triggers values$to_print to update.
    observeEvent(input$dismiss_modal,{
      values$modal_closed <- T
      removeModal()
    })
    ## values$to_print is only updated once the modal is closed.
    observe({
      if(values$modal_closed){
        values$to_print <- paste(input$checkboxes)
      }
    })
    ## Forward the values$to_print to the UI
    output$checked_letters = renderText({values$to_print})
  }
)
sssheridan
  • 690
  • 1
  • 6
  • 15
  • This is indeed a way to only update a modal after it's closed. I'm actually using this method at the moment since it's a very easy implementation. Unfortunately, I want the easy-close to be TRUE and have the same effect as using a button, hence this whole post. – pieterjanvc Aug 15 '17 at 21:49
2

Similar to the first answer, my approach only works with easyClose = FALSE. Instead of using an actionButton together with an observeEvent we could create a custom function modalActionButton, which is basically copy paste from actionButton and just adding data-dismiss = "modal":

I adapt the example from the first answer below:

library(shiny)

# this is basically copied from actionButton() and just "`data-dismiss` = "modal" 
# from modalButton() is added:
modalActionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
  
  value <- restoreInput(id = inputId, default = NULL)
  tags$button(id = inputId, type = "button", style = if (!is.null(width)) 
    paste0("width: ", validateCssUnit(width), ";"), type = "button", 
    class = "btn btn-default action-button", `data-dismiss` = "modal", `data-val` = value, 
    list(shiny:::validateIcon(icon), label), ...)
  
}

shinyApp(

    ui <- fluidPage(
    fluidRow(
      ## Button to display modal:
      actionButton(inputId = "display_modal",label = "Display modal"),
      
      ## Print the choices that were made in the modal:
      h1(textOutput("checked_letters"))
    )
  ),
  
  server <- function(input, output) {
    
    values = reactiveValues(to_print = "")   ## This is the text that will be displayed

    observeEvent(input$display_modal, {
      showModal(modalDialog(
        checkboxGroupInput("checkboxes",label = "Select letters",
                           choices = LETTERS[1:7]),

        title = "This is a modal page with a 'modalActionButton' ",
        size = "l",
        easyClose = FALSE,
        # here is the modalActionButton
        footer = modalActionButton("close",
                                   "Close")
      ))
    })

    observeEvent(input$close, {
        values$to_print <- input$checkboxes
      })

    ## Forward the values$to_print to the UI
    output$checked_letters <- renderText({values$to_print})

   }
)
TimTeaFan
  • 17,549
  • 4
  • 18
  • 39
2

I'm a little late - however, I recently had to solve a similar problem.

We can trigger an input in shiny via listening on the JS events provided by bootstrap's modal class:

Event Type Description
show.bs.modal This event fires immediately when the show instance method is called. If caused by a click, the clicked element is available as the relatedTarget property of the event.
shown.bs.modal This event is fired when the modal has been made visible to the user (will wait for CSS transitions to complete). If caused by a click, the clicked element is available as the relatedTarget property of the event.
hide.bs.modal This event is fired immediately when the hide instance method has been called.
hidden.bs.modal This event is fired when the modal has finished being hidden from the user (will wait for CSS transitions to complete).
loaded.bs.modal This event is fired when the modal has loaded content using the remote option.

The following also works with easyClose = TRUE:

library(shiny)
ui <- fluidPage(
  tags$script(HTML(
    "$(document).on('shown.bs.modal','#shiny-modal', function () {
       Shiny.setInputValue(id = 'modal_visible', value = true);
      });
     $(document).on('hidden.bs.modal','#shiny-modal', function () {
       Shiny.setInputValue(id = 'modal_visible', value = false);
     });"
  )),
  fluidRow(
    actionButton(
      inputId = "enterText",
      label = "Enter name",
      align = "left"
    ),
    h1(textOutput("myOutput"))
  ))

server <- function(input, output, session) {
  myText <- reactiveValues(input = "...")
  myModal <- modalDialog(
    h3(
      "Enter a string, then click outside this modal to close and display the text"
    ),
    textInput(inputId = "myString", label = "Enter a string: "),
    title = "Input",
    easyClose = TRUE,
    footer = modalButton("Dismiss")
  )
  
  # Open the modal when button clicked
  observeEvent(input$enterText, {
    showModal(myModal)
  })
  
  observe({print(input$modal_open)})
  
  # Observe the modal, should fire when it CLOSES by clicking outside the modal (easy-close)
  observeEvent({input$modal_visible == FALSE}, {
    myText$input <- input$myString
  }, ignoreInit = TRUE)
  
  output$myOutput = renderText({myText$input})
  
}

shinyApp(ui = ui, server = server)

Furthermore, this answer was useful to get here, as this approach:

$('#myModal').on('hidden.bs.modal', function (e) {
  // do something...
})

doesn't work with basic shiny.

Please note @Ali's comment:

When you use $(document).on(... you attach your listener to the document context, no matter if your target element exists or not.

ismirsehregal
  • 30,045
  • 5
  • 31
  • 78