glin / reactable

Interactive data tables for R
https://glin.github.io/reactable
Other
613 stars 79 forks source link

Cell function not applied with changing contents on second table page #227

Open eikeschott opened 2 years ago

eikeschott commented 2 years ago

I'm trying to render a custom <div> using html = TRUE and manipulate that <div> with a JS function when hovering the cell.

Here is a shiny example:

shiny::shinyApp(ui = shiny::fluidPage(reactable::reactableOutput(outputId = "table")),
                server = function(input, output, session){

                  data <- tibble::tibble(words = do.call(paste0, replicate(8, sample(LETTERS, 20, TRUE), FALSE))) |>
                    dplyr::rowwise() |>
                    dplyr::mutate(
                      same_content = as.character(
                        shiny::div(class = "same", style = "background-color: yellow", "HOVER ME")
                      ),
                      diff_content = as.character(
                        shiny::div(class = "diff", style = "background-color: yellow", sprintf("HOVER ME: %s", words))
                      )
                    )

                  output$table <- reactable::renderReactable({
                    reactable::reactable(data = data, 
                                         columns = list(
                                           words = reactable::colDef(show = FALSE),
                                           same_content = reactable::colDef(html = TRUE, cell = htmlwidgets::JS(
                                             '
                                             $(function() {
                                                         $(".same").hover(
                                                           function() {
                                                             $(this).attr("style", "background-color: green");
                                                           },
                                                           function() {
                                                             $(this).attr("style", "background-color: yellow");
                                                           }
                                                         );
                                                       })
                                             '
                                           )
                                           ),
                                           diff_content = reactable::colDef(html = TRUE, cell = htmlwidgets::JS(
                                             '
                                             $(function() {
                                                         $(".diff").hover(
                                                           function() {
                                                             $(this).attr("style", "background-color: green");
                                                           },
                                                           function() {
                                                             $(this).attr("style", "background-color: yellow");
                                                           }
                                                         );
                                                       })
                                             '
                                           ))
                                         )
                    )
                  })
                }
)

On the first page of the table everything works as expected. When navigating to the second page only divs in the first column are changing their background color. The second column is stuck with yellow. When switching back to the first page the previously working second column does not change it's background color any more.

Is there a way to "re-apply" the function after switching to the second page of the table or are there any better ways to manipulate cell contents on hover?

glin commented 2 years ago

Hi, there are two different questions here, so I'll answer them separately:

Is there a way to "re-apply" the function after switching to the second page of the table

The cell render function arguments take a JavaScript function like JS("function(cellInfo) { ... }"). The cell arguments in the example app aren't functions, but expressions that call a function (wrapped in $()). Since the cell argument isn't a function, I guess the JS expression is evaluated once when the table loads, then ignored. If you convert the cell code to a function, it should get rerun on page changes:

cell = JS(
  'function(cellInfo) {
     $(function() {
       $(".same").hover(
         function() {
           $(this).attr("style", "background-color: green");
         },
         function() {
           $(this).attr("style", "background-color: yellow");
         }
       );
     })
    return cellInfo.value;
   }'
)

And also see more examples in the Cell rendering - JavaScript render function docs.

However, a caveat to note is that the cell render functions don't work well with operations that manipulate the DOM, like these jQuery functions that add hover events to cells. The hover styles happen to work for the second page of the same_content column because reactable optimizes for fast rendering behind the scenes - if a cell's content hasn't changed, then that cell won't be rerendered unnecessarily. The cell render functions were meant to be used as pure data transformations, so this was more like an oversight. I know it can be useful to have some code that manipulates the DOM when cells are rendered, so it's a possible enhancement for the future, and I'll note this issue as a feature request.

For now, the best supported way to render dynamic content is to create an HTML widget for that content using the htmlwidgets package. An HTML widget will always be rendered correctly by reactable, and will also work out of the box with dynamic UI outputs in Shiny.

You can try wrapping your cell code in a render function first, and it might just work. But if it doesn't, and if creating an HTML widget is too complicated or not feasible, then there are undocumented ways to work around this problem in reactable. A similar issue came up before in the past (https://github.com/glin/reactable/issues/55#issuecomment-655268365), and I can explain more if needed.


are there any better ways to manipulate cell contents on hover?

Yes! If you're just manipulating styles, I'd definitely recommend adding hover styles through CSS alone. It will be so much simpler and more performant than doing it through JavaScript. If you add a class to some element, you can then add hover styles for it using the :hover CSS pseudo-class. For example, if a cell has a my-cell class on it, the CSS might look like:

/* Default styles for the cell */
.my-cell {
  background-color: yellow;
}

/* Hover-only styles for the cell */
.my-cell:hover {
  background-color: green;
}

Here's a minimal example app to demonstrate this:

library(reactable)
library(shiny)

ui <- fluidPage(
  tags$head(
    tags$style("
      .model-cell {
        background-color: yellow;
      }

      .model-cell:hover {
        background-color: green;
      }
    ")
  ),
  reactableOutput("table")
)

server <- function(input, output) {
  data <- MASS::Cars93[1:5, c("Manufacturer", "Model", "Type", "Min.Price")]

  output$table <- renderReactable({
    reactable(
      data,
      columns = list(
        Model = colDef(class = "model-cell")
      )
    )
  })
}

shinyApp(ui, server)