glin / reactable

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

Updating selected row based on user input #20

Open tbradley1013 opened 4 years ago

tbradley1013 commented 4 years ago

Hi,

Is there any way to update the selected or expanded row in a reactable based on another user input?

A simple example can be shown below showing the desired functionality as it can be done in the DT package with datatables. In this example, the reactable is shown on the left hand side while the datatable is shown on the right. In both of these examples, if a row is selected than the respective selectInput and textOutput are updated. However, on the datatable side when the selectInput is updated the selected row in the datatable changes. This is done using the dataTableProxy and selectRows functions. Is there a way to do something similar to this with the reactable? This would be an incredibly useful feature if available! Also, in addition to changing which row is selected, if there was a way to change which row is expanded, that would also be very useful!

Example:

library(shiny)
library(reactable)
library(tidyverse)
library(DT)

ui <- shinyUI(
  fluidPage(
    column(
      width = 6,
      selectInput(
        inputId = "test_select_react",
        label = "Select Row",
        choices = 1:nrow(mtcars)
      ),
      reactableOutput("test_table_react"),
      div(
        h5("Selected Rows\n", textOutput("rows_selected_react")) 
      )
    ),
    column(
      width = 6,
      selectInput(
        inputId = "test_select_dt",
        label = "Select Row",
        choices = 1:nrow(mtcars)
      ),
      dataTableOutput("test_table_dt"),
      div(
        h5("Selected Rows\n", textOutput("rows_selected_dt"))
      )
    )

  )
)

server <- shinyServer(
  function(input, output, session){
    session$onSessionEnded(stopApp)

    output$test_table_react <- renderReactable({
      reactable(
        mtcars,
        selection = "single",
        selectionId = "selection"
      )
    })

    observe({
      # req(input$test_table_selected)
      updateSelectInput(
        session, 
        "test_select_react",
        selected = input$selection
      )
    })

    output$rows_selected_react <- renderText(input$selection)

    output$test_table_dt <- renderDataTable({
      datatable(
        mtcars, 
        rownames = FALSE,
        selection = "single"
      )
    })

    test_proxy <- dataTableProxy("test_table_dt")

    observeEvent(input$test_select_dt, {
      selectRows(test_proxy, input$test_select_dt)
    })

    observe({
      updateSelectInput(
        session, 
        "test_select_dt",
        selected = input$test_table_dt_rows_selected
      )
    })

    output$rows_selected_dt <- renderText(input$test_table_dt_rows_selected)

  }
)

shinyApp(ui, server)

Thanks for the great package!

tbradley1013 commented 4 years ago

FYI - I have also posted this question on RStudio Community

analytichealth commented 4 years ago

Have you had any luck with this @tbradley1013 ? I have a similar issue whereby I need to control the selected rows in a reactable.

Follow these steps with the example below to see what I mean.

  1. Select all rows in Table 1 (Table 2 is then displayed)
  2. Select "a" in Table 2
  3. Unselect "a" in Table 1.

You will see the tick is just passed to "b" in Table 2 as the selectionId tableid2 remains a value of 1. I have tried a few ideas (commented out below) to programatically change the value of tableid2, with no luck.

Ideally I need to deselect all rows in Table 2 when any rows in Table 1 change.

library(reactable)
# reactable   * 0.1.0.9000 2019-12-13 [1] Github (glin/reactable@566a4ba)
library(shiny)
library(data.table)

my_data <- data.table(col1 = letters[1:5])

ui <- shinyUI(
    fluidPage(
        # shinyjs::useShinyjs(),
        column(4,
            h5('Table 1'),      
        reactableOutput("test_table_react1")
            ),
        column(4,
        h5('Table 2'),      
        reactableOutput("test_table_react2")
            )
        )
)

server <- shinyServer(function(input, output, session) {

      output$test_table_react1 <- renderReactable({
        reactable(
            data = my_data,
            selection = "multiple",
            selectionId = "tableid1",
            onClick = "select",
            defaultSelected = NULL,
            fullWidth = FALSE
        )
    })

     observeEvent(input$tableid1, {
        # shinyjs::reset(id = 'tableid2')
        # output$test_table_react2 <- renderReactable({NULL})
        # HTML('Shiny.setInputValue("tableid2", NULL);')
        print(input$tableid2)
     }) 

    output$test_table_react2 <- renderReactable({
        req(input$tableid1) 
        reactable(
            data = my_data[input$tableid1, ],
            selection = "multiple",
            selectionId = "tableid2",
            onClick = "select",
            defaultSelected = NULL,
            fullWidth = FALSE
        )
    })

})

shinyApp(ui, server)
glin commented 4 years ago

Hi, there isn't a way to do this in the current release version of reactable, but I've been thinking about adding something like a proxy to make this possible. In the development version on GitHub, there's a new experimental updateReactable(outputId, selected = ..., expanded = ...) function to update the selected or expanded rows. For example:

https://glin.github.io/reactable/articles/examples.html#update-a-reactable-instance

# Requires reactable v0.1.0.9000
# devtools::install_github("glin/reactable")

library(shiny)
library(reactable)

ui <- fluidPage(
  actionButton("select_btn", "Select rows"),
  actionButton("clear_btn", "Clear selection"),
  actionButton("expand_btn", "Expand rows"),
  actionButton("collapse_btn", "Collapse rows"),
  reactableOutput("tbl"),
  "Rows selected:",
  verbatimTextOutput("rows_selected")
)

server <- function(input, output) {
  output$tbl <- renderReactable({
    reactable(
      iris,
      selection = "multiple",
      selectionId = "selection",
      onClick = "select",
      details = function(index) paste("Row details for row:", index)
    )
  })

  observeEvent(input$select_btn, {
    # Select rows
    updateReactable("tbl", selected = c(1, 3, 5))
  })

  observeEvent(input$clear_btn, {
    # Clear row selection using NA or integer(0)
    updateReactable("tbl", selected = NA)
  })

  observeEvent(input$expand_btn, {
    # Expand all rows
    updateReactable("tbl", expanded = TRUE)
  })

  observeEvent(input$collapse_btn, {
    # Collapse all rows
    updateReactable("tbl", expanded = FALSE)
  })

  output$rows_selected <- renderPrint({
    print(input$selection)
  })
}

shinyApp(ui, server)

If you're able to try it out, let me know what you think. I'm not sure if this will be the final API yet.

Also note that you can only expand or collapse all rows for now. Expanding individual rows may be possible in the future, but it'll be complicated to implement (related: #23).

tbradley1013 commented 4 years ago

This is great! Thanks for getting back to me and implementing this!

A couple of follow-up questions based on playing with the dev version this morning:

  1. When the row selection is updated, is there a way to change which page is "active" based on which row is selected? Currently, if there are 10 rows per page and I select the 11th row via a select Input, that row is selected but the user can't see that with out manually switching to the second page. Obviously this is something that would have to be implemented in the js code, but I am not sure whether it is an option in the react-table library
  2. Is it possible to add a "selected" html class to the selected row so that I can add some custom CSS to it?
  3. Is there a running list of the different components in the react-table library that can be made to be updated programatically/dynamically using the this.setState options? I am sure that some would be easier for you to implement than others but the more table attributes that can be updated after the table is already rendered the better (IMO)

It would be useful to be able to expand particular rows, but I can certainly work around that since I can now dynamically select rows!

Thanks again!

glin commented 4 years ago
  1. There wasn't, but I've added a page argument so you can change the current page like updateReactable("mytbl", selected = 11, page = 2) (example).

  2. Love that idea, and I've wanted to do that for a while now as well. I thought it made sense to add a rowInfo.selected property to the rowClass/rowStyle JS functions so you can set CSS classes or styles on selected rows.

    For example: https://glin.github.io/reactable/articles/examples.html#style-selected-rows

    
    # Requires reactable v0.1.0.9000
    # devtools::install_github("glin/reactable")

library(reactable)

reactable( iris[1:4, ], selection = "multiple", defaultSelected = c(1, 3), rowClass = JS("function(rowInfo) { return rowInfo.selected ? 'selected' : '' }"), rowStyle = JS("function(rowInfo) { if (rowInfo.selected) { return { backgroundColor: '#eee', boxShadow: 'inset 2px 0 0 0 #ffa62d' } } }"), borderless = TRUE, onClick = "select" )



3. I think most of the internal table state can be updated programmatically, and you can find a list of the easier-to-implement ones here: https://github.com/tannerlinsley/react-table/tree/v6#fully-controlled-component. Now that `updateReactable()` exists, these will all probably be added eventually, but feel free to file a feature request if there something you need in particular :) 
tbradley1013 commented 4 years ago

This is awesome! One last question to go with the updating the selected page. Is there a way to access the number of rows per page? If I restrict it to only being 10 then I can obviously figure out what page needs to be selected based on the row selected. But if I allow the user to select a page length of 10, 50, or 100 would there be any way to access which of those they have selected from the table?

My JS ability is virtually non-existent but I may take a look at the ones you have already implemented and create a PR for any of the others that I might need/want! Thanks for making this an option!

glin commented 4 years ago

There's a way to get the page size, but it's not easily accessible right now. That would be through the rowClass/rowStyle callbacks like:

reactable(
  iris,
  showPageSizeOptions = TRUE, 
  rowClass = JS("function(rowInfo, state) {
    console.log(state.pageSize)
    // Could get it in Shiny using something like:
    // Shiny.onInputChange('tbl_page_size', state.pageSize)
  }")
)

One idea would be to support similar JS callbacks in updateReactable():

updateReactable("tbl", page = JS("function(state) {
  // Calculate what page to go to based on state.pageSize
}"))

Another idea would be to add something like a getReactableInfo(outputId) function, where you can access a reactable instance's state directly in R and Shiny:

getReactableInfo("tbl")
# list(
#   pageSize = 20,
#   page = 2,
#   pages = 8,
#   selected = c(3, 4, 5)
# )

This would also be nice to replace the clunky selectionId for getting selected rows.

Is there one particular way you'd prefer to use? I think I'm leaning toward the second way so far, since it'd be easier to work with in R. But both would be fairly straightforward to implement.

If you ever want to create a PR for anything, I'd be happy to help review or get you started. That also reminds me, I haven't gotten around to writing any development/contributing documentation yet. I'll try to do that soon.

tbradley1013 commented 4 years ago

I like the second approach and I think a lot of shiny users will prefer that rather than having to write the JS calback functions. Another benefit would be that as you implement other ways to update the reactable through updateReactable, I assume, you would be able to add that information to the output of getReactable which I think would be useful.

tbradley1013 commented 4 years ago

Hi @glin

I am not sure whether this is something that falls under this issue or if I should open a new one but it looks like if you pass NA_real_ to updateReactable for the selected argument than it successfully deselects the row but the value of the input$selection value becomes 1 rather than NULL. See the example below:

library(shiny)
library(reactable)

ui <- fluidPage(
  actionButton("select_btn", "Select rows"),
  actionButton("clear_btn", "Clear selection"),
  actionButton("clear_btn_real", "Clear Selection - NA_real_"),
  reactableOutput("tbl"),
  "Rows selected:",
  verbatimTextOutput("rows_selected")
)

server <- function(input, output) {
  output$tbl <- renderReactable({
    reactable(
      iris,
      selection = "multiple",
      selectionId = "selection",
      onClick = "select",
      details = function(index) paste("Row details for row:", index)
    )
  })

  observeEvent(input$select_btn, {
    # Select rows
    updateReactable("tbl", selected = c(1, 3, 5))
  })

  observeEvent(input$clear_btn, {
    # Clear row selection using NA or integer(0)
    updateReactable("tbl", selected = NA)
  })

  observeEvent(input$clear_btn_real, {
    # Expand all rows
    updateReactable("tbl", selected = NA_real_)
  })

  output$rows_selected <- renderPrint({
    print(input$selection)
  })
}

shinyApp(ui, server)
glin commented 4 years ago

This issue works for me. Nice catch, I've fixed that in https://github.com/glin/reactable/commit/9f05b906505a5b6d93f93bab0df98c6bc13535fc.

I've also added a getReactableState() function to the development version: https://glin.github.io/reactable/reference/getReactableState.html

You can use getReactableState(outputId) to get the current page, pageSize, number of pages, and selected rows all in a list. Or getReactableState(outputId, "page") to get just a single value.

Here's an example of it in action: https://glin.github.io/reactable/articles/examples.html#get-the-state-of-a-reactable-instance

nergiszaim commented 3 years ago

Is there a way to edit individual cells in reactable? something similar to DT?

https://blog.rstudio.com/2018/03/29/dt-0-4/

abdo5 commented 3 years ago

Hi, how to get the current displayed record number in a page, I means the numbers that displayed at left down of a table : (1- 10 of 100 rows displayed)