diff --git a/R/modal.R b/R/modal.R index 9b15188cf..939edeb28 100644 --- a/R/modal.R +++ b/R/modal.R @@ -1,4 +1,7 @@ -#' Show a modal dialog +#' Show or remove a modal dialog +#' +#' This causes a modal dialog to be displayed in the client browser, and is +#' typically used with \code{\link{modalDialog}}. #' #' @param ui UI content to show in the modal. #' @param session The \code{session} object passed to function given to @@ -15,8 +18,6 @@ showModal <- function(ui, session = getDefaultReactiveDomain()) { deps = res$deps ) ) - - invisible() } #' @rdname showModal @@ -28,14 +29,124 @@ removeModal <- function(session = getDefaultReactiveDomain()) { #' Create a modal dialog UI #' +#' This creates the UI for a modal dialog, using Bootstrap's modal class. Modals +#' are typically used for showing important messages, or for presenting UI that +#' requires input from the user, such as a username and password input. +#' #' @param ... UI elements for the body of the modal dialog box. #' @param title An optional title for the dialog. -#' @param footer UI for footer. Use \coode{NULL} for no footer. +#' @param footer UI for footer. Use \code{NULL} for no footer. #' @param dismissable If \code{TRUE}, the modal dialog can be dismissed by #' clicking outside the dialog box, or be pressing the Escape key. If #' \code{FALSE} (the default), the modal dialog can't be dismissed in those #' ways; instead it must be dismissed by clicking on the dismiss button, or #' from a call to \code{\link{removeModal}} on the server. +#' +#' @examples +#' if (interactive()) { +#' # Display an important message that can be dismissed only by clicking the +#' # dismiss button. +#' shinyApp( +#' ui = basicPage( +#' actionButton("show", "Show modal dialog") +#' ), +#' server = function(input, output) { +#' observeEvent(input$show, { +#' showModal(modalDialog( +#' title = "Important message", +#' "This is an important message!" +#' )) +#' }) +#' } +#' ) +#' +#' +#' # Display a message that can be dismissed by clicking outside the modal dialog, +#' # or by pressing Esc. +#' shinyApp( +#' ui = basicPage( +#' actionButton("show", "Show modal dialog") +#' ), +#' server = function(input, output) { +#' observeEvent(input$show, { +#' showModal(modalDialog( +#' title = "Somewhat important message", +#' "This is a somewhat important message.", +#' dismissable = TRUE, +#' footer = NULL +#' )) +#' }) +#' } +#' ) +#' +#' +#' # Display a modal that requires valid username and password input. +#' shinyApp( +#' ui = basicPage( +#' actionButton("show", "Show modal dialog"), +#' verbatimTextOutput("loginInfo") +#' ), +#' server = function(input, output) { +#' # A string with the current login status. This is in a reactiveValues +#' # object so that it can trigger reactivity. +#' vals <- reactiveValues(loginStatus = "Not logged in.") +#' +#' # Attempt logging in with a username and password, returning TRUE if +#' # successful and FALSE if not. +#' login <- function(username, password) { +#' # In a real-world use case, this would check against some sort of user +#' # database instead of just checking that the values are identical to +#' # hard-coded values. +#' if (identical(username, "user1") && identical(password, "pass1")) { +#' vals$loginStatus <- paste0('Logged in as "', username, '"') +#' return(TRUE) +#' +#' } else { +#' vals$loginStatus <- "Not logged in." +#' return(FALSE) +#' } +#' } +#' +#' # Return the UI for a modal dialog with username/password inputs. +#' # If 'failed' is TRUE, then display a message that the previous username +#' # and password were invalid. +#' loginModal <- function(failed = FALSE) { +#' modalDialog( +#' textInput("username", "Username"), +#' passwordInput("password", "Password"), +#' span('(Try logging in with "user1" and "pass1")'), +#' if (failed) +#' div(tags$b("Invalid username/password", style = "color: red;")), +#' +#' footer = tagList( +#' modalButton("Cancel"), +#' actionButton("login", "Log in") +#' ) +#' ) +#' } +#' +#' observeEvent(input$show, { +#' showModal(loginModal()) +#' }) +#' +#' # When login button is pressed, attempt to log in. If successful, remove the +#' # modal. If not show another modal, but this time with a failure message. +#' observeEvent(input$login, { +#' if (login(input$username, input$password)) { +#' removeModal() +#' } else { +#' showModal(loginModal(failed = TRUE)) +#' } +#' }) +#' +#' # Display current login status +#' output$loginInfo <- renderText({ +#' vals$loginStatus +#' }) +#' } +#' ) +#' +#' } #' @export modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"), dismissable = FALSE) { @@ -53,7 +164,7 @@ modalDialog <- function(..., title = NULL, footer = modalButton("Dismiss"), if (!is.null(footer)) div(class = "modal-footer", footer) ) ), - tags$script("$('#shiny-modal').modal().focus(); ") + tags$script("$('#shiny-modal').modal().focus();") ) } diff --git a/man/modalDialog.Rd b/man/modalDialog.Rd index 4f5f1be3f..d33f1b8f8 100644 --- a/man/modalDialog.Rd +++ b/man/modalDialog.Rd @@ -12,7 +12,7 @@ modalDialog(..., title = NULL, footer = modalButton("Dismiss"), \item{title}{An optional title for the dialog.} -\item{footer}{UI for footer. Use \coode{NULL} for no footer.} +\item{footer}{UI for footer. Use \code{NULL} for no footer.} \item{dismissable}{If \code{TRUE}, the modal dialog can be dismissed by clicking outside the dialog box, or be pressing the Escape key. If @@ -21,6 +21,114 @@ ways; instead it must be dismissed by clicking on the dismiss button, or from a call to \code{\link{removeModal}} on the server.} } \description{ -Create a modal dialog UI +This creates the UI for a modal dialog, using Bootstrap's modal class. Modals +are typically used for showing important messages, or for presenting UI that +requires input from the user, such as a username and password input. +} +\examples{ +if (interactive()) { +# Display an important message that can be dismissed only by clicking the +# dismiss button. +shinyApp( + ui = basicPage( + actionButton("show", "Show modal dialog") + ), + server = function(input, output) { + observeEvent(input$show, { + showModal(modalDialog( + title = "Important message", + "This is an important message!" + )) + }) + } +) + + +# Display a message that can be dismissed by clicking outside the modal dialog, +# or by pressing Esc. +shinyApp( + ui = basicPage( + actionButton("show", "Show modal dialog") + ), + server = function(input, output) { + observeEvent(input$show, { + showModal(modalDialog( + title = "Somewhat important message", + "This is a somewhat important message.", + dismissable = TRUE, + footer = NULL + )) + }) + } +) + + +# Display a modal that requires valid username and password input. +shinyApp( + ui = basicPage( + actionButton("show", "Show modal dialog"), + verbatimTextOutput("loginInfo") + ), + server = function(input, output) { + # A string with the current login status. This is in a reactiveValues + # object so that it can trigger reactivity. + vals <- reactiveValues(loginStatus = "Not logged in.") + + # Attempt logging in with a username and password, returning TRUE if + # successful and FALSE if not. + login <- function(username, password) { + # In a real-world use case, this would check against some sort of user + # database instead of just checking that the values are identical to + # hard-coded values. + if (identical(username, "user1") && identical(password, "pass1")) { + vals$loginStatus <- paste0('Logged in as "', username, '"') + return(TRUE) + + } else { + vals$loginStatus <- "Not logged in." + return(FALSE) + } + } + + # Return the UI for a modal dialog with username/password inputs. + # If 'failed' is TRUE, then display a message that the previous username + # and password were invalid. + loginModal <- function(failed = FALSE) { + modalDialog( + textInput("username", "Username"), + passwordInput("password", "Password"), + span('(Try logging in with "user1" and "pass1")'), + if (failed) + div(tags$b("Invalid username/password", style = "color: red;")), + + footer = tagList( + modalButton("Cancel"), + actionButton("login", "Log in") + ) + ) + } + + observeEvent(input$show, { + showModal(loginModal()) + }) + + # When login button is pressed, attempt to log in. If successful, remove the + # modal. If not show another modal, but this time with a failure message. + observeEvent(input$login, { + if (login(input$username, input$password)) { + removeModal() + } else { + showModal(loginModal(failed = TRUE)) + } + }) + + # Display current login status + output$loginInfo <- renderText({ + vals$loginStatus + }) + } +) + +} } diff --git a/man/showModal.Rd b/man/showModal.Rd index 021c15b16..0332036ba 100644 --- a/man/showModal.Rd +++ b/man/showModal.Rd @@ -3,7 +3,7 @@ \name{showModal} \alias{removeModal} \alias{showModal} -\title{Show a modal dialog} +\title{Show or remove a modal dialog} \usage{ showModal(ui, session = getDefaultReactiveDomain()) @@ -16,7 +16,8 @@ removeModal(session = getDefaultReactiveDomain()) \code{shinyServer}.} } \description{ -Show a modal dialog +This causes a modal dialog to be displayed in the client browser, and is +typically used with \code{\link{modalDialog}}. } \seealso{ \code{\link{modalDialog}} for examples. diff --git a/srcjs/modal.js b/srcjs/modal.js index ea684451f..532192819 100644 --- a/srcjs/modal.js +++ b/srcjs/modal.js @@ -1,19 +1,29 @@ exports.modal = { + // Show a modal dialog. This is meant to handle two types of cases: one is + // that the content is a Bootstrap modal dialog, and the other is that the + // content is non-Bootstrap. Bootstrap modals require some special handling, + // which is coded in here. show: function({ html='', deps=[] } = {}) { - // Get existing DOM element for this ID, or create if needed. + + // If there was an existing Bootstrap modal, then there will be a modal- + // backdrop div that was added outside of the modal wrapper, and it must be + // removed; otherwise there can be multiple of these divs. + $('.modal-backdrop').remove(); + + // Get existing wrapper DOM element, or create if needed. let $modal = $('#shiny-modal-wrapper'); if ($modal.length === 0) { $modal = $('
'); $('body').append($modal); - } - // When the inner modal is hidden, remove the entire thing, including - // wrapper. This is meant to work with Bootstrap modal dialogs. - $modal.on('hidden.bs.modal', function() { - exports.unbindAll($modal); - $modal.remove(); - }); + // If the wrapper's content is a Bootstrap modal, then when the inner + // modal is hidden, remove the entire thing, including wrapper. + $modal.on('hidden.bs.modal', function() { + exports.unbindAll($modal); + $modal.remove(); + }); + } // Set/replace contents of wrapper with html. exports.renderContent($modal, { html: html, deps: deps }); @@ -22,14 +32,11 @@ exports.modal = { remove: function() { const $modal = $('#shiny-modal-wrapper'); - // Look for a Bootstrap modal dialog inside the modal wrapper - const $bsmodal = $modal.find(".modal"); - - if ($bsmodal.length > 0) { - // If the modal wrapper contains a Bootstrap modal dialog, trigger hide - // event. This will trigger the hidden.bs.modal callback taht we set in - // show(), which unbinds and removes the element. - $bsmodal.modal('hide'); + // Look for a Bootstrap modal and if present, trigger hide event. This will + // trigger the hidden.bs.modal callback that we set in show(), which unbinds + // and removes the element. + if ($modal.find('.modal').length > 0) { + $modal.find('.modal').modal('hide'); } else { // If not a Bootstrap modal dialog, simply unbind and remove it.