【问题标题】:Is there a method to using removeUI in for loop?有没有在 for 循环中使用 removeUI 的方法?
【发布时间】:2020-09-10 16:28:30
【问题描述】:

我正在使用自定义函数 render_panels 动态生成输入,该函数创建一个 wellPanel,其中包含 selectizeInputactionButtonactionButton 使用 removeUI 删除整个 wellPanel 使用div 的 id 作为选择器。我还有一个全局添加按钮来添加新的wellPanel

我有一种方法可以通过观察每个面板的删除按钮事件来删除wellPanel,然后使用removeUI 并将相应的 div id 指定为选择器,但我想知道是否有更有效的方法使用 for 循环或矢量化方法。

编辑注意:我专门使用这种方法来代替insertUI,以便提供使用已插入面板来初始化应用程序的能力。例如,闪亮的应用程序将作为一个函数执行,用户可以在其中提供下拉选择值的字符向量。我在服务器中添加了一个字符向量prevInputs,一个反应值counter$n,它已经替换了input$add,以便创建length(prevInputs)的初始面板,如果!is.null(prevInputs)和一个初始化selected值参数的方法对于 selectizeInputmake_panels 中的现有值来说明这一点。

见代表:

library(shiny)


render_panels <- function(n, removed_panels, inputs){
  
  make_panels <- function(n, inputs){
    panels <- tags$div(id = n,
                       wellPanel(
                         selectizeInput(inputId = paste0("dropdown", n), label = paste0("dropdown", n), choices = c("a", "b", "c"), selected = inputs[[paste0("dropdown", n)]]),
                         actionButton(paste0("remove", n), label = paste0("remove", n))
                       )
    )
  }
  
  ui_out <- vector(mode = "list", length = n)
  
  for(i in seq_along(ui_out)){
    if(i %in% removed_panels) next
    ui_out[[i]] <- tagList(
      make_panels(n = i, inputs)
    )
  }
  
  return(ui_out)
}

ui <- fluidPage(
  fluidRow(
    column(width = 6,
           actionButton("add", label = "add"),
           uiOutput("mypanels")
    )
  )
)

server <- function(input, output, session){
  
  removed <- reactiveValues(
    values = list()
  )
  
  prevInputs <- c("a", "b", "c")
  
  reactiveInputs <- reactiveValues(values = list())
  
  observe({
    reactiveInputs$values$dropdown1 = prevInputs[[1]]
    reactiveInputs$values$dropdown2 = prevInputs[[2]]
    reactiveInputs$values$dropdown3 = prevInputs[[3]]
  })
  
  
  counter <- reactiveValues(n = ifelse(!is.null(prevInputs), length(prevInputs), 0))
  
  observeEvent(input$add, {
    counter$n <- counter$n + 1
  })
  
  observeEvent(input$remove1,{
    removed$values <- c(removed$values, 1)
    removeUI(
      selector =  "div#1",  immediate = TRUE,
    )
  }, once = TRUE)
  
  observeEvent(input$remove2,{
    removed$values <- c(removed$values, 2)
    removeUI(
      selector =  "div#2",  immediate = TRUE,
    )
  }, once = TRUE)
  
  observeEvent(input$remove3,{
    removed$values <- c(removed$values, 3)
    removeUI(
      selector =  "div#3",  immediate = TRUE,
    )
  }, once = TRUE)
  
  
  output$mypanels <- renderUI({
    render_panels(n = counter$n, removed_panels = removed$values, inputs = reactiveInputs$values)
  })
  
}

shinyApp(ui, server)

如您所见,如果生成了 100 个wellPanels,我将不得不使用 100 个observeEvent,这不是我们想要的……这是我对 for 循环的尝试:

我想将所有 observeEvent 调用替换为如下所示,但似乎无法正常工作。

observe({
    req(input$remove1)
    for(i in seq_len(input$add)){
      if(input[[paste0("remove", i)]] == 1){
        removeUI(selector = paste0("div#", i), immediate = TRUE)
      }
    }
  })

编辑: 这是使用 shinymaterial 包作为替代 UI 提供的答案的尝试。注意shinymaterial 包要求您将 ui 元素包装在 render_material_from_serverrenderUI 中,以用于在服务器端生成的任何 UI,即

output$dropdown <- renderUI({
    render_material_from_server(
        material_dropdown(input_id = paste0("dropdown", n), label = paste0("dropdown", n), choices = c("a", "b", "c"), selected = "a")
        )
    })

render_material_from_server这个函数是新推出的,只存在于GH上的当前开发版本包中:shinymaterial

在任何情况下,insertUI 都不会使用来自 shinymaterialmaterial_page UI 按预期呈现 UI 元素

library(shiny)
library(shinymaterial)

make_panels <- function(n, selected){
  tags$div(
    material_card(
      material_dropdown(input_id = paste0("dropdown", n), label = paste0("dropdown", n), choices = c("a", "b", "c"), selected = selected),
      actionButton(paste0("remove", n), label = paste0("remove", n), class = "mybtn")
    )
  )
}


ui <- material_page(
  tags$script("
        $(document).on('click', '.mybtn', function(){
          $(this).parent().remove();
        })
                "),
  material_row(
    material_column(width = 6,
           actionButton("add", label = "add"),
           uiOutput("mypanels")
    )
  )
)

server <- function(input, output, session){
  choices = c("a", "b", "c")
  init_counter <- reactiveVal(3)
  
  observe({
    for(i in seq_len(isolate(init_counter()))){
      insertUI(selector = "#mypanels", where = "beforeEnd", ui = make_panels(i, choices[i]))
    }
  })
  observeEvent(input$add, {
    panel_index <- init_counter() + input$add
    insertUI(selector = "#mypanels", where = "beforeEnd", ui = make_panels(panel_index, choices[panel_index]))
  })
}

shinyApp(ui, server)

【问题讨论】:

    标签: r shiny


    【解决方案1】:

    我认为这种情况对于modules 来说是一个很好的用例。基本上,您只需编写一次如何生成面板的代码,然后在每次需要新面板时调用此模块。在模块内部,observeEvent 是自动生成的,因此您不必重复代码。

    要添加的两件事:

    • 如果要访问模块返回的数据,需要在主服务器函数中store the output of the module call
    • 拥有大量模块会产生大量观察者。当模块 ui 被移除时,这些观察者也会留下来。如果出现问题,请参阅 this blog post 如何处理。
    library(shiny)
    
    mod_panel_ui <- function(id) {
      ns <- NS(id)
      panel_number <- regmatches(id,
                                 regexpr("[0-9]+", id))
      tags$div(id = id,
               wellPanel(
                 selectizeInput(inputId = ns("dropdown"),
                                label = paste0("dropdown ", panel_number),
                                choices = c("a", "b", "c"),
                                selected = NULL),
                 actionButton(ns("remove"), label = paste0("remove ", panel_number))
               )
      )
    }
    
    mod_panel <- function(id) {
      moduleServer(id,
                   function(input, output, session) {
                     observeEvent(input$remove, {
                       removeUI(selector = paste0("div#", id))
                     })
                   })
      
      return(list(
        dropdown = reactive(input$dropdown)
      ))
    }
    
    ui <- fluidPage(
      fluidRow(
        column(width = 6,
               actionButton("add", label = "add"),
               div(id = "add_panels_here")
        )
      )
    )
    
    server <- function(input, output, session) {
      counter_panels <- 1
      
      observeEvent(input$add, {
        current_id <- paste0("panel_", counter_panels)
        mod_panel(current_id)
        insertUI(selector = "#add_panels_here",
                 ui = mod_panel_ui(current_id))
        
        # update counter
        counter_panels <<- counter_panels + 1
      })
    }
    
    shinyApp(ui, server)
    

    编辑

    这是一个使用shinymaterial 的解决方案,并且在启动时已经显示了 2 个面板。所选元素可以通过模块服务器函数的附加参数指定:

    library(shiny)
    library(shinymaterial)
    
    mod_panel_ui <- function(id) {
      ns <- NS(id)
      uiOutput(ns("placeholder"))
    }
    
    mod_panel <- function(id, selection = NULL) {
      moduleServer(id,
                   function(input, output, session) {
                     # generate the UI on the server side
                     ns <- session$ns
                     panel_number <- regmatches(id,
                                                regexpr("[0-9]+", id))
                     output$placeholder <- renderUI({render_material_from_server(tags$div(id = id,
                                                              material_card(
                                                                material_dropdown(input_id = ns("dropdown"),
                                                                                  label = paste0("dropdown ", panel_number),
                                                                                  choices = c("a", "b", "c"),
                                                                                  selected = selection),
                                                                actionButton(ns("remove"), label = paste0("remove ", panel_number))
                                                              )
                     ))
                     })
                     
                     # remove the element
                     observeEvent(input$remove, {
                       removeUI(selector = paste0("div#", id))
                     })
                   })
      
      return(list(
        dropdown = reactive(input$dropdown)
      ))
    }
    
    ui <- material_page(
      material_row(
        material_column(width = 6,
                        actionButton("add", label = "add"),
                        div(id = "add_panels_here")
        )
      )
    )
    
    server <- function(input, output, session) {
      counter_panels <- 1
      panels_on_startup <- 2
      selected_on_startup <- c("b", "c")
      
      # add counters on startup
      lapply(seq_len(panels_on_startup), function(i) {
        current_id <- paste0("panel_", counter_panels)
        mod_panel(current_id, selected_on_startup[i])
        insertUI(selector = "#add_panels_here",
                 ui = mod_panel_ui(current_id))
        
        # update counter
        counter_panels <<- counter_panels + 1
      })
      
      observeEvent(input$add, {
        current_id <- paste0("panel_", counter_panels)
        mod_panel(current_id)
        insertUI(selector = "#add_panels_here",
                 ui = mod_panel_ui(current_id))
        
        # update counter
        counter_panels <<- counter_panels + 1
      })
    }
    
    shinyApp(ui, server)
    

    【讨论】:

    • 感谢您的回复以及您对模块如何在此处提供解决方案的说明。我使用我的方法的原因之一是允许用户使用已经生成的面板初始化应用程序(闪亮的应用程序将作为一个函数启动,并且用户可以提供一个字符向量作为函数的参数以初始化 n @987654328 @ 在应用程序启动时不依赖于observeEvent(input$add,{ 我不确定这是否可以通过您的模块方法完成?我已经更新了带有编辑说明和更新代表的原始帖子。再次感谢您的彻底回复。
    • 查看我的编辑。您可以在server 函数中生成非反应性代码,该函数仅在启动时执行一次
    • 感谢您的回答,使用您的方法,如何使用已经定义的值初始化启动时创建的前两个面板的选择,即如果我们在服务器中有panel_vals &lt;- c("b", "c"),我想初始化下拉1 个带有“b”的选择和下拉 2 个带有“c”的选择
    • 您可以将附加参数传递给模块服务器函数;查看我编辑的代码
    • 感谢您帮助我解决这个问题!请注意,初始面板确实按预期呈现,但是,添加新面板会产生问题。我能够通过在mod_panel 函数中包装render_material_from_server() 中的所有ui 元素来解决这个问题,即`renderUI({render_material_from_server(...)})`
    【解决方案2】:

    如果您了解一些 javascript,有一个非常简单的方法。

    • 没有必要使用for循环
    • 无需将内容保存在列表中。
    • 不需要renderUI
    • 无需观察每个面板

    你需要做的就是给remove按钮添加一个js监听器,并在R中添加一个类class = "mybtn"让js监听。

    $(document).on('click', '.mybtn', function(){
      $(this).parent().remove();
    })
    

    在您的服务器中,您需要反过来思考,使用insertUI 而不是removeUIadd 按钮只需要一名观察者。当你每次点击add时,给一个div添加一个面板。就我而言,我很懒,所以我直接选择你的uiOutput("mypanels")

    library(shiny)
    make_panels <- function(n){
        tags$div(
            wellPanel(
                selectizeInput(inputId = paste0("dropdown", n), label = paste0("dropdown", n), choices = c("a", "b", "c"), selected = NULL),
                actionButton(paste0("remove", n), label = paste0("remove", n), class = "mybtn")
            )
        )
    }
    
    
    ui <- fluidPage(
        tags$script("
            $(document).on('click', '.mybtn', function(){
              $(this).parent().remove();
            })
                    "),
        fluidRow(
            column(width = 6,
                   actionButton("add", label = "add"),
                   uiOutput("mypanels")
            )
        )
    )
    
    server <- function(input, output, session){
        observeEvent(input$add, {
            insertUI(selector = "#mypanels", where = "beforeEnd", ui = make_panels(input$add))
        })
        observe({
            print(input$dropdown5)
        })
    }
    
    shinyApp(ui, server)
    

    为了确保这能正常工作,我添加了一个测试观察者来观察dropdown5(添加第 5 个面板时的下拉菜单)。添加第 5 个面板后,您将在控制台中看到下拉值。

    编辑为您的笔记:

    您仍然可以使用预设面板进行插入。为您要启动的面板数量添加一个反应计数器。只要确保你 isolate 计数器和 choice 如果那也是反应性的。在我的示例中,choice 是硬编码的,所以我没有隔离。这是为了防止稍后运行面板初始化。我添加的观察只会运行一次。

    我还使用[] 而不是[[]],这会在超出边界时给出NA 而不是错误。

    library(shiny)
    make_panels <- function(n, selected){
        tags$div(
            wellPanel(
                selectizeInput(inputId = paste0("dropdown", n), label = paste0("dropdown", n), choices = c("a", "b", "c"), selected = selected),
                actionButton(paste0("remove", n), label = paste0("remove", n), class = "mybtn")
            )
        )
    }
    
    
    ui <- fluidPage(
        tags$script("
            $(document).on('click', '.mybtn', function(){
              $(this).parent().remove();
            })
                    "),
        fluidRow(
            column(width = 6,
                   actionButton("add", label = "add"),
                   uiOutput("mypanels")
            )
        )
    )
    
    server <- function(input, output, session){
        choices = c("a", "b", "c")
        init_counter <- reactiveVal(3)
        observe({
            for(i in seq_len(isolate(init_counter()))){
                insertUI(selector = "#mypanels", where = "beforeEnd", ui = make_panels(i, choices[i]))
            }
        })
        observeEvent(input$add, {
            panel_index <- init_counter() + input$add
            insertUI(selector = "#mypanels", where = "beforeEnd", ui = make_panels(panel_index, choices[panel_index]))
        })
    }
    
    shinyApp(ui, server)
    

    使用materialUI:

    tags$script()改成这个

    library(shiny)
    library(shinymaterial)
    
    make_panels <- function(n, selected){
        tags$div(
            material_card(
                material_dropdown(input_id = paste0("dropdown", n), label = paste0("dropdown", n), choices = c("a", "b", "c"), selected = selected),
                actionButton(paste0("remove", n), label = paste0("remove", n), class = "mybtn")
            )
        )
    }
    
    
    ui <- material_page(
        HTML("<script>
            $(document).on('click', '.mybtn', function(){
              $(this).parent().remove();
            })
            var formatDropdown = function() {
                function initShinyMaterialDropdown(callback) {
                    $('.shiny-material-dropdown').formSelect();
                    callback();
                }
        
                initShinyMaterialDropdown(function() {
        
                var shinyMaterialDropdown = new Shiny.InputBinding();
                $.extend(shinyMaterialDropdown, {
                  find: function(scope) {
                    return $(scope).find('select.shiny-material-dropdown');
                  },
                  getValue: function(el) {
                    var ans;
                    ans = $(el).val();
                    if (ans === null) {
                      return ans;
                    }
                    if (typeof(ans) == 'string') {
                      return ans.replace(new RegExp('_shinymaterialdropdownspace_', 'g'), ' ');
                    } else if (typeof(ans) == 'object') {
                      for (i = 0; i < ans.length; i++) {
                        if (typeof(ans[i]) == 'string') {
                          ans[i] = ans[i].replace(new RegExp('_shinymaterialdropdownspace_', 'g'), ' ');
                        }
                      }
                      return ans;
                    } else {
                      return ans;
                    }
                  },
                  subscribe: function(el, callback) {
                    $(el).on('change.shiny-material-dropdown', function(e) {
                      callback();
                    });
                  },
                  unsubscribe: function(el) {
                    $(el).off('.shiny-material-dropdown');
                  }
                });
        
                Shiny.inputBindings.register(shinyMaterialDropdown);
                });
                }
            $(document).ready(function(){
                setTimeout(formatDropdown, 500);
            })
            $(document).on('click', '#add', function(){
                setTimeout(formatDropdown, 100);
            })
    </script>"),
        material_row(
            material_column(width = 6,
                            actionButton("add", label = "add"),
                            uiOutput("mypanels")
            )
        )
    )
    
    server <- function(input, output, session){
        choices = c("a", "b", "c")
        init_counter <- reactiveVal(3)
        
        observe({
            for(i in seq_len(isolate(init_counter()))){
                insertUI(selector = "#mypanels", where = "beforeEnd", ui = make_panels(i, choices[i]))
            }
        })
        observeEvent(input$add, {
            panel_index <- init_counter() + input$add
            insertUI(selector = "#mypanels", where = "beforeEnd", ui = make_panels(panel_index, choices[panel_index]))
        })
    }
    
    shinyApp(ui, server)
    

    【讨论】:

    • 感谢您的回复,我忽略了使用 insertUI 来使用已生成面板初始化应用程序所需的某些功能。如果我是正确的,我认为这不能通过 insertUI 来实现?我已经更新了 reprex 并提供了编辑说明。
    • 感谢更新示例,对于我正在开发的实际闪亮应用程序,我一直在使用 shinymaterial 包中的材料设计组件,并且一直面临insertUI 的 UI 生成问题。我不想让事情进一步复杂化,并试图说明我在使用shiny ui 组件时遇到的问题,但似乎shinymaterialinsertUI 似乎不能很好地结合在一起。我在原始帖子中使用您的解决方案提供了shinymaterial 的尝试。
    • 感谢更新的脚本。我们快到了,但正如您所见,当将 fluidPage 更改为 material_page 和将 selectizeInput 更改为 material_dropdown 时,material_dropdown 不会按预期呈现。
    • 删除按钮的功能现在似乎也被破坏了。
    • 那是你的电脑和浏览器的问题。我用windows和linux测试过。一切都很完美。
    猜你喜欢
    • 2022-01-07
    • 1970-01-01
    • 2011-04-30
    • 2023-03-04
    • 2017-06-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多