Dt: Link Binding Is Lost After Re-Rendering the Table

Text Input in DT::datatable unbinds and I can't rebind it

Putting aside your version restrictions, here is how I'd approach this with the latest library(DT) version (Hopefully useful for future readers and maybe someday you will also update):

Edit: now using dataTableProxy to avoid re-rendering.

library(shiny)
library(DT)

ui <- shinyUI(
fluidPage(
radioButtons(inputId = "rdo_cyl",
label = "Cylinders",
choices = sort(unique(mtcars$cyl)),
inline = TRUE),
h3("Automatic"),
actionButton(inputId = "btn_save_automatic",
label = "Save Comments"), p(),
DTOutput("am0"),
hr(),
h3("Manual"),
actionButton(inputId = "btn_save_manual",
label = "Save Comments"), p(),
DTOutput("am1")
)
)

server <- shinyServer(function(input, output, session){
globalData <- mtcars
globalData$comment <- rep("", nrow(mtcars))
globalData$row_id <- seq_len(nrow(mtcars))

diabledCols <- grep("comment", names(globalData), invert = TRUE)
AppData <- reactiveVal(globalData)

automaticAppData <- reactive({
AppData()[AppData()[["cyl"]] %in% input$rdo_cyl & AppData()[["am"]] %in% "0", ]
})

manualAppData <- reactive({
AppData()[AppData()[["cyl"]] %in% input$rdo_cyl & AppData()[["am"]] %in% "1", ]
})

output$am0 <- DT::renderDT(
# isolate: render only once
expr = {isolate(automaticAppData())},
editable = list(target = "cell", disable = list(columns = diabledCols))
)

output$am1 <- DT::renderDT(
# isolate: render only once
expr = {isolate(manualAppData())},
editable = list(target = "cell", disable = list(columns = diabledCols))
)

observeEvent(input$btn_save_automatic, {
info = input$am0_cell_edit
str(info)
i = automaticAppData()$row_id[[info$row]]
j = info$col
v = info$value
globalData[i, j] <<- DT::coerceValue(v, globalData[i, j])
AppData(globalData)
# update database...
})

observeEvent(input$btn_save_manual, {
info = input$am1_cell_edit
str(info)
i = manualAppData()$row_id[[info$row]]
j = info$col
v = info$value
globalData[i, j] <<- DT::coerceValue(v, globalData[i, j])
AppData(globalData)
# update database...
})

am0Proxy <- dataTableProxy("am0")
am1Proxy <- dataTableProxy("am1")

observeEvent(automaticAppData(), {
replaceData(am0Proxy, automaticAppData(), resetPaging = FALSE)
})

observeEvent(manualAppData(), {
replaceData(am1Proxy, manualAppData(), resetPaging = FALSE)
})

})

shinyApp(ui = ui, server = server)

Result

Here are some related infos.


Update for DT Version 0.2

Here is another solution closer to your initial code. I'm using isolate(), dataTableProxy() and replaceData() which are available since DT version 0.2 to avoid re-rendering the table, which resolves the binding issue and should be faster.

Another problem in your code was that you called session$sendCustomMessage("unbind-DT", "am0") twice instead of using it for "am1".

library(shiny)
library(DT)
library(dplyr)

mtcars$comment <- rep("", nrow(mtcars))
mtcars$row_id <- seq_len(nrow(mtcars))
AppData <- split(mtcars, mtcars[c("cyl", "am")])

# Makes a text input column out of a data frame
make_inputtable <- function(df){
df$comment <-
mapply(
function(comment, id){
as.character(textInput(inputId = sprintf("txt_comment_%s", id),
label = "",
value = comment))
},
comment = df$comment,
id = df$row_id,
SIMPLIFY = TRUE)

df
}

ui <- shinyUI(
fluidPage(
radioButtons(inputId = "rdo_cyl",
label = "Cylinders",
choices = sort(unique(mtcars$cyl)),
inline = TRUE),

h3("Automatic"),
actionButton(inputId = "btn_save_automatic",
label = "Save Comments"),
DT::dataTableOutput("am0"),

hr(),

h3("Manual"),
actionButton(inputId = "btn_save_manual",
label = "Save Comments"),
DT::dataTableOutput("am1"),

# unbind a datatable. Needs to be done before a table is redrawn.
tags$script(HTML(
"Shiny.addCustomMessageHandler('unbind-DT', function(id) {
Shiny.unbindAll($('#'+id).find('table').DataTable().table().node());
})"))
)
)

server <- shinyServer(function(input, output, session){
reactiveData <- reactiveValues(
am0_cyl4 = AppData[["4.0"]],
am0_cyl6 = AppData[["6.0"]],
am0_cyl8 = AppData[["8.0"]],
am1_cyl4 = AppData[["4.1"]],
am1_cyl6 = AppData[["6.1"]],
am1_cyl8 = AppData[["8.1"]]
)

# Reactive Objects ------------------------------------------------

ref0 <- reactive({
sprintf("am0_cyl%s", input$rdo_cyl)
})

data0 <- reactive({
reactiveData[[ref0()]]
})

ref1 <- reactive({
sprintf("am1_cyl%s", input$rdo_cyl)
})

data1 <- reactive({
reactiveData[[ref1()]]
})

# Event Observers -------------------------------------------------

observeEvent(
input$btn_save_automatic,
{
in_field <- names(input)[grepl("^txt_comment_", names(input))]
in_field_id <- sub("^txt_comment_", "", in_field)
in_field_id <- as.numeric(in_field_id)
in_field_id <- in_field_id[in_field_id %in% data0()$row_id]

exist_frame <- data0()[c("row_id", "comment")]
new_frame <-
data.frame(
row_id = in_field_id,
comment = vapply(in_field_id,
function(id){ input[[sprintf("txt_comment_%s", id)]]},
character(1)),
stringsAsFactors = FALSE)

Compare <- left_join(exist_frame,
new_frame,
by = "row_id",
suffix = c("_exist", "_new")) %>%
filter(comment_exist != comment_new)

message(sprintf("* %s comment(s) saved", nrow(Compare)))

# Only perform the save operations if there are changes to be made.
if (nrow(Compare)){
session$sendCustomMessage("unbind-DT", "am0")

for(i in seq_len(nrow(Compare))){
row <- Compare$row_id
reactiveData[[ref0()]]$comment[reactiveData[[ref0()]]$row_id == row] <-
input[[sprintf("txt_comment_%s", row)]]
}
print(data0())
}

}
)

# Very similar to btn_save_automatic
observeEvent(
input$btn_save_manual,
{
in_field <- names(input)[grepl("^txt_comment_", names(input))]
in_field_id <- sub("^txt_comment_", "", in_field)
in_field_id <- as.numeric(in_field_id)
in_field_id <- in_field_id[in_field_id %in% data1()$row_id]

exist_frame <- data1()[c("row_id", "comment")]
new_frame <-
data.frame(
row_id = in_field_id,
comment = vapply(in_field_id,
function(id){ input[[sprintf("txt_comment_%s", id)]]},
character(1)),
stringsAsFactors = FALSE)

Compare <- left_join(exist_frame,
new_frame,
by = "row_id",
suffix = c("_exist", "_new")) %>%
filter(comment_exist != comment_new)

message(sprintf("* %s comment(s) saved", nrow(Compare)))

# Only perform the save operations if there are changes to be made.
if (nrow(Compare)){
session$sendCustomMessage("unbind-DT", "am1")

for(i in seq_len(nrow(Compare))){
row <- Compare$row_id
reactiveData[[ref1()]]$comment[reactiveData[[ref1()]]$row_id == row] <-
input[[sprintf("txt_comment_%s", row)]]
}
print(data1())
}

}
)

# Output Objects --------------------------------------------------

output$am0 <-
DT::renderDataTable({
# isolate: render table only once!
make_inputtable(isolate(data0())) %>%
datatable(escape = -13,
options = list(preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
}, server = TRUE)

output$am1 <-
DT::renderDataTable({
# isolate: render table only once!
make_inputtable(isolate(data1())) %>%
datatable(escape = -13,
options = list(preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
}, server = TRUE)

am0Proxy <- dataTableProxy("am0")
am1Proxy <- dataTableProxy("am1")

observeEvent(data0(), {
replaceData(am0Proxy, make_inputtable(data0()), resetPaging = FALSE) # important
}, ignoreInit = TRUE)

observeEvent(data1(), {
replaceData(am1Proxy, make_inputtable(data1()), resetPaging = FALSE) # important
}, ignoreInit = TRUE)

})

shinyApp(ui = ui, server = server)

How to re-render react-table in .js when data changes in another .js

So, from my understanding, you are updating a global variable created in a js file ./globalVariable.js. I haven't seen the file, but I believe these variables are outside the scope of what react can "keep an eye on".

So I suggest you to create a state for your data in the Usage.js, and then update it. Example:

Usage.js

import { Table } from './table.js';
...
const [data, setData] = useState(initialData); //define your inital data like your global variables

async function DummyCall() {
try {
const response = await axios.get(URL, headers);
if(response.status === 200) {
const updatedData = { ...data };
updatedData.config.value.exactval.push(reponse.ID[1]);
setData(updatedData)
}

} catch(error) {
console.log(error);
}
}
...
<button type ="button" onClick={() => DummyCall()} > Test me! </button>


Edit #1

Since you cannot merge both files or export the function, i suggest you tu use ContextAPI

TableDataContext.js

import { createContext } from 'react'

const TableDataContext = createContext();

const TableDataProvider = ({ children }) => {
const [data, setData] = useState(initialData); //define your inital

<TableDataContext.Provider value={{ data, setData }}>
{children}
</TableDataContext.Provider>
}

export { TableDataContext, TableDataProvider };

Now you have to wrap your components taht need to consume the TableDataContext with the provider

If you want in your whole app, for example:

import { TableDataProvider } from 'path/to/context/TableDataContext'

<TableDataProvider>
<App/>
</TableDataProvider>

This way, you can setData in DummyCall by importing from context:

Updater.js

import { useContext } from 'react';
import { TableDataContext } from 'path/to/context/TableDataContext'
....
const context = useContext(TableDataContext);

async function DummyCall() {
//...code
// on setData
const updatedData = { ...context.data };
updatedData.config.value.exactval.push(reponse.ID[1]);
context.setData(updatedData)
}

Finally just import the data from context in your table.js, and you will not need to pass by props

You can learn more about the ContextAPI on React Docs

Hope it works

Table not rendering after button push

It's renderDataTable, not renderTable. Always helps to have a second set of eyes on code sometimes!

data.csi.m <- data.frame(col1 = c(18617736, 18617736, 1699471, 1699471), col2 = c("33/47", "33/47", "N/A", "N/A"), col3 = c("N/A", "N/A", "N/A", "N/A"), stringsAsFactors = FALSE)

library(shiny)
library(dplyr)
library(DBI)
library(ggplot2)
library(DT)

RV <- reactiveValues(data = data.csi.m)

ui <- fluidPage(
headerPanel('Search Filter'),
sidebarPanel(
selectInput("ageddmenu", "Age:",
c(">" = ">",
"<" = "<",
"=" = "=",
">=" = ">=",
"<=" = "<=",
"!=" = "!=")),
sliderInput("ageslider", "Age:",
min = 50, max = 100, value = 60
),
checkboxGroupInput("genderCheckBox", "Sex:",
c("M" = "M",
"F" = "F")),
actionButton("filterButton", "Filter", icon("filter"))
),
mainPanel(
h2("Clinical Subject Information"),
DT::dataTableOutput("csi"),
h2("Summary Plot"),
plotOutput('plot1')

)
)

server = (function(input, output) {

observeEvent(input$filterButton, {
temp <- RV$data[1:3, 1:10]
RV$data <- temp
print(RV$data)

})
output$csi <- DT::renderDataTable(RV$data)
})

shinyApp(ui = ui, server = server)

navigate to next tab post clicking on link

When adding inputs (like the actionLink) to a datatable you need to bind them manually, here is why.

Please check the following:

library(shiny)
library(shinydashboard)
library(DT)

# UI ---------------------------------------------------------------------

ui <- fluidPage(tabsetPanel(
id = "panels",
tabPanel("A",
p(),
DTOutput("tab")),
tabPanel("B",
h3("Some information"),
tags$li("Item 1"),
tags$li("Item 2"),
actionLink("link_to_tabpanel_a", "Link to panel A")
)
))

# Server ------------------------------------------------------------------

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

DF <- data.frame(a = c(1))
DF$b <- HTML('<a id="Iclickehase" class="action-button" href="#">I clicked</a>')

output$tab <- renderDataTable({
datatable(
DF,
escape = FALSE,
selection = 'none',
options = list(
preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } ')
)
)
})

observeEvent(input$Iclickehase, {
newvalue <- "B"
updateTabItems(session, "panels", newvalue)
})
observeEvent(input$link_to_tabpanel_a, {
newvalue <- "A"
updateTabsetPanel(session, "panels", newvalue)
})
}

shinyApp(ui, server)

Register all inputs inside a multi-page data table

Here is a workaround based on your addendum (not sure if you need the changes regarding btn_restoreDefault and btn_saveSelection), but the general procedure should be clear:

# Set up environment ------------------------------------------------
library(shiny)
library(DT)
library(magrittr)

# Example of data coming from the database. -------------------------

set.seed(pi^2)

SourceData <-
data.frame(sample_id = 1:25,
is_selected = sample(c(TRUE, FALSE), 25, replace = TRUE))

# Support Functions -------------------------------------------------
# These would exist, for example, in an internal package

shinyInput <- function (FUN, id_base, suffix, label = "", ...)
{
inputId <- paste0(id_base, suffix)
args <- list(...)
args <- c(list(label = label), args)
args <- lapply(args, function(a) rep(a, length.out = length(inputId)))
rv <- character(length(inputId))
for (i in seq_along(rv)) {
this_arg <- lapply(args, `[`, i)
ctrl <- do.call(FUN, c(list(inputId = inputId[i]), this_arg))
rv[i] <- as.character(ctrl)
}
rv
}

prepareDataForDisplay <- function(Data){
Data$is_selected <-
shinyInput(shiny::checkboxInput,
id_base = "is_selected_",
suffix = Data$sample_id,
value = Data$is_selected)

Data
}

# User Interface ----------------------------------------------------

ui <-
fluidPage(
verbatimTextOutput("value_check"),

actionButton(inputId = "btn_saveSelection",
label = "Save Selection"),
actionButton(inputId = "btn_selectAll",
label = "Select All"),
actionButton(inputId = "btn_unselectAll",
label = "Unselect All"),
actionButton(inputId = "btn_restoreDefault",
label = "Restore Default (select odd only)"),

DT::dataTableOutput("dt")
)

# Server ------------------------------------------------------------

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

# Event Observers -----------------------------------------------

observeEvent(
input$btn_selectAll,
{
TmpData <- SourceData
TmpData$is_selected <- TRUE
replaceData(dt_proxy, prepareDataForDisplay(TmpData))
}
)

observeEvent(
input$btn_unselectAll,
{
TmpData <- SourceData
TmpData$is_selected <- FALSE
replaceData(dt_proxy, prepareDataForDisplay(TmpData))
}
)

observeEvent(
input$btn_restoreDefault,
{
replaceData(dt_proxy, prepareDataForDisplay(SourceData))
}
)

observeEvent(
input$btn_saveSelection,
{

check_input <- names(input)[grepl("is_selected_", names(input))]

id <- as.numeric(sub("is_selected_", "", check_input))

TmpData <- SourceData

for (i in seq_along(check_input)){
TmpData$is_selected[TmpData$sample_id == id[i]] <-
input[[check_input[i]]]
}

# At this point, I would also save changes to the remote database.

DT::replaceData(proxy = dt_proxy,
data = prepareDataForDisplay(TmpData))
}
)

# Output elements -----------------------------------------------

output$value_check <-
renderPrint({
sort(names(input))
})

output$dt <-
DT::renderDataTable({
SourceData %>%
prepareDataForDisplay() %>%
DT::datatable(selection = "none",
escape = FALSE,
filter = "top",
class = "compact cell-border",
options = list(preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
})

dt_proxy <- DT::dataTableProxy("dt")

})

# Run the application -----------------------------------------------

shinyApp(
ui = ui,
server = server
)

React state updating but not showing while render

as per react docs, UI components shouldn't be in state and it should only contain the minimal amount of data needed to represent your UI's state.

so instead of adding a UI element in the state, you should just add a minimal amount of data in it

Here's an example based on your code

constructor(props) {
...
this.state = {
status: "loading",
data: "Fetching Records....",
create_box: "",
};
}

table(d) {
this.setState({ data: d, status: "table" });
}

renderData() {
const d = this.state.data
const status = this.state.status
if (status == "loading") { return d }
if (status == "table") { return <Table data={d} /> }
}

render() {
return (
<>
...
<div className="row m-0 py-2 px-4">{renderData()}</div>
...
</>
);
}

Using react-table 7, editable cell re-renders on every change when using two-way binding to table's data source and I lose focus on the input

I ended up not being able to get around this, and had to manage state in each cell as in the example. I do want to share some code that I created for a cell with a select tag instead of a regular input tag:

import React from "react";

const SelectCell = ({
row,
column,
value: initialValue,
updateData,
ddOptions
}) => {
let options = <option key={"productLoading"} value={""}>Loading...</option>
if (ddOptions) {
options = ddOptions.map((prod) => (
<option key={prod.id} value={prod.id}>
{prod.name}
</option>
));
};
return (
<select value={initialValue} onChange={(e) => updateData(column, row, e.target.value)}>
{options}
</select>
);
};

export default SelectCell;

Then defined my ddOptions on my table instance:

  const ddOptions = props.ddOptions; 
const tableInstance = useTable(
{
columns,
data,
defaultColumn,
updateData,
ddOptions
},
useSortBy,
usePagination,
updateData,

);

With ddOptions passed around from wherever you want in this format:

[
{
value: 1,
name: "Test Name"
},
{
value: 2,
name: "Test Name 2"
},
{
value: 3,
name: "Test Name 3"
},
]


Related Topics



Leave a reply



Submit