| name | rhandsontable-table |
| description | Auto-invoked when creating or modifying rhandsontable tables in Shiny. Governs rHandsontableOutput/renderRHandsontable patterns, reading edits back to R, column types and validation, read-only controls, and hot_to_r() usage. |
rhandsontable Table Skill
Canonical Shiny Pattern
library(rhandsontable)
rHandsontableOutput("my_table")
output$my_table <- renderRHandsontable({
req(data())
rhandsontable(data(), rowHeaders = NULL) %>%
hot_table(highlightCol = TRUE, highlightRow = TRUE)
})
edited_data <- reactive({
req(input$my_table)
hot_to_r(input$my_table)
})
hot_to_r() converts the Handsontable JSON input back to an R data frame. Always use hot_to_r() — do not parse input$tableId manually.
Column Configuration with hot_col()
rhandsontable(data()) %>%
hot_col("status", type = "dropdown", source = c("Active", "Completed", "Withdrawn")) %>%
hot_col("confirmed", type = "checkbox") %>%
hot_col("visit_dt", type = "date", dateFormat = "YYYY-MM-DD") %>%
hot_col("aval", type = "numeric", format = "0.00") %>%
hot_col("notes", type = "text")
Column types: "text", "numeric", "date", "checkbox", "dropdown", "autocomplete", "password", "time", "handsontable".
Read-Only Columns
rhandsontable(data()) %>%
hot_col("usubjid", readOnly = TRUE) %>%
hot_col("studyid", readOnly = TRUE)
rhandsontable(data(), readOnly = TRUE)
Column Widths and Visibility
rhandsontable(data()) %>%
hot_col("usubjid", width = 120) %>%
hot_col("notes", width = 250) %>%
hot_cols(columnSorting = TRUE)
rhandsontable(data()) %>%
hot_col("internal_id", width = 0.1)
Table-Level Options
rhandsontable(
data(),
rowHeaders = NULL,
stretchH = "all",
contextMenu = TRUE,
overflow = "hidden",
height = 400,
width = "100%"
) %>%
hot_table(
highlightCol = TRUE,
highlightRow = TRUE,
enableComments = FALSE
)
Conditional Cell Formatting
rhandsontable(data()) %>%
hot_col(
"aval",
renderer = "
function(instance, td, row, col, prop, value, cellProperties) {
Handsontable.renderers.NumericRenderer.apply(this, arguments);
if (value > 100) {
td.style.background = '#f8d7da';
td.style.color = '#721c24';
}
}
"
)
Renderer strings are JavaScript. Keep them simple — complex logic belongs in R before the table renders.
Preserving Edits Across Re-Renders
The table re-renders whenever renderRHandsontable re-executes, losing unsaved edits. Preserve edits by seeding the render from the input:
output$my_table <- renderRHandsontable({
current <- if (!is.null(input$my_table)) hot_to_r(input$my_table) else data()
rhandsontable(current, rowHeaders = NULL)
})
Use this pattern when the table data can also change from outside (e.g., a reset button). Add an ignoreInit = TRUE guard if needed to avoid circular reactivity.
Validation
rhandsontable(data()) %>%
hot_col(
"age",
type = "numeric",
validator = "function(value, callback) { callback(value >= 0 && value <= 120); }",
allowInvalid = FALSE
)
allowInvalid = TRUE (default) highlights invalid cells but still accepts them. Set FALSE to block the entry.
Save / Reset Pattern in Shiny
rHandsontableOutput("tbl")
actionButton("save", "Save Changes")
actionButton("reset", "Reset")
rv <- reactiveValues(data = initial_data)
output$tbl <- renderRHandsontable({
rhandsontable(rv$data, rowHeaders = NULL)
})
observeEvent(input$save, {
rv$data <- hot_to_r(input$tbl)
})
observeEvent(input$reset, {
rv$data <- initial_data
})
Common Pitfalls
- Always use
hot_to_r() — input$tableId is a JSON list, not a data frame; hot_to_r() converts it correctly
- Re-render wipes edits — any reactive dependency in
renderRHandsontable that changes will discard unsaved edits; seed from input$tableId or use reactiveValues to persist
- Column order must match data frame —
hot_col() refers to columns by name; if you reorder columns in the data frame, verify hot_col() calls still target the right columns
- Date columns — rhandsontable returns dates as strings; coerce with
as.Date() after hot_to_r()
- Large data — Handsontable renders all rows in the DOM; avoid > 500 rows; use pagination or aggregation upstream
- Circular reactivity — if
renderRHandsontable reads input$my_table to seed itself, wrap with isolate() or use reactiveValues to break the cycle