英文:
shiny / rhandsontable / updating cells
问题
在Shiny中,我使用rhandsontable显示了两个表格。我已经实现了每当更改单元格值时,单元格会变色。但是,当在两个表格之间切换时,所有更改都会消失,因为表格在每次切换时都会从头开始加载。是否有可能在不退出会话或不按“重置”按钮的情况下保存更改的单元格,以便在两个表格之间切换时保留这些更改?
(基于以下主题的解决方案:https://stackoverflow.com/questions/73337727/in-shiny-rhandsontable-afterchange-change-multiple-cell-backgrounds-at-once)
library(shiny)
library(rhandsontable)
library(tidyverse)
change_hook <- "function(el, x) {
hot = this.hot;
cellchngs = [];
afterChange = function(changes, source) {
$.each(changes, function (index, elem) {
change = elem; /* gather the row, col, old, new values */
if (change[2] !== change[3]) { /* if old isn't the same as new */
cellchg = ({rowind: change[0], colind: change[1]});
cellchngs.push(cellchg); /* add row and column indices to array */
}
});
$.each(cellchngs, function(ind, elem) {
td = hot.getCell(elem['rowind'], elem['colind']); /* get the HTML element */
td.style.background = 'cyan'; /* set background color */
});
}
hot.addHook('afterChange', afterChange); /* add event to table */
}"
ui <- div(actionButton(inputId = "reset_button", label = "Reset"), selectInput("data", "Choose data", choices = c("mtcars" = 1, "iris" = 2), selected = 1),
rHandsontableOutput(outputId = "mtcars"))
server <- function(input, output, session) {
reset <- reactiveVal(0)
output$mtcars <- renderRHandsontable({
r = reset()
myvec <- c("mtcars", "iris")
mydata <- eval(parse(text = myvec[as.numeric(input$data)]))
rht = rhandsontable(mydata, reset = r, stretchH = "all", height = 300)
reset(0)
htmlwidgets::onRender(rht, change_hook)
})
observeEvent(input$reset_button, {
reset(1)
})
}
shinyApp(ui, server)
英文:
In shiny, I have two tables displayed with rhandsontable, I have already implemented that whenever a cell value is changed the cell gets colored, however when switching between the tables all the changes disappear since naturally the table reloads everytime from the start, is it possible to somehow save the changed cell, so as long as you do not quit the session or do not press "reset" all the changes remain even if you switch between the tables?
(code based on the solution of the following thread: https://stackoverflow.com/questions/73337727/in-shiny-rhandsontable-afterchange-change-multiple-cell-backgrounds-at-once)
library(shiny)
library(rhandsontable)
library(tidyverse)
change_hook <- "function(el,x) {
hot = this.hot;
cellchngs = [];
afterChange = function(changes, source) {
$.each(changes, function (index, elem) {
change = elem; /* gather the row, col, old, new values */
if(change[2] !== change[3]) { /* if old isn't the same as new */
cellchg = ({rowind: change[0], colind: change[1]});
cellchngs.push(cellchg); /* add row and column indicies to array */
}
});
$.each(cellchngs, function(ind, elem) {
td = hot.getCell(elem['rowind'], elem['colind']); /* get the html element */
td.style.background = 'cyan'; /* set background color */
});
}
hot.addHook('afterChange', afterChange); /* add event to table */
}"
ui <- div(actionButton(inputId = "reset_button",label = "Reset"), selectInput("data", "Choose data",choices=c("mtcars"=1, "iris"=2), selected=1)
,rHandsontableOutput(outputId="mtcars"))
server <- function(input, output, session) {
reset <- reactiveVal(0)
output$mtcars <- renderRHandsontable({
r = reset()
myvec <- c("mtcars", "iris")
mydata <- eval(parse(text=myvec[as.numeric(input$data)]))
rht = rhandsontable(mydata,reset=r,stretchH="all",height=300)
reset(0)
htmlwidgets::onRender(rht,change_hook)
})
observeEvent(input$reset_button,
{
reset(1)
})
}
shinyApp(ui, server)
</details>
# 答案1
**得分**: 1
这段文字是英文的,以下是翻译部分:
"This capitalizes on your browser's *session storage*. That means that as long as it is one continuous event, the data will save. If you wanted to be able to close the browser or rerun the app and still have the saved changes, you could use local storage instead (`sessionStorage` versus `localStorage`).
This will only work with you have the graphs set up right now. If you change your `ui` to have two separate tables, this won't work. (I can help if you set it up differently, just let me know.)"
"这是利用了您的浏览器的 *会话存储*。这意味着只要是一个连续的事件,数据就会被保存下来。如果您希望能够关闭浏览器或重新运行应用程序并仍然保留保存的更改,您可以改用本地存储 (`sessionStorage` 与 `localStorage` 相对比较)。
这仅在您目前设置的图表上起作用。如果您将您的 `ui` 更改为具有两个单独的表格,这将不起作用。(如果您以不同方式设置它,我可以提供帮助,请告诉我。)"
"Changes to the `change_hook`:
`wh` will capture either `mtcars` or `iris` from your `selectInput` dropdown menu.
`cellchngs` will capture changes as it did before, but now you'll have one for each table. There are three arrays because it was the simplest way to align the 1... indexing in R and the 0, 1.... indexing in JS. In other words, that first array within `cellchngs` will remain empty, the next array (index 1 in JS) will capture everything that changes in the `selectInput` `value = 1` -- that's your `mtcars` table (as you've assigned it).
`afterChange` didn't change very much from your original question.
* added `idx` to capture the index from your `selectInput`
* modified `cellchngs = ` to `cellchngs[idx] = `
* added `sCookie()`: function that saves the data in the sessions storage of your browser (see the image after this list of changes)
* added global var collection with `chgs` (this is declared in your `ui`; more on that when I cover the changes to the `ui`)
* lastly, `if(sessionStorage...` looks to see if there is data saved in session storage, if there is, this will update the table to reflect the changes made since you started the browser session (regardless of how many times you flip between tables with the dropdown)"
"对 `change_hook` 的更改:
`wh` 将从您的 `selectInput` 下拉菜单中捕获 `mtcars` 或 `iris`。
`cellchngs` 将像以前一样捕获更改,但现在每个表格都有一个。有三个数组是因为这是在 R 中的 1... 索引和 JavaScript 中的 0、1.... 索引之间对齐的最简单方法。换句话说,在 `cellchngs` 中的第一个数组将保持为空,下一个数组(JavaScript 中的索引 1)将捕获 `selectInput` 中更改的所有内容 `value = 1` - 这是您的 `mtcars` 表格(如您所分配的)。
`afterChange` 与您最初的问题并没有太大的不同。
* 添加了 `idx` 来捕获来自 `selectInput` 的索引
* 修改了 `cellchngs = ` 为 `cellchngs[idx] = `
* 添加了 `sCookie()`: 保存数据到浏览器会话存储的函数(请参阅更改列表后的图像)
* 添加了全局变量集合 `chgs`(在您的 `ui` 中声明;当我讨论 `ui` 的更改时,会详细介绍)
* 最后,`if(sessionStorage...` 用于查看是否保存了会话存储中的数据,如果保存了,它将更新表格,以反映自您启动浏览器会话以来所做的更改(无论您使用下拉菜单在表格之间切换多少次)。"
"Changes to the `ui`
I've added a `tags$script` element to your `ui`, the code you originally had in your `ui` remains the same.
In this script, you'll find the declaration of the global variable `chgs`, the function `colr`: change cell colors outside of the change event, and `sCookie`: save the data to session storage."
"对 `ui` 的更改
我在您的 `ui` 中添加了一个 `tags$script` 元素,您最初在 `ui` 中的代码保持不变。
在此脚本中,您将找到全局变量 `chgs` 的声明,函数 `colr`:在更改事件之外更改单元格颜色以及 `sCookie`:将数据保存到会话存储中。"
"All the code altogether
Here's everything again, but all at once. If you have any questions, let me know.
Note that I didn't change anything in your `server` (it may look different due to spacing, though)."
"所有代码汇总
以下是所有内容,一次性呈现。如果您有任何问题,请告诉我。
请注意,我没有更改您的 `server` 中的任何内容(尽管由于间距问题,它可能看起来不同)。
<details>
<summary>英文:</summary>
This is a bit ugly (in terms of coding redundancy)...but I'm tired, and it works. At the end of my answer, I've provided all of the code again, all together, for easier copy + paste.
[![enter image description here][1]][1]
This capitalizes on your browser's *session storage*. That means that as long as it is one continuous event, the data will save. If you wanted to be able to close the browser or rerun the app and still have the saved changes, you could use local storage instead (`sessionStorage` versus `localStorage`).
This will only work with you have the graphs set up right now. If you change your `ui` to have two separate tables, this won't work. (I can help if you set it up differently, just let me know.)
### Changes to the `change_hook`:
`wh` will capture either `mtcars` or `iris` from your `selectInput` dropdown menu.
`cellchngs` will capture changes as it did before, but now you'll have one for each table. There are three arrays because it was the simplest way to align the 1... indexing in R and the 0, 1.... indexing in JS. In other words, that first array within `cellchngs` will remain empty, the next array (index 1 in JS) will capture everything that changes in the `selectInput` `value = 1` -- that's your `mtcars` table (as you've assigned it).
`afterChange` didn't change very much from your original question.
* added `idx` to capture the index from your `selectInput`
* modified `cellchngs = ` to `cellchngs[idx] = `
* added `sCookie()`: function that saves the data in the sessions storage of your browser (see the image after this list of changes)
* added global var collection with `chgs` (this is declared in your `ui`; more on that when I cover the changes to the `ui`)
* lastly, `if(sessionStorage...` looks to see if there is data saved in session storage, if there is, this will update the table to reflect the changes made since you started the browser session (regardless of how many times you flip between tables with the dropdown)
>Looking at Session Storage: you can see this in your browsers developer tools; tab: *Application*, left menu: *Session Storage* -> your IP (see the image below)
[![enter image description here][2]][2]
change_hook <- "function(el, x) {
var hot = el.htmlwidget_data_init_result.hot;
var wh = document.querySelector('#data option').innerHTML; /* DD table select /
var cellchngs = [[], [], []]; / 3 arrays: 0 start index vs 1 start */
afterChange = function(changes, source) {
var idx = document.querySelector('#data option').value; /* DD table select /
$.each(changes, function (index, elem) {
var change = elem; / gather the row, col, old, new values /
if(change[2] !== change[3]) { / if old isn't the same as new /
var cellchg = ({rowind: change[0], colind: change[2]});
cellchngs[idx].push(cellchg); / add row and column indicies to array /
sCookie(); / save the updated data to cookie /
}
});
$.each(cellchngs[idx], function(ind, ele) {
var td = hot.getCell(ele['rowind'], ele['colind']); / get the html element /
td.style.background = 'yellow'; / set background color */
});
chgs[idx] = chgs[idx].concat(cellchngs[idx]); /* save list of changes to global var*/
chgs[idx].filter((v,i,a)=>a.findIndex(v2=>['rowind','colind'].every(k=>v2[k] ===v[k]))===i); /* remove duplicates */
}
hot.addHook('afterChange', afterChange); /* add event to table */
if(sessionStorage[wh]) { /* if data already stored for current table, retrieve it*/
hot.loadData(JSON.parse(sessionStorage[wh]));
hot.render();
colr(chgs, hot); /* re-highlight changes */
}
}"
### Changes to the `ui`
I've added a `tags$script` element to your `ui`, the code you originally had in your `ui` remains the same.
In this script, you'll find the declaration of the global variable `chgs`, the function `colr`: change cell colors outside of the change event, and `sCookie`: save the data to session storage.
ui <- div(
tags$script(HTML(
'setTimeout(function() { /* ensure table loads before looking for tbl elem /
chgs = [[], [], []]; / global variable /
colr = function(chgs, hot) { / for outside of change events (update data from stg /
var idx = document.querySelector("#data option").value;/ DD table select /
$.each(chgs[idx], function(ind, ele) {
var td = hot.getCell(ele["rowind"], ele["colind"]); / get the html element /
td.style.background = "yellow"; / set background color /
});
}
sCookie = function() { / whenever data changes are made, save to local*/
var el = document.querySelector(".rhandsontable.html-widget"); /* capture table el /
var hot = el.htmlwidget_data_init_result.hot; / capture instance /
var wh = document.querySelector("#data option").innerHTML; / DD table select /
sessionStorage[wh] = JSON.stringify(hot.getData()); / DD table select */
return
}
}, 200)')),
actionButton(inputId = "reset_button", label = "Reset"),
selectInput("data", "Choose data",
choices = c("mtcars" = 1, "iris" = 2), selected = 1),
rHandsontableOutput(outputId = "mtcars"))
### All the code altogether
Here's everything again, but all at once. If you have any questions, let me know.
Note that I didn't change anything in your `server` (it may look different due to spacing, though).
library(shiny)
library(rhandsontable)
change_hook <- "function(el, x) {
var hot = el.htmlwidget_data_init_result.hot;
var wh = document.querySelector('#data option').innerHTML; /* DD table select /
var cellchngs = [[], [], []]; / 3 arrays: 0 start index vs 1 start */
afterChange = function(changes, source) {
var idx = document.querySelector('#data option').value; /* DD table select /
$.each(changes, function (index, elem) {
var change = elem; / gather the row, col, old, new values /
if(change[2] !== change[3]) { / if old isn't the same as new /
var cellchg = ({rowind: change[0], colind: change[2]});
cellchngs[idx].push(cellchg); / add row and column indicies to array /
sCookie(); / save the updated data to cookie /
}
});
$.each(cellchngs[idx], function(ind, ele) {
var td = hot.getCell(ele['rowind'], ele['colind']); / get the html element /
td.style.background = 'yellow'; / set background color */
});
chgs[idx] = chgs[idx].concat(cellchngs[idx]); /* save list of changes to global var*/
chgs[idx].filter((v,i,a)=>a.findIndex(v2=>['rowind','colind'].every(k=>v2[k] ===v[k]))===i); /* remove duplicates */
}
hot.addHook('afterChange', afterChange); /* add event to table */
if(sessionStorage[wh]) { /* if data already stored for current table, retrieve it*/
hot.loadData(JSON.parse(sessionStorage[wh]));
hot.render();
colr(chgs, hot); /* re-highlight changes */
}
}"
ui <- div(
tags$script(HTML(
'setTimeout(function() { /* ensure table loads before looking for tbl elem /
chgs = [[], [], []]; / global variable /
colr = function(chgs, hot) { / for outside of change events (update data from stg /
var idx = document.querySelector("#data option").value;/ DD table select /
$.each(chgs[idx], function(ind, ele) {
var td = hot.getCell(ele["rowind"], ele["colind"]); / get the html element /
td.style.background = "yellow"; / set background color /
});
}
sCookie = function() { / whenever data changes are made, save to local*/
var el = document.querySelector(".rhandsontable.html-widget"); /* capture table el /
var hot = el.htmlwidget_data_init_result.hot; / capture instance /
var wh = document.querySelector("#data option").innerHTML; / DD table select /
sessionStorage[wh] = JSON.stringify(hot.getData()); / DD table select */
return
}
}, 200)')),
tags$style(HTML(".colorMe {background: yellow !important;}")),
actionButton(inputId = "reset_button", label = "Reset"),
selectInput("data", "Choose data",
choices = c("mtcars" = 1, "iris" = 2), selected = 1),
rHandsontableOutput(outputId = "mtcars"))
server <- function(input, output, session) { # unchanged from your question
reset <- reactiveVal(0)
output$mtcars <- renderRHandsontable({
r = reset()
myvec <- c("mtcars", "iris")
mydata <- eval(parse(text = myvec[as.numeric(input$data)]))
rht = rhandsontable(mydata, reset = r, stretchH = "all", height = 300)
reset(0)
htmlwidgets::onRender(rht, change_hook)
})
observeEvent(input$reset_button, {reset(1)})
}
shinyApp(ui, server)
[1]: https://i.stack.imgur.com/HD14O.png
[2]: https://i.stack.imgur.com/IyJa6.png
</details>
# 答案2
**得分**: 1
你可以使用隐藏的 `tabsetPanels` 来实现这个目标。
请注意,我没有更改 `change_hook` 块。
```r
library(shiny)
library(rhandsontable)
change_hook <- "function(el,x) {
hot = this.hot;
cellchngs = [];
afterChange = function(changes, source) {
$.each(changes, function (index, elem) {
change = elem; /* gather the row, col, old, new values */
if(change[2] !== change[3]) { /* if old isn't the same as new */
cellchg = ({rowind: change[0], colind: change[1]});
cellchngs.push(cellchg); /* add row and column indicies to array */
}
});
$.each(cellchngs, function(ind, elem) {
td = hot.getCell(elem['rowind'], elem['colind']); /* get the html element */
td.style.background = 'cyan'; /* set background color */
});
}
hot.addHook('afterChange', afterChange); /* add event to table */
}"
ui <- fluidPage(
actionButton(inputId = "reset_button",label = "Reset"),
selectInput(
inputId = "data",
label = "Choose data",
choices = c("mtcars", "iris")
),
tabsetPanel(
id = "tabs",
type = "hidden",
tabPanelBody(
value = "mtcars",
rHandsontableOutput(outputId = "mtcars")
),
tabPanelBody(
value = "iris",
rHandsontableOutput(outputId = "iris")
)
)
)
server <- function(input, output, session) {
reset <- reactiveVal(0)
output$mtcars <- renderRHandsontable({
r = reset()
rht = rhandsontable(
mtcars,
reset = r,
stretchH = "all",
height = 300
)
reset(0)
htmlwidgets::onRender(rht, change_hook)
})
output$iris <- renderRHandsontable({
r = reset()
rht = rhandsontable(
iris,
reset = r,
stretchH = "all",
height = 300
)
reset(0)
htmlwidgets::onRender(rht, change_hook)
})
observeEvent(input$reset_button, {
reset(1)
})
# if the selectInput value changes, switch tabs:
observeEvent(input$data, {
updateTabsetPanel(
session = session,
inputId = "tabs",
selected = input$data
)
})
}
shinyApp(ui, server)
英文:
You can use hidden tabsetPanels to achieve that.
Note that I did not change the change_hook
block.
library(shiny)
library(rhandsontable)
change_hook <- "function(el,x) {
hot = this.hot;
cellchngs = [];
afterChange = function(changes, source) {
$.each(changes, function (index, elem) {
change = elem; /* gather the row, col, old, new values */
if(change[2] !== change[3]) { /* if old isn't the same as new */
cellchg = ({rowind: change[0], colind: change[1]});
cellchngs.push(cellchg); /* add row and column indicies to array */
}
});
$.each(cellchngs, function(ind, elem) {
td = hot.getCell(elem['rowind'], elem['colind']); /* get the html element */
td.style.background = 'cyan'; /* set background color */
});
}
hot.addHook('afterChange', afterChange); /* add event to table */
}"
ui <- fluidPage(
actionButton(inputId = "reset_button",label = "Reset"),
selectInput(
inputId = "data",
label = "Choose data",
choices = c("mtcars", "iris")
),
tabsetPanel(
id = "tabs",
type = "hidden",
tabPanelBody(
value = "mtcars",
rHandsontableOutput(outputId = "mtcars")
),
tabPanelBody(
value = "iris",
rHandsontableOutput(outputId = "iris")
)
)
)
server <- function(input, output, session) {
reset <- reactiveVal(0)
output$mtcars <- renderRHandsontable({
r = reset()
rht = rhandsontable(
mtcars,
reset = r,
stretchH = "all",
height = 300
)
reset(0)
htmlwidgets::onRender(rht, change_hook)
})
output$iris <- renderRHandsontable({
r = reset()
rht = rhandsontable(
iris,
reset = r,
stretchH = "all",
height = 300
)
reset(0)
htmlwidgets::onRender(rht, change_hook)
})
observeEvent(input$reset_button, {
reset(1)
})
# if the selectInput value changes, switch tabs:
observeEvent(input$data, {
updateTabsetPanel(
session = session,
inputId = "tabs",
selected = input$data
)
})
}
shinyApp(ui, server)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论