Shiny golem模块化场景下rintrojs引导弹窗显示在左上角问题
rintrojs 模块化开发下引导弹窗固定在左上角的解决方法
问题背景
使用rintrojs包实现应用分步引导功能,应用基于golem框架采用模块化架构开发,每个标签页对应独立模块。触发引导流程时,所有引导对话框都显示在屏幕左上角,非模块化场景下的解决方案在当前架构下不生效。
问题效果参考:问题截图
可复现问题的示例代码如下:
library(shiny) library(rintrojs) library(shinyWidgets) # UI Module 1 mod_module1_ui <- function(id){ ns <- NS(id) tagList( rintrojs::introjsUI(), column( width = 12, actionButton( inputId = ns("bt"), label = "Display Button" ) ), div( sidebarPanel( style = "height: 100px;", width = 12, shiny::column( width = 3, rintrojs::introBox( shiny::numericInput( inputId = ns("numeric"), label = "Numeric Input", value = 45 ), data.step = 1, data.intro = div( h5("Description goes here") ) ), ), shiny::column( width = 3, rintrojs::introBox( shinyWidgets::pickerInput( inputId = ns("picker"), label = "Picker Input", choices = c(1, 2, 3, 4, 5) ), data.step = 2, data.intro = div( h5("Description goes here") ) ), ), ), ), ) } # SERVER Module 1 mod_module1_server <- function(id){ moduleServer( id, function(input, output, session){ ns <- session$ns observeEvent(input$bt, rintrojs::introjs(session)) }) } # UI Module 2 mod_module2_ui <- function(id){ ns <- NS(id) tagList( rintrojs::introjsUI(), column( width = 12, actionButton( inputId = ns("bt"), label = "Display Button" ) ), div( sidebarPanel( style = "height: 100px;", width = 12, shiny::column( width = 3, rintrojs::introBox( shiny::numericInput( inputId = ns("numeric"), label = "Numeric Input", value = 45 ), data.step = 1, data.intro = div( h5("Description goes here") ) ), ), shiny::column( width = 3, rintrojs::introBox( shinyWidgets::pickerInput( inputId = ns("picker"), label = "Picker Input", choices = c(1, 2, 3, 4, 5) ), data.step = 2, data.intro = div( h5("Description goes here") ) ), ), ), ), ) } # SERVER Module 2 mod_module2_server <- function(id){ moduleServer( id, function(input, output, session){ ns <- session$ns observeEvent(input$bt, rintrojs::introjs(session)) }) } # APP UI app_ui <- function(request) { tagList( shiny::navbarPage( title = ("Example"), fluid = TRUE, # 1 - Tab 1 ---- tabPanel( title = "tab1", shinydashboard::dashboardHeader( title = span( h1("Title tab 1") ) ), shinydashboard::dashboardBody( mod_module1_ui("module1_1") ), ), # 2 - Tab 2 ---- shiny::tabPanel( title = "tab2", shinydashboard::dashboardHeader( title = h1("Title tab 2") ), shinydashboard::dashboardBody( mod_module2_ui("module2_1") ), ), ) ) } # APP SERVER app_server <- function(input, output, session) { mod_module1_server("module1_1") mod_module2_server("module2_1") } shinyApp(app_ui, app_server)
故障原因
- 重复加载introjs依赖:
rintrojs::introjsUI()的作用是向页面注入intro.js所需的CSS和JS资源,全局只需要调用一次即可。示例中两个子模块都调用了该函数,导致JS事件重复绑定,元素定位逻辑冲突。 - 命名空间下元素查找失效:Shiny模块化开发时,所有模块内的元素ID都会自动加上模块命名空间前缀,旧版本
rintrojs默认全局查找带data-step属性的元素时,无法正确匹配模块内带命名空间的元素,定位失败后弹窗默认显示在页面左上角。
修复方案
按照以下两点修改即可解决问题:
- 删除所有子模块UI中的
rintrojs::introjsUI()调用,仅在全局app_ui的最外层调用一次,加载全局依赖。 - (兼容旧版本rintrojs可选配置)给每个模块的内容用
div包裹作为根容器并设置唯一ID,调用introjs时显式指定从当前模块的根容器内查找引导元素,避免全局匹配失效。
核心修改代码
首先修改全局UI,在顶层引入introjs依赖:
app_ui <- function(request) { tagList( # 全局只加载一次introjs依赖 rintrojs::introjsUI(), shiny::navbarPage( title = ("Example"), fluid = TRUE, # 原有tabPanel代码保持不变 tabPanel( title = "tab1", shinydashboard::dashboardHeader( title = span(h1("Title tab 1")) ), shinydashboard::dashboardBody( mod_module1_ui("module1_1") ), ), tabPanel( title = "tab2", shinydashboard::dashboardHeader( title = h1("Title tab 2") ), shinydashboard::dashboardBody( mod_module2_ui("module2_1") ), ), ) ) }
删除两个子模块UI里的rintrojs::introjsUI()行即可正常运行。如果仍存在定位问题,可按如下方式给模块加根容器限定查找范围,以模块1为例:
mod_module1_ui <- function(id){ ns <- NS(id) # 用div包裹模块全部内容,作为引导查找的根容器 div( id = ns("module_root"), column( width = 12, actionButton( inputId = ns("bt"), label = "Display Button" ) ), div( sidebarPanel( style = "height: 100px;", width = 12, shiny::column( width = 3, rintrojs::introBox( shiny::numericInput( inputId = ns("numeric"), label = "Numeric Input", value = 45 ), data.step = 1, data.intro = div(h5("Description goes here")) ), ), shiny::column( width = 3, rintrojs::introBox( shinyWidgets::pickerInput( inputId = ns("picker"), label = "Picker Input", choices = c(1, 2, 3, 4, 5) ), data.step = 2, data.intro = div(h5("Description goes here")) ), ), ), ) ) } mod_module1_server <- function(id){ moduleServer( id, function(input, output, session){ ns <- session$ns observeEvent(input$bt, { # 指定引导元素查找范围为当前模块根容器 rintrojs::introjs( session, options = list(scope = I(paste0("#", ns("module_root")))) ) }) }) }
模块2按照相同逻辑修改即可。
内容的提问来源于stack exchange,提问作者Guilherme Fonseca
相关产品推荐
相关产品推荐

