`Reactable.setFilter` clears checked status for checkboxes
uriahf opened this issue ยท 5 comments
Hi all, first thing: I'm in love with {reactable}!
Thank you @glin for your hard word ๐
I'm trying to combine external range filter and inline checkboxes, unfortunately everytime I change the value in the range slider the checkboxes status returns to be unchecked.
Inspiration:
- https://glin.github.io/reactable/articles/custom-filtering.html#external-range-filter
- https://glin.github.io/reactable/articles/examples.html#javascript-render-function
reprex:
library(htmltools)
library(reactable)
data <- MASS::Cars93[1:5, c("Manufacturer", "Model", "Type", "AirBags", "Price")]
# Custom range input filter with label and value
rangeFilter <- function(tableId, columnId, label, min, max, value = NULL, step = NULL, width = "200px") {
value <- if (!is.null(value)) value else min
inputId <- sprintf("filter_%s_%s", tableId, columnId)
valueId <- sprintf("filter_%s_%s__value", tableId, columnId)
oninput <- paste(
sprintf("document.getElementById('%s').textContent = this.value;", valueId),
sprintf("Reactable.setFilter('%s', '%s', this.value)", tableId, columnId)
)
div(
tags$label(`for` = inputId, label),
div(
style = sprintf("display: flex; align-items: center; width: %s", validateCssUnit(width)),
tags$input(
id = inputId,
type = "range",
min = min,
max = max,
step = step,
value = value,
oninput = oninput,
onchange = oninput, # For IE11 support
style = "width: 100%;"
),
span(id = valueId, style = "margin-left: 8px;", value)
)
)
}
browsable(tagList(
rangeFilter(
"cars-ext-range",
"Price",
"Filter by Minimum Price",
floor(min(data$Price)),
ceiling(max(data$Price))
),
reactable(
data,
columns = list(
Price = colDef(
html = TRUE,
cell = JS(
"function(cellInfo) {
return `<input type = 'checkbox'>${cellInfo.value}`
}"
))
),
defaultPageSize = 5,
elementId = "cars-ext-range"
)
))
Here is my approach for solving the problem, hope I'll get it right:
- set initial meta argument for each checkbox
checked
status, set everything to FALSE. - Add id for each checkbox.
- Add onClick function that updates the original meta with
Reactable.setMeta()
once a checkbox is clicked.
On theory the filter should re-render the selected rows with the updated checked status for each checkbox. Sounds reasonable?
I'll try to write some code soon enough...
Hey @uriahf, so the checkboxes get reset on filtering because the table is rerendering each cell that changes on any table state changes. Since that checkbox is just a plain unchecked <input type='checkbox'>
in every row, that is what will be rerendered. If you want checkbox state to persist, you will need to hook it up to some external saved state. I think meta
would work for saving that state, but let me know.
The render function might look something like (untested):
function(cellInfo, state) {
const checked = state.meta.checked[cellInfo.index] ? 'checked' : ''
return `<input type='checkbox' ${checked} onclick=${onclick}>${cellInfo.value}`
}
Then you could attach an inline onclick
handler to that checkbox, or do it with reactable's onClick
.
Sounds good, but are you sure about working with indexes and unnamed lists / arrays?
When I try to define the following function for the onclick events I get an error:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Missing_colon_after_property_id?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default
script <- sprintf("function synchronizeCheckboxes(checkbox) {
Reactable.setMeta('cars-ext-range',
prevMeta => {
return { checked: { true true true true true } }
})}")
Correct me if I'm wrong, but working with meta on reachable requires named lists.
If checked
was supposed to be a map, then I think you'd need keys there, like { 0: true, 1: true, 2: true, 3: true, 4: true }
Here's a working example using the onClick
handler. I also tried an inline onclick
attribute, but getting complex multiline expressions in there is super janky. I'm using meta.checked
as an array of true/false values here, where the index is the row index.
library(htmltools)
library(reactable)
data <- MASS::Cars93[1:5, c("Manufacturer", "Model", "Type", "AirBags", "Price")]
# Custom range input filter with label and value
rangeFilter <- function(tableId, columnId, label, min, max, value = NULL, step = NULL, width = "200px") {
value <- if (!is.null(value)) value else min
inputId <- sprintf("filter_%s_%s", tableId, columnId)
valueId <- sprintf("filter_%s_%s__value", tableId, columnId)
oninput <- paste(
sprintf("document.getElementById('%s').textContent = this.value;", valueId),
sprintf("Reactable.setFilter('%s', '%s', this.value)", tableId, columnId)
)
div(
tags$label(`for` = inputId, label),
div(
style = sprintf("display: flex; align-items: center; width: %s", validateCssUnit(width)),
tags$input(
id = inputId,
type = "range",
min = min,
max = max,
step = step,
value = value,
oninput = oninput,
onchange = oninput, # For IE11 support
style = "width: 100%;"
),
span(id = valueId, style = "margin-left: 8px;", value)
)
)
}
# Filter method that filters numeric columns by minimum value
filterMinValue <- JS("function(rows, columnId, filterValue) {
return rows.filter(function(row) {
return row.values[columnId] >= filterValue
})
}")
browsable(tagList(
rangeFilter(
"cars-ext-range",
"Price",
"Filter by Minimum Price",
floor(min(data$Price)),
ceiling(max(data$Price))
),
reactable(
data,
columns = list(
Price = colDef(
html = TRUE,
cell = JS(
"function(cellInfo, state) {
const checked = state.meta.checked[cellInfo.index] ? 'checked' : ''
return `<input type='checkbox' ${checked} onclick='${onclick}'>${cellInfo.value}`
}"
),
filterMethod = filterMinValue
)
),
meta = list(checked = list()),
onClick = JS("(rowInfo, column) => {
Reactable.setMeta('cars-ext-range', prevMeta => {
const checked = [...prevMeta.checked]
checked[rowInfo.index] = !checked[rowInfo.index]
return { ...prevMeta, checked }
})
}"),
defaultPageSize = 5,
elementId = "cars-ext-range"
)
))
That's perfect, thanks!