I have a DT table in shiny app that have background color set to match certain values. I'm also using the selected rows in table to control other part of the app. Now my problem is to make it obvious which rows are selected.
Usually selected rows in table will have background color changed, but I don't have this option since I set the background color already and don't want to change it. Changing foreground color (font color) for selected rows is not optimal as this is not obvious and intuitive.
Right now I'm making selected rows have different opacity with unselected rows, which works to some degree but still not optimal.
One approach can be add some checked icon to the selected rows. Note I don't want real checkbox input because that will lead user to click the checkbox, while I think it's easier just to click row to select.
There are some examples to show html content in DT table, however that will mean dynamically change table content by row selection, which is not acceptable to my app, since each table content change will trigger table refresh, which reset the row selection and come into a loop.
I think it should be possible to use js to change selected rows css class and thus add a checked icon to them. I saw this question which is kind of similar, however the example is hard to understand to me.
Update: This answer by #Stéphane Laurent solved my problem exactly. I searched SO extensively before but didn't find this.
Update 2: My use cases is more complex, and I'm having problems adapting this approach. I need 2 control tables, and I'm switching them based on a radio button control. With dynamic rendering of the table, the excluded status get reset in every switch. Previously I used DT row selection which don't have this problem.
See example below, exclude some rows in table 1, switch to table 2, then switch back, the exclude status is restored.
library(shiny)
library(DT)
# DT checked js ----
rowNames <- FALSE # whether to show row names in the table
colIndex <- as.integer(rowNames)
# making variants since we have two table. not worth a function since only two instances. main changes are function name and shiny input id excludedRows
callback1 <- c(
sprintf("table.on('click', 'td:nth-child(%d)', function(){", colIndex+1),
" var td = this;",
" var cell = table.cell(td);",
" if(cell.data() === 'ok'){",
" cell.data('remove');",
" } else {",
" cell.data('ok');",
" }",
" var $row = $(td).closest('tr');",
" $row.toggleClass('excluded');",
" var excludedRows = [];",
" table.$('tr').each(function(i, row){",
" if($(this).hasClass('excluded')){",
" excludedRows.push(parseInt($(row).attr('id').split('_')[1]));",
" }",
" });",
" Shiny.setInputValue('excludedRows1', excludedRows);",
"})"
)
callback2 <- c(
sprintf("table.on('click', 'td:nth-child(%d)', function(){", colIndex+1),
" var td = this;",
" var cell = table.cell(td);",
" if(cell.data() === 'ok'){",
" cell.data('remove');",
" } else {",
" cell.data('ok');",
" }",
" var $row = $(td).closest('tr');",
" $row.toggleClass('excluded');",
" var excludedRows = [];",
" table.$('tr').each(function(i, row){",
" if($(this).hasClass('excluded')){",
" excludedRows.push(parseInt($(row).attr('id').split('_')[1]));",
" }",
" });",
" Shiny.setInputValue('excludedRows2', excludedRows);",
"})"
)
# for select all, not using it now
# restore <- c(
# "function(e, table, node, config) {",
# " table.$('tr').removeClass('excluded').each(function(){",
# sprintf(" var td = $(this).find('td').eq(%d)[0];", colIndex),
# " var cell = table.cell(td);",
# " cell.data('ok');",
# " });",
# " Shiny.setInputValue('excludedRows', null);",
# "}"
# )
render <- c(
'function(data, type, row, meta){',
' if(type === "display"){',
' var color = data === "ok" ? "#027eac" : "gray";',
' return "<span style=\\\"color:" + color +',
' "; font-size:18px\\\"><i class=\\\"glyphicon glyphicon-" +',
' data + "\\\"></i></span>";',
' } else {',
' return data;',
' }',
'}'
)
# test app ----
ui <- fluidPage(
tags$head(
tags$style(HTML(
".excluded { color: gray; font-style: italic; }"
))
),
fluidRow(
column(
6,
tags$label("Excluded rows Table 1"),
verbatimTextOutput("excludedRows1"),
tags$label("Excluded rows Table 2"),
verbatimTextOutput("excludedRows2")
),
column(
6,
tags$label("Included rows"),
verbatimTextOutput("includedRows1"),
verbatimTextOutput("includedRows2")
)
),
br(),
radioButtons("select_table", label = "Select table", choices = c("1", "2"), inline = TRUE),
uiOutput("control_table_ui")
# tabBox(tabPanel("1", DTOutput("mytable1")),
# tabPanel("2", DTOutput("mytable2")))
)
server <- function(input, output,session) {
output$control_table_ui <- renderUI({
if (input$select_table == "1") {
column(12, offset = 0, DTOutput("mytable1"))
} else {
column(12, offset = 0, DTOutput("mytable2"))
}
})
dt <- cbind(On = "ok", mtcars[1:6,], id = paste0("row_",1:6))
row_colors <- rep(c("red", "blue", "green"), 2)
names(row_colors) <- dt$id
output[["mytable1"]] <- renderDT({
datatable(dt, caption = "table 1",
rownames = rowNames, extensions = c("Select"),
selection = "none", callback = JS(callback1),
options = list(
# pageLength = 3,
sort = FALSE,
rowId = JS(sprintf("function(data){return data[%d];}",
ncol(dt)-1+colIndex)),
columnDefs = list(
list(visible = FALSE, targets = ncol(dt)-1+colIndex),
list(className = "dt-center", targets = "_all"),
list(className = "notselectable", targets = colIndex),
list(targets = colIndex, render = JS(render))
),
dom = "t",
# buttons = list(list(
# extend = "collection",
# text = 'Select All',
# action = JS(restore)
# )
# ),
select = list(style = "single", selector = "td:not(.notselectable)")
# select = list(style = 'os', # set 'os' select style so that ctrl/shift + click in enabled
# items = 'row') # items can be cell, row or column
)
) %>%
formatStyle("id", target = "row",
backgroundColor = styleEqual(dt$id, row_colors))
}, server = FALSE)
output[["mytable2"]] <- renderDT({
datatable(dt, caption = "table 2",
rownames = rowNames, extensions = c("Select"),
selection = "none", callback = JS(callback2),
options = list(
# pageLength = 3,
rowId = JS(sprintf("function(data){return data[%d];}",
ncol(dt)-1+colIndex)),
columnDefs = list(
list(visible = FALSE, targets = ncol(dt)-1+colIndex),
list(className = "dt-center", targets = "_all"),
list(className = "notselectable", targets = colIndex),
list(targets = colIndex, render = JS(render))
),
dom = "t",
# buttons = list(list(
# extend = "collection",
# text = 'Select All',
# action = JS(restore)
# )
# ),
select = list(style = "single", selector = "td:not(.notselectable)")
)
) %>%
formatStyle("id", target = "row",
backgroundColor = styleEqual(dt$id, row_colors))
}, server = FALSE)
output$excludedRows1 <- renderPrint({
input[["excludedRows1"]]
})
output$excludedRows2 <- renderPrint({
input[["excludedRows2"]]
})
output$includedRows1 <- renderPrint({
setdiff(1:nrow(dt), input[["excludedRows1"]])
})
}
shinyApp(ui, server)
Update 3: Per #Stéphane Laurent 's suggestion, using conditionalPanel solved the problem. Although it's a little bit slower than renderUI, but it's working.
Thanks to #StéphaneLaurent 's answer which is a great js based solution and solved my 95% needs. However I need a button to clear all selection and cannot write that one because of my limited js skills. I also forgot the important server=FALSE parameter so met problem of sorting lost selection. Thus I switched back to my original row selection mechanism.
I used to try to modify the table by row selection, but that will trigger reactive event loop. Later I realized I only need to change the view, not the underlying data, and changing view is possible by purely css rules.
Checking the great example here, the more icons example can show different icon depend on checkbox selection. By inspecting the css rules, I found both icons are there all the time, just the css rule is different depend on selection status.
Thus I came up with this solution, which used the builtin row selection in DT and some css rules, this way you still have all the feature of row selection control in DT without needs of js code, and everything is implemented by css.
library(shiny)
library(DT)
library(data.table)
ui <- fluidPage(
tags$head(
tags$style(HTML("
.selected .table-icon-yes {
opacity: 1;
display: inline-block;
color: #3c763d;
}
.table-icon-yes {
opacity: 0;
display: none;
}
.selected .table-icon-no {
opacity: 0;
display: none;
}
.table-icon-no {
opacity: 1;
display: inline-block;
color: #999;
}
"))
),
DTOutput("table")
)
icon_col <- tagList(span(class = "table-icon-yes", icon("ok", lib = "glyphicon")),
span(class = "table-icon-no", icon("remove", lib = "glyphicon")))
server <- function(input, output, session) {
output$table <- renderDT({
dt <- data.table(iris)
dt[, Selected := as.character(icon_col)]
setcolorder(dt, c(ncol(dt), 1:(ncol(dt) - 1)))
datatable(dt, escape = FALSE)
})
}
shinyApp(ui = ui, server = server)
I have a first DT table oTable with cell selection enabled. When the user click (select) a cell, that will generate another DT table nTable.
Then, in nTable I want to insert a selectInput. The code below is a working example. Mostly adapted from this post.
Problem:
When nTable is regenerated, the connection (binding?) with shinyValue is somehow broken.
Step to reproduce the problem:
launch the app.
select top left cell (e.g. Sepal.Length=5.1). In fact, select any cell will also work.
In the second DT generated below, change the selectInput in col from A to something else, say, B. Check that this change is detected in the TableOutput below.
De-select the selected cell
Re-select the same cell.
Now, you can change the selectInput again but no changes will be detected.
Also, I am not sure how to use session$sendCustomMessage("unbind-DT", "oTable"), I tried changing oTable to nTable but that didn't work.
library(shiny)
library(DT)
runApp(list(
ui = basicPage(
tags$script(
HTML(
"Shiny.addCustomMessageHandler('unbind-DT', function(id) {
Shiny.unbindAll($('#'+id).find('table').DataTable().table().node());
})"
)
),
h2('The data'),
DT::dataTableOutput("oTable"),
DT::dataTableOutput("nTable"),
h2("Selected"),
tableOutput("checked")
),
server = function(input, output, session) {
# helper function for making checkbox
shinyInput = function(FUN, len, id, ...) {
inputs = character(len)
for (i in seq_len(len)) {
inputs[i] = as.character(FUN(paste0(id, i),label=NULL, ...))
}
inputs
}
mydata=reactive({
session$sendCustomMessage("unbind-DT", "oTable")
input$oTable_cells_selected
})
output$nTable=renderDataTable({
req(mydata())
dd=as.data.frame(mydata())
dd$col=shinyInput(selectInput,nrow(dd),"selecter_",choices=LETTERS[1:3])
dd
},selection='none',server=FALSE,escape=FALSE,rownames=FALSE,
options=list(
preDrawCallback = JS(
'function() {
Shiny.unbindAll(this.api().table().node()); }'
),
drawCallback = JS('function() {
Shiny.bindAll(this.api().table().node()); } ')
))
output$oTable=renderDataTable(DT::datatable(iris,selection=list(mode="multiple",target='cell')))
# helper function for reading select input
shinyValue = function(id, len) {
unlist(lapply(seq_len(len), function(i) {
value = input[[paste0(id, i)]]
if (is.null(value))
NA
else
value
}))
}
# output read selectInput
output$checked <- renderTable({
req(mydata())
data.frame(selected = shinyValue("selecter_", nrow(mydata())))
})
}
))
You have to run Shiny.unbindAll on nTable (the table which contains the inputs). But only after the table has been created a first time.
library(shiny)
library(DT)
runApp(list(
ui = basicPage(
tags$head(tags$script(
HTML(
"Shiny.addCustomMessageHandler('unbindDT', function(id) {
var $table = $('#'+id).find('table');
if($table.length > 0){
Shiny.unbindAll($table.DataTable().table().node());
}
})"
))
),
h2('The data'),
DT::dataTableOutput("oTable"),
DT::dataTableOutput("nTable"),
h2("Selected"),
tableOutput("checked")
),
server = function(input, output, session) {
# helper function for making checkbox
shinyInput = function(FUN, len, id, ...) {
inputs = character(len)
for (i in seq_len(len)) {
inputs[i] = as.character(FUN(paste0(id, i),label=NULL, ...))
}
inputs
}
observeEvent(input$oTable_cells_selected, {
session$sendCustomMessage("unbindDT", "nTable")
})
mydata = eventReactive(input$oTable_cells_selected, {
if(length(input$oTable_cells_selected)){
input$oTable_cells_selected
}
})
output$nTable=DT::renderDataTable({
req(mydata())
dd=as.data.frame(mydata())
dd$col=shinyInput(selectInput,nrow(dd),"selecter_",choices=LETTERS[1:3])
datatable(dd, selection='none', escape=FALSE,rownames=FALSE,
options=list(
preDrawCallback = JS(
'function() {
Shiny.unbindAll(this.api().table().node()); }'
),
drawCallback = JS('function() {
Shiny.bindAll(this.api().table().node()); } ')
))
},server=FALSE)
output$oTable=DT::renderDataTable(
DT::datatable(iris,selection=list(mode="multiple",target='cell'),
options=list(pageLength = 5)))
# helper function for reading select input
shinyValue = function(id, len) {
unlist(lapply(seq_len(len), function(i) {
value = input[[paste0(id, i)]]
if (is.null(value))
NA
else
value
}))
}
# output read selectInput
output$checked <- renderTable({
req(mydata())
data.frame(selected = shinyValue("selecter_", nrow(mydata())))
})
}
))
[![enter image description here][1]][1]i have a nested table , I want to having button to generate the row data to info.php by post method (looks like info.php?user = data[0] & key2 = data2) in one column for each row ,
I have one button but I need one button and perform some MySql when they are clicked to get the row data .
when click the button will get to every columns data in the row and post these data to info.php and view in popup window,
How can I perform post the row data in the nested datatable to other php using the button?
my code
click the button ,cannot get the row data ?
$('#example tbody').on( 'click', 'button', function () {
var index = $(this).closest('tr').index();
var data = table.row( $(this).parents('tr') ).data();
alert("References is "+ data[0] +"and section is "+ data[ 1 ]+ " and Stature Titles is "+data[2] );
} );
-UPDATED
just add class for button class='button-info'
columns:[
{ data:'name' },
{ data:'position' },
{ data:'salary' },
{
"targets": -1,
"data": null,
"defaultContent": "<button class='button-info'>Click Me!</button>"
}
]
first assign index value for every parent row
$("table tbody tr").each(function(index) {
$(this).attr('index', index);
})
then add new event for click event of that button and get the parent tr index
just get the index of your selected parent row using data attribute "index" added above
var parent = $(this).closest('table').parents('tr').index();
var parentIndex = $('tbody tr:nth-child('+(parent)+')').attr('index');
and to get your current row in nested data
var index = $(this).closest('tr').index();
so this is the final
$('table').on( 'click', 'td .button-info', function () {
var parent = $(this).closest('table').parents('tr').index();
var parentIndex = $('tbody tr:nth-child('+(parent)+')').attr('index');
var currentIndex = $(this).closest('tr').index();
var data = sections[parentIndex][currentIndex];
console.log(data);
return;
window.open("/info.php?name=" + data.name + "&sal=" + data.salary);
} );
See this updated JSFiddle
I am working on a wordpress blog with a custom metabox on the edit page of each post.
This metabox consists of table with each row containing image src selected from media library.
Now every new row added has an id :
row 1 : img_metabox_src_0
row 2 : img_metabox_src_1
row 3 : img_metabox_src_2
Table headers goes like :
----Image < img >------ |------- URL (Input textbox)------ | -------- Select Image (Input submit)------ | -----Delete Image (Input submit)--------
Now,
On click on "Select Image" on any row, I retrieve the row index from jquery, and then send : "img_metabox_src_"+index to file_frame.on( 'select', function() for url update.
i.e.
jQuery('tr #select_image').off().on('click', function( event ){
event.preventDefault();
var row_index = jQuery(this).closest('tr').index();
var id = "img_metabox_src_" + row_index;
//******** 1 ***********
console.log('row_index');
console.log(row_index);
console.log(id);
console.log(jQuery('#' + id));
if ( file_frame ) {
file_frame.open();
return;
}
file_frame = wp.media.frames.file_frame = wp.media({
title: "Select/Upload Image",
button: {
text: "Select",
},
library : { type : 'image'},
multiple: false
});
file_frame.on( 'select', function() {
attachment = file_frame.state().get('selection').first().toJSON();
// "mca_features_tray" is the ID of my text field that will receive the image
// I'm getting the ID rather than the URL:
// but you could get the URL instead by doing something like this:
//******** 2 ***********
console.log(id);
console.log(jQuery('#' + id));
jQuery('#' + id).attr('value',attachment.url);
id = null;
});
Now,
Case 1 : When I FIRST click with row index3, the URL updates on img_metabox_src_3.
Case 2 : But after that whichever row i click, the url updates on img_metabox_src_3.
Also on adding logs, I get
(for Case 2, say I clicked row index 1) :
//******** 1 ***********
row index : 1
id : img_metabox_src_1
//******** 2 ***********
id : img_metabox_src_3
i.e. inside file_frame.on( 'select', function() {,
the ID value changes to first clicked value.
Please help on how to pass updated row index/id to the select function
Thanks, I used global concept :
function set_row_index (ind){
row_index = ind;
}
function get_row_index(){
return row_index;
}
jQuery(document).ready(function(){
jQuery('tr input.select_media_library').off().on('click', function( event ){
event.preventDefault();
var index = jQuery(this).closest('tr').index();
**set_row_index(index);**
.
.
.
file_frame.on( 'select', function() {
attachment = file_frame.state().get('selection').first().toJSON();
**index = get_row_index();**
var id = "img_src_" + index;
jQuery('#' + id).attr('value',attachment.url);
});
file_frame.open();
});
I have following code for jQuery DataTables:
Contact.DataTable = $('#otable').DataTable( {
"ajax": {
"url" : '/Contact/' + Contact.id,
"dataSrc": function(check) {
return check.data;
},
},
"responsive": true,
"columns": [
{ "data": "id"},
{ "data": "category", "sClass": "category" },
{ "data": "name", "sClass": "name" },
{ "data": "lname" },
{
"render": function ( data, type, method, meta ) {
return Contact.saveContact(method);
}
},
]
} );
Datatable - dropdown - inline edit:
$('#otable tbody').on('click', '.category', function () { //second column
var row = this.parentElement;
if(!$('#otable').hasClass("editing")){
$('#otable').addClass("editing");
var data = Contact.DataTable.row(row).data();
var $row = $(row);
var thiscategory = $row.find("td:nth-child(2)");
var thiscategoryText = thiscategory.text();
thiscategory.empty().append($("<select></select>",{
"id":"category" + data[0],
"class":"in_edit"
}).append(function(){
var options = [];
$.each(Categories, function(key, value){
options.push($("<option></option>",{
"text":value,
"value":value
}))
})
return options;
}));
$("#category" + data[0]).val(thiscategoryText)
}
})
;
For changing values in dropdown
$('#otable tbody').on("change", ".in_edit", function(){ //Inline editing
var val = $(this).val();
$(this).parent("td").empty().text(val);
$('#otable').removeClass("editing");
});
Below code for saving new values(after inline edit) while clicking save:
$('#divsave').on("click", ".saveContact", function() {
var data = Contact.DataTable.row($(this).closest('tr')).data();
// Here I have to get new values after inline editing - but getting old values
});
My problem is : while clicking edit, in 'data', I am getting old values in the row of datatable, not the modified value after inline edit
datatable view - 1:
datatable - dropdown in column:
datatable after inline editing:
What I need: Save modified row while clicking 'save' image - currently it saves older value before inline editing(datatable view - 1)
When using dataTables it is generally a very bad idea to manipulate the DOM <table> or any content by "hand" - you should always go through the dataTables API.
Thats why you are getting "old values" - you have manipulated the content of the <table>, or so it seems - dataTables are not aware of those changes.
In a perfect world you should refactor the setup completely (i.e to use the API) but I guess you can solve the issue by using invalidate() on the row being changed in order to refresh the dataTables internals :
$('#otable tbody').on("change", ".in_edit", function(){ //Inline editing
var val = $(this).val();
$(this).parent("td").empty().text(val);
//add this line to refresh the dataTables internals
Contact.DataTable.row($(this).parent("tr")).invalidate();
//
$('#otable').removeClass("editing");
});