Ввод текста в DT :: datatable отменяет привязку, и я не могу связать его - PullRequest
2 голосов
/ 06 февраля 2020

Я работаю над блестящим приложением, которое позволяет пользователям вводить комментарии о наблюдении. Затем комментарии сохраняются в базе данных SQL на серверной части. Приведенный ниже код является рабочим представлением моего текущего приложения.

То, что происходит, - это загрузка таблиц с подмножеством Cylinder = 4 (переключатели), пользователь может сохранять комментарии, получая Cylinder = 6, сохранить комментарии, а затем Cylinder = 8 и сохранить комментарии. Но если я когда-нибудь верну цилиндр обратно к значению, в котором я уже сохранил комментарии, ввод текста будет свободным, и комментарии не будут сохранены. Для того, чтобы восстановить функционал, я должен перезапустить приложение. Я обнаружил, что это раздражает моих пользователей.

Что мне нужно сделать, чтобы продолжать сохранять комментарии, если я go вернусь к значению цилиндра, которое я уже использовал?

Мне жаль, что это не очень краткий пример. При вводе комментария консоль напечатает количество сохраненных комментариев и отобразит измененный фрейм данных, чтобы вы могли сравнить то, что отображается в приложении.

library(shiny)
library(DT)
library(dplyr)

mtcars$comment <- rep("", nrow(mtcars))
mtcars$row_id <- seq_len(nrow(mtcars))
AppData <- split(mtcars, mtcars[c("cyl", "am")])

# Makes a text input column out of a data frame
make_inputtable <- function(df){
  df$comment <- 
    mapply(
      function(comment, id){
        as.character(textInput(inputId = sprintf("txt_comment_%s", id), 
                               label = "", 
                               value = comment))
      }, 
      comment = df$comment, 
      id = df$row_id, 
      SIMPLIFY = TRUE)

  df
}

ui <- shinyUI(
  fluidPage(
    radioButtons(inputId = "rdo_cyl", 
                 label = "Cylinders", 
                 choices = sort(unique(mtcars$cyl)), 
                 inline = TRUE), 

    h3("Automatic"), 
    actionButton(inputId = "btn_save_automatic", 
                 label = "Save Comments"),
    DT::dataTableOutput("am0"),

    hr(),

    h3("Manual"), 
    actionButton(inputId = "btn_save_manual", 
                 label = "Save Comments"),
    DT::dataTableOutput("am1"), 

    # unbind a datatable. Needs to be done before a table is redrawn.
    tags$script(HTML(
      "Shiny.addCustomMessageHandler('unbind-DT', function(id) {
          Shiny.unbindAll($('#'+id).find('table').DataTable().table().node());
          })"))
  )
)


server <- shinyServer(function(input, output, session){
  reactiveData <- reactiveValues(
    am0_cyl4 = AppData[["4.0"]],
    am0_cyl6 = AppData[["6.0"]], 
    am0_cyl8 = AppData[["8.0"]],
    am1_cyl4 = AppData[["4.1"]],
    am1_cyl6 = AppData[["6.1"]], 
    am1_cyl8 = AppData[["8.1"]]
  ) 

  # Reactive Objects ------------------------------------------------

  ref0 <- reactive({
    sprintf("am0_cyl%s", input$rdo_cyl)
  })

  data0 <- reactive({
    reactiveData[[ref0()]]
  })

  ref1 <- reactive({
    sprintf("am1_cyl%s", input$rdo_cyl)
  })

  data1 <- reactive({
    reactiveData[[ref1()]]
  })

  # Event Observers -------------------------------------------------

  observeEvent(
    input$btn_save_automatic, 
    {
      in_field <- names(input)[grepl("^txt_comment_", names(input))]
      in_field_id <- sub("^txt_comment_", "", in_field)
      in_field_id <- as.numeric(in_field_id)
      in_field_id <- in_field_id[in_field_id %in% data0()$row_id]

      exist_frame <- data0()[c("row_id", "comment")]
      new_frame <- 
        data.frame(
          row_id = in_field_id, 
          comment = vapply(in_field_id, 
                           function(id){ input[[sprintf("txt_comment_%s", id)]]}, 
                           character(1)), 
          stringsAsFactors = FALSE)

      Compare <- left_join(exist_frame, 
                           new_frame, 
                           by = "row_id", 
                           suffix = c("_exist", "_new")) %>% 
        filter(comment_exist != comment_new)

      message(sprintf("* %s comment(s) saved", nrow(Compare)))

      # Only perform the save operations if there are changes to be made.
      if (nrow(Compare)){
        session$sendCustomMessage("unbind-DT", "am0")

        for(i in seq_len(nrow(Compare))){
          row <- Compare$row_id
          reactiveData[[ref0()]]$comment[reactiveData[[ref0()]]$row_id == row] <- 
            input[[sprintf("txt_comment_%s", row)]]
        }
        print(data0())
      }

    }
  )

  # Very similar to btn_save_automatic
  observeEvent(
    input$btn_save_manual, 
    {
      in_field <- names(input)[grepl("^txt_comment_", names(input))]
      in_field_id <- sub("^txt_comment_", "", in_field)
      in_field_id <- as.numeric(in_field_id)
      in_field_id <- in_field_id[in_field_id %in% data1()$row_id]

      exist_frame <- data1()[c("row_id", "comment")]
      new_frame <- 
        data.frame(
          row_id = in_field_id, 
          comment = vapply(in_field_id, 
                           function(id){ input[[sprintf("txt_comment_%s", id)]]}, 
                           character(1)), 
          stringsAsFactors = FALSE)

      Compare <- left_join(exist_frame, 
                           new_frame, 
                           by = "row_id", 
                           suffix = c("_exist", "_new")) %>% 
        filter(comment_exist != comment_new)

      message(sprintf("* %s comment(s) saved", nrow(Compare)))

      # Only perform the save operations if there are changes to be made.
      if (nrow(Compare)){
        session$sendCustomMessage("unbind-DT", "am1")

        for(i in seq_len(nrow(Compare))){
          row <- Compare$row_id
          reactiveData[[ref1()]]$comment[reactiveData[[ref1()]]$row_id == row] <- 
            input[[sprintf("txt_comment_%s", row)]]
        }
        print(data1())
      }

    }
  )


  # Output Objects --------------------------------------------------

  output$am0 <-
    DT::renderDataTable({
      make_inputtable(data0()) %>%
        datatable(escape = -13, 
                  options = list(preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
                                 drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
    })

  output$am1 <-
    DT::renderDataTable({
      make_inputtable(data1()) %>%
        datatable(escape = -13, 
                  options = list(preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
                                 drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
    })


})

shinyApp(ui = ui, server = server)

Изменения и обновления

editable таблицы данных являются потенциальным решением, но потребуют обновления нашей библиотеки пакетов. В настоящее время мы используем R 3.4.1 с shiny 1.0.4 и DT 0.2.12.

Да, это сравнительно древний. Но стоимость обновления значительна, учитывая чувствительность отчетов, поддерживаемых этим приложением, и гарантию качества, требуемую при любом обновлении.

Ответы [ 2 ]

1 голос
/ 10 февраля 2020

Оставляя в стороне ваши ограничения версии, вот как я бы подошел к этому с последней library(DT) версией (надеюсь, полезной для будущих читателей и, возможно, когда-нибудь вы тоже обновитесь):

Редактировать : теперь используется dataTableProxy, чтобы избежать повторного рендеринга.

library(shiny)
library(DT)

ui <- shinyUI(
  fluidPage(
    radioButtons(inputId = "rdo_cyl", 
                 label = "Cylinders", 
                 choices = sort(unique(mtcars$cyl)), 
                 inline = TRUE), 
    h3("Automatic"), 
    actionButton(inputId = "btn_save_automatic", 
                 label = "Save Comments"), p(),
    DTOutput("am0"),
    hr(),
    h3("Manual"), 
    actionButton(inputId = "btn_save_manual", 
                 label = "Save Comments"), p(),
    DTOutput("am1")
  )
)

server <- shinyServer(function(input, output, session){
  globalData <- mtcars
  globalData$comment <- rep("", nrow(mtcars))
  globalData$row_id <- seq_len(nrow(mtcars))

  diabledCols <- grep("comment", names(globalData), invert = TRUE)
  AppData <- reactiveVal(globalData)

  automaticAppData <- reactive({
    AppData()[AppData()[["cyl"]] %in% input$rdo_cyl & AppData()[["am"]] %in% "0", ]
  })

  manualAppData <- reactive({
    AppData()[AppData()[["cyl"]] %in% input$rdo_cyl & AppData()[["am"]] %in% "1", ]
  })

  output$am0 <- DT::renderDT(
    # isolate: render only once
    expr = {isolate(automaticAppData())},
    editable = list(target = "cell", disable = list(columns = diabledCols))
  )

  output$am1 <- DT::renderDT(
    # isolate: render only once
    expr = {isolate(manualAppData())},
    editable = list(target = "cell", disable = list(columns = diabledCols))
  )

  observeEvent(input$btn_save_automatic, {
    info = input$am0_cell_edit
    str(info)
    i = automaticAppData()$row_id[[info$row]]
    j = info$col
    v = info$value
    globalData[i, j] <<- DT::coerceValue(v, globalData[i, j])
    AppData(globalData)
    # update database...
  })

  observeEvent(input$btn_save_manual, {
    info = input$am1_cell_edit
    str(info)
    i = manualAppData()$row_id[[info$row]]
    j = info$col
    v = info$value
    globalData[i, j] <<- DT::coerceValue(v, globalData[i, j])
    AppData(globalData)
    # update database...
  })

  am0Proxy <- dataTableProxy("am0")
  am1Proxy <- dataTableProxy("am1")

  observeEvent(automaticAppData(), {
    replaceData(am0Proxy, automaticAppData(), resetPaging = FALSE)
  })

  observeEvent(manualAppData(), {
    replaceData(am1Proxy, manualAppData(), resetPaging = FALSE)
  })

})

shinyApp(ui = ui, server = server)

Result

Здесь - некоторые связанная информация.


Обновление для DT версии 0.2

Вот еще одно решение, более близкое к вашему исходному коду. Я использую isolate(), dataTableProxy() и replaceData(), которые доступны с DT версии 0.2 , чтобы избежать повторного рендеринга таблицы, что решает проблему с привязкой и должно быть быстрее.

Другая проблема в вашем коде заключалась в том, что вы дважды вызывали session$sendCustomMessage("unbind-DT", "am0") вместо того, чтобы использовать его для "am1".

library(shiny)
library(DT)
library(dplyr)

mtcars$comment <- rep("", nrow(mtcars))
mtcars$row_id <- seq_len(nrow(mtcars))
AppData <- split(mtcars, mtcars[c("cyl", "am")])

# Makes a text input column out of a data frame
make_inputtable <- function(df){
  df$comment <- 
    mapply(
      function(comment, id){
        as.character(textInput(inputId = sprintf("txt_comment_%s", id), 
                               label = "", 
                               value = comment))
      }, 
      comment = df$comment, 
      id = df$row_id, 
      SIMPLIFY = TRUE)

  df
}

ui <- shinyUI(
  fluidPage(
    radioButtons(inputId = "rdo_cyl", 
                 label = "Cylinders", 
                 choices = sort(unique(mtcars$cyl)), 
                 inline = TRUE), 

    h3("Automatic"), 
    actionButton(inputId = "btn_save_automatic", 
                 label = "Save Comments"),
    DT::dataTableOutput("am0"),

    hr(),

    h3("Manual"), 
    actionButton(inputId = "btn_save_manual", 
                 label = "Save Comments"),
    DT::dataTableOutput("am1"),

    # unbind a datatable. Needs to be done before a table is redrawn.
    tags$script(HTML(
      "Shiny.addCustomMessageHandler('unbind-DT', function(id) {
          Shiny.unbindAll($('#'+id).find('table').DataTable().table().node());
          })"))
  )
)


server <- shinyServer(function(input, output, session){
  reactiveData <- reactiveValues(
    am0_cyl4 = AppData[["4.0"]],
    am0_cyl6 = AppData[["6.0"]], 
    am0_cyl8 = AppData[["8.0"]],
    am1_cyl4 = AppData[["4.1"]],
    am1_cyl6 = AppData[["6.1"]], 
    am1_cyl8 = AppData[["8.1"]]
  ) 

  # Reactive Objects ------------------------------------------------

  ref0 <- reactive({
    sprintf("am0_cyl%s", input$rdo_cyl)
  })

  data0 <- reactive({
    reactiveData[[ref0()]]
  })

  ref1 <- reactive({
    sprintf("am1_cyl%s", input$rdo_cyl)
  })

  data1 <- reactive({
    reactiveData[[ref1()]]
  })

  # Event Observers -------------------------------------------------

  observeEvent(
    input$btn_save_automatic, 
    {
      in_field <- names(input)[grepl("^txt_comment_", names(input))]
      in_field_id <- sub("^txt_comment_", "", in_field)
      in_field_id <- as.numeric(in_field_id)
      in_field_id <- in_field_id[in_field_id %in% data0()$row_id]

      exist_frame <- data0()[c("row_id", "comment")]
      new_frame <- 
        data.frame(
          row_id = in_field_id, 
          comment = vapply(in_field_id, 
                           function(id){ input[[sprintf("txt_comment_%s", id)]]}, 
                           character(1)), 
          stringsAsFactors = FALSE)

      Compare <- left_join(exist_frame, 
                           new_frame, 
                           by = "row_id", 
                           suffix = c("_exist", "_new")) %>% 
        filter(comment_exist != comment_new)

      message(sprintf("* %s comment(s) saved", nrow(Compare)))

      # Only perform the save operations if there are changes to be made.
      if (nrow(Compare)){
        session$sendCustomMessage("unbind-DT", "am0")

        for(i in seq_len(nrow(Compare))){
          row <- Compare$row_id
          reactiveData[[ref0()]]$comment[reactiveData[[ref0()]]$row_id == row] <- 
            input[[sprintf("txt_comment_%s", row)]]
        }
        print(data0())
      }

    }
  )

  # Very similar to btn_save_automatic
  observeEvent(
    input$btn_save_manual, 
    {
      in_field <- names(input)[grepl("^txt_comment_", names(input))]
      in_field_id <- sub("^txt_comment_", "", in_field)
      in_field_id <- as.numeric(in_field_id)
      in_field_id <- in_field_id[in_field_id %in% data1()$row_id]

      exist_frame <- data1()[c("row_id", "comment")]
      new_frame <- 
        data.frame(
          row_id = in_field_id, 
          comment = vapply(in_field_id, 
                           function(id){ input[[sprintf("txt_comment_%s", id)]]}, 
                           character(1)), 
          stringsAsFactors = FALSE)

      Compare <- left_join(exist_frame, 
                           new_frame, 
                           by = "row_id", 
                           suffix = c("_exist", "_new")) %>% 
        filter(comment_exist != comment_new)

      message(sprintf("* %s comment(s) saved", nrow(Compare)))

      # Only perform the save operations if there are changes to be made.
      if (nrow(Compare)){
        session$sendCustomMessage("unbind-DT", "am1")

        for(i in seq_len(nrow(Compare))){
          row <- Compare$row_id
          reactiveData[[ref1()]]$comment[reactiveData[[ref1()]]$row_id == row] <- 
            input[[sprintf("txt_comment_%s", row)]]
        }
        print(data1())
      }

    }
  )


  # Output Objects --------------------------------------------------

  output$am0 <-
    DT::renderDataTable({
      # isolate: render table only once!
      make_inputtable(isolate(data0())) %>%
        datatable(escape = -13, 
                  options = list(preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
                                 drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
    }, server = TRUE)

  output$am1 <-
    DT::renderDataTable({
      # isolate: render table only once!
      make_inputtable(isolate(data1())) %>%
        datatable(escape = -13, 
                  options = list(preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
                                 drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
    }, server = TRUE)

  am0Proxy <- dataTableProxy("am0")
  am1Proxy <- dataTableProxy("am1")

  observeEvent(data0(), {
    replaceData(am0Proxy, make_inputtable(data0()), resetPaging = FALSE)  # important
  }, ignoreInit = TRUE)

  observeEvent(data1(), {
    replaceData(am1Proxy, make_inputtable(data1()), resetPaging = FALSE)  # important
  }, ignoreInit = TRUE)

})

shinyApp(ui = ui, server = server)
0 голосов
/ 10 февраля 2020

Вы отменяете привязку слишком рано или слишком поздно, я не уверен из размещенного вами фрагмента кода. Можете ли вы сделать вместо этого несколько объектов одного типа для привязки?

Редактировать:

Я считаю эту строку подозрительной:

# unbind a datatable. Needs to be done before a table is redrawn.
 tags$script(HTML(
   "Shiny.addCustomMessageHandler('unbind-DT', function(id) {


    Shiny.unbindAll($('#'+id).find('table').DataTable().table().node());
           })"))   )

Похоже, что вы отменяете привязку дважды и переплет только один раз.

...