0

I'm using the rhandsontable package in a Shiny dashboard and want to add a custom "Copy row" option to the context menu, where users can copy a row and insert the duplicate row into the table (i.e., copy a full row and insert the duplicate below the original row). This helps prevent errors and saves users time by editing fewer cells in the new row. Unfortunately, I have no experience with JavaScript and am not sure how to create this option. I'm following this related question on custom menu options, but it hasn't received any responses yet.

How can I add a custom context menu option for duplicating a table row and inserting it into a new row below? Thanks for your help!

Reprex


library(shiny)
library(shinyjs)
library(htmlwidgets)
library(tidyverse)
library(rhandsontable)
library(datasets)

ui <- fluidPage(
  titlePanel("Edit Iris"),
      mainPanel(rHandsontableOutput("iris_table"))
)

server <- function(input, output, session) {
  
  data(iris)
  summary(iris)
  
  copy_row_js <- "function (key, selection, clickEvent) {
                    this.alter('insert_row');
                    this.render();
                  }"
  
  output$iris_table <- renderRHandsontable({
    
    table <- rhandsontable(iris,
                           rowHeaders = FALSE) %>%
      hot_context_menu(allowRowEdit = TRUE, 
                       allowColEdit = FALSE,
                       customOpts = list(
                         copy_row = list(
                           name = "Copy row",
                           callback = htmlwidgets::JS(copy_row_js))
                       )
      )
    
    table
    
  })
  
}

shinyApp(ui = ui, server = server)

Update

I couldn't figure out how to add this option to the context menu, so I created a "Copy Row" button to achieve the same function using reactiveValues (cheating, but I needed a workaround). This answer on accessing rhandsontable values was very helpful. One new challenge: suddenly the reactive() filter is not re-running after changing the filter input. I created a reprex, but bizarrely this reprex seems to work (?), since the iris dataset filter is re-run after changing the selected species input. The filter in the more complex app is not re-run. Any advice on what could be going on?

library(shiny)
library(shinyjs)
library(htmlwidgets)
library(tidyverse)
library(rhandsontable)
library(datasets)

data("iris")
summary(iris)

ui <- fluidPage(
  titlePanel("Edit Iris"),
  sidebarLayout(
    sidebarPanel(
      selectizeInput("iris_species", "Iris Species", 
                  choices = iris$Species,
                  multiple = FALSE,
                  selected = (iris$Species == "setosa")),
      br(),
      actionButton("copy_row", "Copy Row"),
      br(),
      actionButton("reset", "Reset"),
      br(),
    ),
  mainPanel(rHandsontableOutput("iris_table"))
  )
)

server <- function(input, output, session) {
  
  data("iris")
  summary(iris)
  
  df <- reactive({
    
    input$iris_species
    
    data("iris")
    summary(iris)
    
    df <- iris %>%
      filter(Species == input$iris_species) %>%
      arrange(Species, Sepal.Length)
    df
  })
  
  iris_values <- reactiveValues(data = as.data.frame(NULL))
  
  # Beginning state: filter data by input$iris_species
  observeEvent(input$iris_species, {
      iris_values$data <- df()
  })
  
  # Reset to initially displayed df(), deleting any user changes
  observeEvent(input$reset, {
    iris_values$data <- df()
  })
  
  # Copy row for additional Sepal.Length entries by user
  observeEvent(input$copy_row, {
    
    row_select <- input$iris_table_select$select$r
    
    copy_row <- as.data.frame(hot_to_r(req(input$iris_table))) %>%
      slice(row_select)
    
    iris_values$data <- as.data.frame(hot_to_r(req(input$iris_table))) %>%
      bind_rows(copy_row) %>%
      arrange(Species, Sepal.Length)
  })
  
  # Output handsontable
  output$iris_table <- renderRHandsontable({
    
    table <- rhandsontable({iris_values$data},
                           rowHeaders = FALSE,
                           height = 500,
                           width = 1000,
                           selectCallback = TRUE) %>%
      hot_context_menu(allowRowEdit = TRUE,
                       allowColEdit = FALSE
      )
    
    table

  })
}

shinyApp(ui = ui, server = server)

RESOLVED

Was hunting around for an answer to the "not re-running" problem and discovered this answer, describing how dependents on validated reactives might not pass on the validation error. Success! I find the "copy" button quite useful--keeping it posted in case of interest.

szmsp
  • 1
  • 1

0 Answers0