在一个Shiny App中,若是点击一个按钮,每每意味着一些R代码会被执行。若是这段代码执行时间很短,用户体验不会受到影响;若是这段代码执行时间很长,界面上若是不提供给用户一些即时的反馈,就会让用户感到困惑。html
本文从Github上找到了做者daattali的一个做品,专门用来提高长时运算按钮的点击体验。当点击一个按钮后,按钮状态转为disabled,同时显示处于计算状态;计算成功后,返回执行成功额标识;计算失败时,返回具体的失败信息。源代码的连接请点这里git
本文对这段代码进行详细地解读,须要读者有必定的HTML和shinyjs基础。github
withBusyIndicatorUI <- function(button) { id <- button[['attribs']][['id']] # 使用str(actionButton("test", "test"))查看Button的结构,是一个长度为3的list,其中一个元素是名为attribs的list,里面包含id、type和class属性 div( `data-for-btn` = id, # 为div建立一个attribute,取值为button id,这样方便CSS Selector对其进行查询 button, # 等价于 <button id="test" type="button" class="btn btn-default action-button">test</button> span( class = "btn-loading-container", hidden( strong("loading...", class = "btn-loading-indicator"),, icon("check", class = "btn-done-indicator") ) ), # 内联元素,会跟在button的右侧显示,初始状态为隐藏,用于显示正在执行和执行成功 hidden( div(class = "btn-err", div(icon("exclamation-circle"), tags$b("Error: "), span(class = "btn-err-msg") ) ) ) # 块级元素,会在button的下侧显示,初始状态为隐藏,用于显示执行错误的消息 ) }
withBusyIndicatorServer <- function(buttonId, expr) { # UX stuff: show the "busy" message, hide the other messages, disable the button # 构造CSS选择器,根据attribute定位按钮,根据class获取按钮所处的状态 loadingEl <- sprintf("[data-for-btn=%s] .btn-loading-indicator", buttonId) doneEl <- sprintf("[data-for-btn=%s] .btn-done-indicator", buttonId) errEl <- sprintf("[data-for-btn=%s] .btn-err", buttonId) # 使按钮失效 shinyjs::disable(buttonId) # 显示正在执行部分 shinyjs::show(selector = loadingEl) # 隐藏执行成功部分 shinyjs::hide(selector = doneEl) # 隐藏执行失败部分 shinyjs::hide(selector = errEl) # 执行完成后须要调用的函数:使按钮有效,隐藏正在执行部分 on.exit({ shinyjs::enable(buttonId) shinyjs::hide(selector = loadingEl) }) # Try to run the code when the button is clicked and show an error message if # an error occurs or a success message if it completes tryCatch({ # 执行按钮点击后的expr value <- expr # 显示执行成功 shinyjs::show(selector = doneEl) # 延时两秒后,隐藏执行成功 shinyjs::delay(2000, shinyjs::hide(selector = doneEl, anim = TRUE, animType = "fade", time = 0.5)) # 返回执行结果 value }, error = function(err) { errorFunc(err, buttonId) }) } errorFunc <- function(err, buttonId) { errEl <- sprintf("[data-for-btn=%s] .btn-err", buttonId) errElMsg <- sprintf("[data-for-btn=%s] .btn-err-msg", buttonId) errMessage <- err$message shinyjs::html(html = errMessage, selector = errElMsg) shinyjs::show(selector = errEl, anim = TRUE, animType = "fade") }
library(shiny) library(shinyjs) ui <- fluidPage( useShinyjs(), tags$style(appCSS), selectInput("select", "Select an option", c("This one is okay" = "ok", "This will give an error" = "error")), # Wrap the button in the function `withBusyIndicatorUI()` withBusyIndicatorUI( actionButton( "uploadFilesBtn", "Process data", class = "btn-primary" ) ) ) server <- function(input, output, session) { observeEvent(input$uploadFilesBtn, { # When the button is clicked, wrap the code in a call to `withBusyIndicatorServer()` withBusyIndicatorServer("uploadFilesBtn", { Sys.sleep(1) if (input$select == "error") { stop("choose another option") } }) }) } shinyApp(ui = ui, server = server)