shinyr app using dt, javascript callback react to observeEvent table_cell_edit - javascript

I am determined to learn R-Shiny, for this I am trying to implement an editable table using the DT package, that reads and writes the edits to a database using the RSQLite package, and this actually works so I am excluding this part of the code from the example below to drastically improve the readability.
I am able to write the edits with the approach suggested on the shiny website https://rstudio.github.io/DT/shiny.html that links to the examples in this page https://yihui.shinyapps.io/DT-edit/ .
With the following code code, once I inline edit a cell, I can print a couple of debug lines and using the dbExecute() function I can send the edit to the database.
observeEvent(input$data_cell_edit, {
info = input$data_cell_edit
str(info)
print(info)
print("Edit Triggered")
#Here I write my edits
}
Now I'd also like to add a dropdown menu for editing factorial columns, so I suggested to follow the approach suggested at this answer render dropdown for single column in DT shiny
Using the javascript below I can replace the cells with the corresponding dropdown menu. However the problem here is that once an edit is performed via the dropdown menu, the event data_cell_edit is not triggered, so the database update can't be performed.
While an option could be to enclose the write to the database directly in the javascript code, the best way for me would be to improve that javascript to trigger the data_cell_edit callback somehow.
Is this possible at all? Also, for some reasons
library(shiny)
library(DT)
ui <- fluidPage(
DT::dataTableOutput('foo'),
verbatimTextOutput('sel')
)
server <- function(input, output, session) {
data <- head(iris, 5)
for (i in 1:nrow(data)) {
data$species_selector[i] <- as.character(selectInput(paste0("sel", i), "", choices = unique(iris$Species), width = "100px"))
}
output$foo = DT::renderDataTable(
data,
escape = FALSE,
editable=TRUE,
callback = JS("table.rows().every(function(i, tab, row) {
var $this = $(this.node());
$this.attr('id', this.data()[0]);
$this.addClass('shiny-input-container');
});
Shiny.unbindAll(table.table().node());
Shiny.bindAll(table.table().node());")
)
observeEvent(input$foo_cell_edit, {
info = input$foo_cell_edit
print("event triggered")
#Here I write my edits
})
}
shinyApp(ui, server)
tldr:
with the code above, when I edit a normal cell, I trigger the event that writes "event triggered" to the console.
when I edit a cell with the dropdown menu, this event is not triggered.
How can I trigger that event?

Related

Executing JavaScript on multi-row table rendering

I'm using a custom Primefaces-based framework to display a datatable, and it looks like that:
<xy:dataTable id="tableId" value="#{lazyTableBean.dates}" var="date">
<xy:column id="nameColumnId">
<xy:outputText id="nameOutputId" value="date.name"/>
</xy:column>
<xy:column id="actionColumnId">
<xy:actionButton id="actionButtonId" label="Button"
action="#{someBean.someAction(date.id)}"/>
</xy:column>
</xy:dataTable>
Now I want to set the tooltip of the button. Since the actionButton component of that framework doesn't have the title attribute, I'm using JavaScript to alter it:
var rows = // getting the table content row components here
// iterating through table rows and setting the button tooltip to the name of the corresponding date
for (const row of rows) {
var myTooltip = row.children.item(0).textContent;
row.children.item(1).firstChild.setAttribute("title", myTooltip);
}
This basically works as it should when I import the JS script at the end of the file.
However, there are several AJAX events (e.g. when sorting or filtering the table, or when using pagination...) that reprint the table content. Since the JS script isn't triggered again, the tooltips aren't set in that case.
Now I've planned to simply import the script at some appropriate place (e.g. inside the component that gets rerendered) so that it's executed whenever the button is rendered. However, I haven't found quite the right place to make it work. When I'm putting it inside the column:
<xy:dataTable id="tableId" value="#{lazyTableBean.dates}" var="date">
<xy:column id="nameColumnId">
<xy:outputText id="nameColumnId" value="date.name"/>
</xy:column>
<xy:column id="actionColumnId">
<xy:actionButton id="actionColumnId" label="Button"
action="#{someBean.someAction(date.id)}"/>
<h:outputScript library="js" name="addTooltipToTableButtons.js" />
</xy:column>
</xy:dataTable>
This results in only the first row to correctly set their tooltip, all other rows keep their generic one. But on AJAX events, the correct behavior takes place, all rows set their tooltip correctly. The same behavior takes place if the script is also imported at the end. I guess this has to do with the table format of dynamically printing a number of rows with the same column components, but this is just guessing.
Putting it inside the table (directly before </xy:dataTable>) results in no script execution at all.
I'm totally new to JavaScript and we're just using this approach until our custom framework supports setting arbitrary attributes. I hope you have an idea (or an explanation why it won't work like that) - thanks in advance!
Greetings
In case anyone's interested in my solution, I used a MutationObserver to handle the events, in addition to the "normal" JS at page load.
The whole JS file looked like that:
var table = ...; // get table by normal means
for (var i = 0, row; row = table.rows[i]; i++) {
var tooltip = row.cells[0].textContent;
row.cells[1].firstChild.setAttribute(tooltip);
}
var observer = new MutationObserver(function( mutations ) {
mutations.forEach(function( mutation ) {
var newNodes = mutation.addedNodes;
if( newNodes !== null ) {
var $nodes = $( newNodes );
$nodes.each(function() {
var tooltip = this.cells[0].textContent;
this.cells[1].firstChild.setAttribute(tooltip);
});
}
});
});
var config = {
attributes: true,
childList: true,
characterData: true
};
observer.observe(table.children.item(1), config);

incorporating JS callback function into RShiny DT::renderdatatable options

I am building a Shiny app and leveraging the DTedit library to allow users to edit data tables inline in the UI. This is working well, but I want to add some additional formatting to the tables (making some columns appear as percents, making other columns appear as dollar amounts). The problem with this is that the output of a DTedit function is a rendered output object (it expects to be passed directly to the UI - I can't do any paste0 or sapply operations on it).
The only upside is that I can pass dataframe options arguments to the DTEdit function before the output gets rendered - this includes the ability to pass JS Callbacks. Something like this:
datatable(head(iris, 20), options = list(
initComplete = JS(
"function(settings, json) {",
"$(this.api().table().header()).css({'background-color': '#000', 'color': '#fff'});",
"}")
))
The example above is showing changing the background color of the header to black, but as I mentioned, I'm interested in formatting several columns as percents / dollar amounts.
So this is all well and good, but the only problem is I know nothing about JS! I'm looking for guidance on building the correct JS callback to format my data table - thanks in advance!
I'm afraid I don't know Javascript either, but I know enough R to have modified DTedit to allow formatting with DT's format*() functions.
A modified version of DTedit is available on my Github repository, and is referenced as a pull request on jbryer/DTedit.
A vignette is available, look under 'formatting columns', and the example code is reproduced below, using the mtcars dataset.
library(DTedit)
library(magrittr) # provides the pipe '%>%' operator
server <- function(input, output, session) {
dtedit(
input, output,
name = 'mtcarstable',
thedata = mtcars,
datatable.rownames = TRUE, # needed for the format*() functions to work
datatable.call = function(...) {
datatable(...) %>%
formatSignif('qsec', 2) %>%
formatCurrency('mpg') %>%
formatStyle(
'cyl',
color = 'red', backgroundColor = 'orange', fontWeight = 'bold'
)
# note, none of this is proper formatting for the mtcars data!
# but serves to demonstrate the formatting
}
)
}
ui <- fluidPage(
h3('mtcars'),
uiOutput('mtcarstable')
)
shinyApp(ui = ui, server = server)
The formatting done is, by and large, not actually appropriate to the mtcars dataset, but used just as an example. Picture of formatted mtcars table

Ag-Grid rowDataChanged not firing properly

Here is my onDataChanged() event. It's plugged into my Ag-Grid HTML. It does fire (3 times) but each time it only thinks there's 1 row being displayed. I'm using the serverSide row model and data coming in from the server is a bit slow, so I think that's the problem. I need to have this event fire when the data is changed though, so I can perform some actions when I have a full list of data. At the moment, again, it only ever thinks there's 1 row being displayed when I see that there are 20 in the list.
onDataChanged: function(event) {
var count;
console.log("data changed");
count = this.gridOptions.api.getDisplayedRowCount();
console.log(count);
}
// only ever outputs "1" even though I see 20+ items in the list
Because I'm using the serverSide row model, I'm using a serverDatasource to populate the data. Is there another way to detect when data has been changed? Thank you

Invoke JavaScript function at GridView cell click

I have a DevExpress gridview with the following settings.
settings.Name = "DetailGridView";
// this calls DetailGridView.StartEditRow() on client side
settings.ClientSideEvents.RowClick = "Fn.startEditingRow";
settings.SettingsEditing.Mode = DevExpress.Web.GridViewEditingMode.Inline;
I have removed many other settings I have for simplicity, but ask if you think I need to show some other settings which are relevant.
Now I want to invoke a JavaScript function at each cell click. To do that I have added this settings, as per this SO answer, and this DevExpress thread
settings.HtmlDataCellPrepared += (sender, e) =>
{
string onClickFunctionJS = "Fn.DetailOnlyOnCellClick({0},'{1}');";
e.Cell.Attributes.Add("onclick", String.Format(onClickFunctionJS, e.VisibleIndex, e.DataColumn.FieldName));
};
The JS function Fn.DetailOnlyOnCellClick() prints to console the value of the field name (the 2nd argument). But it only prints clicked cell's field name the first time the row is clicked. After that clicking on a different cell in the selected row doesn't trigger the function Fn.DetailOnlyOnCellClick() anymore. I have observed that if I turn off ClientSideEvents.RowClick, it works fine, but I can't turn that off for other reasons. How can I get the which cell the user clicks on while keeping ClientSideEvents.RowClick on?
I have solved my problem, thanks to DevExpress support center. First, I couldn't use settings.HtmlDataCellPrepared because, as I mentioned in the question, the grid view enters edit mode when a row is clicked, and in edit mode settings.HtmlDataCellPrepared is not supposed to work the way I need it for my situation. So I started to use this code:
settings.CellEditorInitialize = (s, e) =>
{
var elementEditor = e.Editor;
string onClickFunctionJS = "DetailOnlyOnCellClick({0},'{1}');";
DevExpress.Web.Rendering.GridViewTableInlineEditorCell cell =
elementEditor.Parent as DevExpress.Web.Rendering.GridViewTableInlineEditorCell;
cell.Attributes.Add("onclick",
String.Format(onClickFunctionJS, e.VisibleIndex, e.Column.FieldName));
};
This code worked fine for every different control. The only catch was when the field is a checkbox and the cell is disabled. To get around this, I have used this code for checkbox controls:
string func = String.Format(
"ASPxClientUtils.AttachEventToElement(s.GetMainElement(), 'click', function() {{ {0} }} )",
String.Format(onClickFunctionJS, e.VisibleIndex, e.Column.FieldName));
(elementEditor as ASPxCheckBox).ClientSideEvents.Init = String.Format("function(s,e){{ {0}; }}", func);
This code works perfectly.
Links to the two methods used:
ASPxClientUtils.AttachEventToElement
ASPxClientControlBase.GetMainElement
P.S.: Both these approaches I have shown here are from DevExpress experts, and I got these answers from DevExpress's support center. I will provide link to my question once I can fix the issue with license and the ticket becomes public.

How to initialize data table in R shiny to show user which columns are editable

I have a data table in R shiny which I have made editable using the DT package editor package. My problem now is I would like to initialize the table so that user can clearly see which columns they can edit. My current table initializes like this:
I would like the data table to initially look like this:
My first thought was to adjust the value of input$table_cell_clicked when the app is launched to the value it should be when activated. I couldn't figure out how to do this.
My next idea was to simulate a click event using JavaScript. I was able to simulate the click event (shown in example code) but I'm not sure how to access specific elements within the table. Also, I feel there may be a better way to do it than simulating a bunch of click events on the table. Here's my code:
#devtools::install_github('rstudio/DT#feature/editor')
#install.packages("shinythemes")
library(shiny)
library(data.table)
library(DT)
library(shinyjs)
library(shinythemes)
library(V8)
jscode <- '$(document).ready(function() {
setTimeout(function(){$("#clickMe").trigger("click");},100);
});'
ui <- fluidPage(tags$script(jscode)
,actionButton("clickMe", "Click Me")
,dataTableOutput("table")
)
server <- function(input, output,session) {
Data<-data.frame(x1=1:10,x2=1:10,x3=11:20)
values <- reactiveValues(dfWorking = Data)
observeEvent(input$clickMe,{
values$dfWorking<-data.frame(x1=11:20,x2=11:20,x3=31:40)
})
observeEvent(input$table_cell_edit, {
info = input$table_cell_edit
i = info$row
j = info$col+1
v = info$value
values$dfWorking[i, j] <<- DT:::coerceValue(v, values$dfWorking[i, j])
})
output$table <- renderDataTable({values$dfWorking}
,escape=FALSE,server=FALSE,selection='single',rownames=FALSE)
}
shinyApp(ui = ui, server = server)

Categories