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)
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
How to Update Window.Location.Hash Without Jumping the Document
The Mystery of the Disappearing Checkmarks
What Is the Fastest Factorial Function in JavaScript
How to Make Regular Expression into Non-Greedy
What Are "Top Level JSON Arrays" and Why Are They a Security Risk
How to Force Reloading a Page When Using Browser Back Button
Override Console.Log(); for Production
Deserializing a JSON into a JavaScript Object
Simplest Way to Wait Some Asynchronous Tasks Complete, in JavaScript
Angular 4.3.3 Httpclient:How Get Value from the Header of a Response
Convert a Number into a Roman Numeral in JavaScript
How to Detect If a Function Is Called as Constructor