Search code examples
rdtkableextrareactable

How to implement expand/collapse rows in HTML table


I would like to make some rows expand on click, i.e. by clicking on the arrow. Is there a way in reactable to add java script code to specific rows? There exists and option for expanding/collapsing all rows on click in reactable (see here), but it does not do exactly what I need. Is there any reactable/javascript guru out there who has an idea how this might be feasible.

The required output is designed with kableExtra but I think my problem is more suited for reactable.

df_open <- tibble::tibble(
  Category = c("Fruits &#x25bc;&#xFE0E;", "Apple", "Banana",
               "Kiwi", "Vegetable &#x25bc;&#xFE0E;", "Carrots &#x25bc;&#xFE0E;", "Red Carrots", "Orange Carrots", "Diary"), 
  Value_sum = c(1:9), 
  Value_one = c(10:18),
  Value_two = c(200:208)
)

df_closed <- tibble::tibble(
  Category = c("Fruits &#x25b6;&#xFE0E","Vegetable &#x25b6;&#xFE0E;",  "Diary"), 
  Value_sum = c(1, 5,9), 
  Value_one = c(10,14,18),
  Value_two = c(200, 204,208)
)


library(kableExtra)

# all collapsed, everything shown
kbl(df_open, escape = FALSE) |> 
  kable_styling(bootstrap_options = "bordered") |> 
  row_spec(row = c(2:4, 6), extra_css = "padding-left:30px;") |> 
  row_spec(row = c(7,8), extra_css = "padding-left:50px")

Completely unfolded table: enter image description here

# all closed
kbl(df_closed, escape = FALSE) |> 
  kable_styling(bootstrap_options = "bordered") 

Completely folded table:

enter image description here

Raw Data:

df <- tibble::tibble(
  Category = c("Fruits", "Apple", "Banana", "Kiwi", "Vegetable", "Carrots", "Red Carrots", "Orange Carrots", "Diary"), 
  Value_sum = c(1:9), 
  Value_one = c(10:18),
  Value_two = c(200:208), 
  Another_column = "ABC"
)

My attempt using this blog post

library(dplyr)
library(reactable)
top_level <- df |> 
  mutate(id = c(1,1,1,1,2,2, 2,2,3),
         id_large = c(1, NA, NA, NA,2, NA, NA, NA, 3)) |> 
  filter(!is.na(id_large)) |> 
  relocate(id, .before = 1) |> 
  select(-id_large)

second_level <- df |> 
  mutate(id = c(1,1,1,1,2,2, 2,2,3),
         id_large = c(1, NA, NA, NA,2, NA, NA, NA, 3)) |> 
  filter(is.na(id_large)) |> 
  relocate(id, .before = 1) |> 
  select(-id_large)


reactable(
  data       = top_level,
  compact    = TRUE, 
  striped    = TRUE, 
  resizable  = TRUE, 
  columns    = list(
  id   = colDef(name = "ID", show = FALSE)),
  details = function(index) { # index is the row number of current row.
    sec_lvl = second_level[second_level$id == top_level$id[index], ] 
    reactable(data       = sec_lvl,
              compact    = TRUE, 
              bordered   = TRUE, 
              resizable  = TRUE,
              colna
              columns    = list(
                id   = colDef(name = "ID", show = FALSE))
    )
  }
)

enter image description here


Solution

  • I'm not sure this is currently possible with {reactable} - but it looks like an open feature request: https://github.com/glin/reactable/issues/147

    The {DT} package might be able to do something like this. Using the code in this blog post: https://laustep.github.io/stlahblog/posts/DT_childTables.html you can create something similar. In the example below, the NestedData function and callback are copied directly from the blog post.

    The idea is to store your data as a nested list. Ideally, you'd have some column in your df data frame that identifies what level the table is and use that to subset it, rather than manually creating the different data frames as I've done here.

    main <- tibble::tribble(
      ~Category,          ~Value_sum, ~Value_one, ~Value_two, ~Another_column,
      "Fruits",                 1,        10,       200, "ABC", 
      "Vegetable",              5,        14,       204, "ABC",  
      "Dairy",                  9,        18,       208, "ABC",  
    )
    
    fruits <- tibble::tribble(
      ~Category,          ~Value_sum, ~Value_one, ~Value_two, ~Another_column,
      "Apple",                 2,        11,       200, "ABC", 
      "Banana",                3,        12,       201, "ABC",  
      "Kiwi",                  4,        13,       202, "ABC",  
    ) 
    
    veg <- tibble::tribble(
      ~Category,          ~Value_sum, ~Value_one, ~Value_two, ~Another_column,
      "Carrots",                 6,        15,       205, "ABC"
    )
    
    carrots <- tibble::tribble(
      ~Category,          ~Value_sum, ~Value_one, ~Value_two, ~Another_column,
      "Red Carrots",                 2,        11,       200, "ABC", 
      "Oraneg Carrots",                3,        12,       201, "ABC"
      ) 
    
    Dat <- NestedData(
      dat = main, 
      children = list(
        fruits, 
        list(  
          veg, 
          children = list(
            carrots
          )
        ), 
        data.frame(NULL)
      )
    )
    
    library(DT)
    datatable(
      Dat, 
      callback = callback, rownames = rowNames, escape = -colIdx-1,
      options = list(
        paging = FALSE,
        searching = FALSE,
        columnDefs = list(
          list(
            visible = FALSE, 
            targets = ncol(Dat)-1+colIdx
          ),
          list(
            orderable = FALSE, 
            className = "details-control", 
            targets = colIdx
          ),
          list(
            className = "dt-center", 
            targets = "_all"
          )
        )
      )
    )
    

    gives something like this:

    enter image description here