reactable icon indicating copy to clipboard operation
reactable copied to clipboard

Example: Excel download through context menu (not csv but xlsx)

Open daeyoonlee opened this issue 3 years ago • 2 comments

I wanted to share the method I use for downloading reactable to excel. Right-click on the table to bring up the context menu.

image

shiny app

library(shiny)
library(reactable)

ui <- fluidPage(
  tags$script(src = "https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.15.6/xlsx.mini.min.js"), # excel export library
  includeScript(path = "app.js"),
  includeCSS(path = "app.css"),
  reactableOutput("test")
)

server <- function(input, output, session) {
  output$test <- renderReactable(
    reactable(
      data = iris
    )
  )
}

shinyApp(ui, server)

app.js

document.addEventListener("contextmenu", function (event) {
  try {
    if (event.target.closest(".reactable") != null) {
      event.preventDefault();
      let id = event.target.closest(".reactable").id;
      let menu = document.createElement("div");
      menu.classList.add("context_menu");
      menu.style = `top:${event.pageY-5-window.pageYOffset}px;left:${event.pageX-5-window.pageXOffset}px`;
      menu.onmouseleave = function() { menu.outerHTML = ''; };
      menu.innerHTML = ("<div onclick ='JS_reactable_excel_download(\"" + id + "\")'>Excel Download</div>");
      document.body.appendChild(menu);
    }
  } catch {};
},false);

function JS_reactable_excel_download(id) {
  var json = Reactable.getInstance(id).preGroupedRows.map(row => row.original);
  for (let row = 0; row < json.length; row++) {
      for (key of Object.keys(json[row])) {
        try { 
          json[row][key] = json[row][key].replace(/<\/?[^>]+>/gi, ''); // Remove html tag if included
        } catch {};
      }
  }
  var ws = XLSX.utils.json_to_sheet(json);
  var wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
  XLSX.writeFile(wb, id+'.xlsx'); 
}

css

.context_menu {
  position: fixed;
  background: #fff;
  color: #333;
  cursor: pointer;
  padding: 5px 0px;
  border: 1px solid #ccc;
  box-shadow: rgba(0, 0, 0, 0.5) 2.4px 2.4px 3.2px;
  z-index: 111;
}

.context_menu > div {
  padding: 4px 20px;
  margin: 0;
  font-size: 12px;
}

.context_menu > div:hover {
  background: #edeeee;
}

daeyoonlee avatar Nov 17 '22 13:11 daeyoonlee

That's pretty cool, thanks for sharing! I didn't know about the xlsx library (or really SheetJS). It looks like a lightweight library for creating XLSX files which would definitely be useful to a lot of people. I might adapt a simplified version of this for an example at some point.

And also FYI, this is using some undocumented reactable APIs that may change without notice in the future. But I can see how preGroupedRows would be useful for cases like this, so that could be a publicly supported state property in the future.

glin avatar Dec 18 '22 00:12 glin

Yes. In fact, there seem to be a few more undocumented functions, but they don't seem official, so I only referred to the pregroupedrows used in your csv conversion logic.

daeyoonlee avatar Dec 20 '22 09:12 daeyoonlee