Disabling selectizeInput in second page of datatable in RShiny - r

I have a datatable, in which I have embedded selectizeInputs. I have used jquery to enable some options in the selectizeInputs (like creation options).
Now, due to a business use case I would like to disable some selectizeInputs (dynamically selected, through some condition). These inputs could be on the 2nd, 3rd,.. nth page of the datatable as well.
However, I am only able to disable the inputs on the 1st page and not on the subsequent pages. I am attaching a minimal and reproducible example, it would be great if someone could help me out.
library(shiny)
library(DT)
ui <- fluidPage(
shinyjs::useShinyjs(),
selectizeInput(
inputId = "input",
label = "",
choices = letters[1:26],
selected = letters[1]
),
fluidRow(
DTOutput(outputId = "table"),
tags$script(HTML("Shiny.addCustomMessageHandler('unbind-DT', function(id) {
Shiny.unbindAll($('#'+id).find('table').DataTable().table().node());
})"))
)
)
df <- data.frame('a' = c(1,2), 'sel_input' = NA)
df[1,'sel_input'] <- as.character(
selectizeInput(inputId = 'mselect', choices=c('car','cars','dog'),
label=NULL, selected=NULL))
df[2,'sel_input'] <- as.character(
selectizeInput(inputId = 'nselect', choices=c('lambo','audi','merc'),
label=NULL, selected=NULL))
js <- c(
"function(){Shiny.bindAll(this.api().table().node());",
" $('#mselect').selectize({
delimiter: \',\',
persist: false,
create: function(input) {
return {
value: input,
text: input
}
}
});",
"$('#nselect').selectize({
delimiter: \',\',
persist: false,
create: function(input) {
return {
value: input,
text: input
}
}
});",
"$('#mselect')[0].selectize.enable()",
"$('#nselect')[0].selectize.disable()",
"}"
)
server <- function(input, output, session) {
observe({
print(input$mselect)
})
session$sendCustomMessage('unbind-DT', 'table')
output$table <- renderDT({
datatable(
data = df,
escape = FALSE,
options = list(
dom='tp',
pageLength=1,
processing=F,
preDrawCallback = JS('function(){Shiny.unbindAll(this.api().table().node());}'),
drawCallback = JS(js)
)
)
})
}
shinyApp(ui = ui, server = server)

So, I was able to resolve it on my own. Basically the problem here is not R/Rshiny based. It is actually a javascript bug in the code that I was overlooking.
When you do pagination, the elements only in current (selected) page are a part of the DOM. All others are removed/not created. In the code above, in the drawCallback (this is the piece of code that runs every time the datatable needs to be re-rendered) I am issuing commands for all the elements, irrespective if they are present in the DOM or not. Because of this the javascript code fails and the disabling/enabling does not happen.
The solution here is to first check if an element is active in the DOM or not and only then issue enabling/disabling command.
So, in essence, enclose the above commands in a if else statement
if ($('#mselect').length > 0){
$('#mselect').selectize()[0].selectize.enable();
}
if ($('#nselect').length > 0){
$('#nselect').selectize()[0].selectize.disable();
}
This way the javascript code will only run when that specific element exists in the DOM and then you would be able to implement disabling selectInput in second page of paginated datatable.

Related

Combining DT::datatable, DT::dataTableProxy and "SearchPanes" Extension in R Shiny

It appears that DT::dataTableProxy is not possible with SearchPanes extension because:
SearchPanes requires Select extension.
Select extension requires DT::renderDT(server = FALSE) option.
DT::dataTableProxy does not work on the client side and throws DT error.
library(shiny)
library(shinydashboard)
library(tidyverse)
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(
shiny::selectInput("rows", label = "Rows", choices = 1:nrow(mtcars)),
shiny::actionButton("new", label = "New Data")
),
dashboardBody(DT::dataTableOutput("cars"))
)
server <- function(input, output) {
rows <- reactive({ input$rows })
output$cars <- DT::renderDataTable(server = FALSE, {
expr = DT::datatable(
data = mtcars |> head(rows())
#,
#extensions = c("SearchPanes", "Select", "Buttons"),
#options = list(
# dom = "Btip",
# buttons = list("searchPanes")
#)
)
})
dtProxy <- DT::dataTableProxy("cars")
observeEvent(input$new, label = "Observe button proxy update", {
doubledata <- bind_rows(mtcars, mtcars)
DT::replaceData(proxy = dtProxy,
data = doubledata,
resetPaging = FALSE)
})
}
shinyApp(ui, server)
Try this code using server = FALSE, click New Data, you will receive DT Warning:
DataTables warning: table id=DataTables_Table_0 - Invalid JSON response. For more information about this error, please see http://datatables.net/tn/1
Remove server = FALSE and proxy runs.
Remove the commented section, and search panes appear, but with no actual filters represented, and message stating that if we really want to use select extension then set select = 'none'.
Here are some reference materials:
RStudio DT Extensions
Matt Herman Tutorial
What I ended up doing was use only the DT::datatableProxy feature, then use a custom button for the search panes. Custom button was found here How to add custom button in R Shiny datatable. This required making a new reactive which was invalidated by the first, and checking if the inputs had any new values. Then the proxy received the filtered data.
Maybe someday they will add support for search panes.
Add server-side support for SearchPanes extension #877

R Shiny: how to call a JavaScript function from a custom button inside a Datatable

I have downloaded and would like to incorporate in my R Shiny app the following JavaScript library: GitHub link
As you can see in the snippet below, I have incorporated the sequence-viewer.min.js file in the head of the UI. Additionally, using the tags$div(id="sequence-viewer") I am able to see the corresponding <div> tag in the source of the web page, so up to this point I think everything is fine.
Using the following code, I construct and render a sample Datatable with sequences. From the link of the last column, I would like to dynamically change the value of the var seq=new Sequence(''); with the sequences of the Datatable and draw the result in the sequence-viewer div every time a link is clicked.
library(shiny)
ui <- fluidPage(
theme = shinytheme("yeti"),
useShinyjs(),
useShinyalert(),
tags$head(tags$script(src = "handlebars.js"),
tags$script(src = "sequence-viewer.min.js")),
mainPanel( DT::dataTableOutput("DTtable"),
br(),
tags$div(id="sequence-viewer")
)
)
server <- function(input, output) {
exp1 <- reactive({
tbl <- as.data.frame(c("MALWMPGPGAGSL", "MALKYTFDCVBJUYGFGPGAGSL", "IUYTFVBNMKIUYF"))
names(tbl) <- "Sequence"
tbl$link <- createLink(tbl$Sequence)
return(tbl)
})
createLink <- function(val) {
link <- paste0("<a href='#' onclick='var seq=new Sequence('",val,"'); seq.render('#sequence-viewer');'>Show seq</a>", sep="")
return(link)
}
output$DTtable <- DT::renderDataTable({ exp1()
}, escape = FALSE, options = list(scrollX = TRUE, dom = 'lfrtip', pageLength = 10,
lengthMenu=c(10, 25, 50, 100)), rownames = FALSE)
}
# Run the application
shinyApp(ui = ui, server = server)
I have read many threads and tutorials on how to run custom javascript code in R Shiny, but all the examples that I've found make the call in the ui , not in the server side, could you please advice me how to get the desired output?
NOTE: According to the instructions on github page, the dependencies jquery, handlebars and bootstrap.min.css are required. I suppose that only the handlebars.js has to manually be added given that R Shiny comes already with bootstrap and jquery?
UPDATE 1: Ok, I think I'm now almost there. Thanks to the comments of #YBS I managed to get the idea of how to work with external javascript libraries. The code below works fine if I click the actionLink but it does not work when clicking the custom links inside the Datatable that I create using the createLink function. I get the following exception in the console of the browser: Uncaught ReferenceError: js$seque is not defined. Any ideas why this is happening?
library(shiny)
library(shinyjs)
jsCode = '
shinyjs.seque = function(params) {
var defaultParams = {
seq : "LKJHGFVBJKUGFKGFFGHJKUYTRFGHJUYTGHJIUYT"
};
params = shinyjs.getParams(params, defaultParams);
var seq=new Sequence(params.seq);
seq.render("#sequence-viewer");
}
'
ui <- fluidPage(
useShinyjs(),
extendShinyjs(text = jsCode, functions = c("seque")),
tags$head(tags$script(src = "handlebars.js"),
tags$script(src = "sequence-viewer.min.js")
),
mainPanel( DT::dataTableOutput("DTtable"),
br(),
actionLink(inputId = "action",
label = "action"),
br(),
tags$div(id="sequence-viewer")
)
)
server <- function(input, output) {
exp1 <- reactive({
tbl <- as.data.frame(c("MALWMPGPGAGSL", "MALKYTFDCVBJUYGFGPGAGSL", "IUYTFVBNMKIUYF"))
names(tbl) <- "Sequence"
tbl$link <- createLink(tbl$Sequence)
return(tbl)
})
createLink <- function(val) {
link <- paste0("<a href='#' onclick='js$seque(",val,")' id='",val,"' class='action-button shiny-bound-input'>Show sequence</a>", sep="")
return(link)
}
observeEvent(input$action, {
js$seque("MALKYTFDCVBJUYGFGPGAGSL")
})
output$DTtable <- DT::renderDataTable({
exp1()
}, escape = FALSE, options = list(scrollX = TRUE, dom = 'lfrtip', pageLength = 10,
lengthMenu=c(10, 25, 50, 100)), rownames = FALSE)
}
# Run the application
shinyApp(ui = ui, server = server)
UPDATE 2:
After many hours of debbugging I managed to solve the issue by replacing the onclick='js$seque(",val,")' event of the button in the createLink function with the following: onclick='shinyjs.seque(\"",val,"\")' - or alternatively even clearer onclick='shinyjs.seque(JSON.stringify(val))'
In short, the js$seque call was incorrect at this point, I had to replace this line with shinyjs.seque, namely with the actual name of the function in JS. On the other hand, a typical R actionButton element requires js$seque. I will try to write a clear MRE code and provide it as an answer of this thread.
The usage of shinyjs is an overkill in your case, because you do not want to call the JS function from R but anyways through the client. Thus you can simply use plain JavaScript like in this toy example:
library(shiny)
js <- HTML("seque = function(seq) {
alert(seq);
}")
ui <- fluidPage(tags$head(tags$script(js)),
tags$a(hreg = "#", onclick = "seque('test message')",
"Click me to send test message"))
server <- function(...) {}
shinyApp(ui, server)
Don't get me wrong shinyjs: has its merits, but the typical use case is that you want to trigger JavaScript code from the R side like in this example:
library(shiny)
library(shinyjs)
js_code <- HTML("shinyjs.seque = function(par) {
var def_par = {seq: 'test message'};
par = shinyjs.getParams(par, def_par);
alert(par.seq);
}")
ui <- fluidPage(useShinyjs(),
extendShinyjs(text = js_code, functions = "seque"),
actionButton("click", "Click me to send message"))
server <- function(input, output, session) {
observeEvent(input$click, {
js$seque("test message from R")
})
}
shinyApp(ui, server)
In this example I use shinyjs to call JS directly from R, while in the previous example the JS is called via the onclick.
For your example you could use an actionLink in your table and add an observer and in this you call js$queue but since you will have one link per line this may be tricky (yet not impossible) to code (basically you need dynamic listeners),
Thus, relying on pure JS (onclick) as in your example may be the better option, but then you don't need shinyjs.

hideTab doesn't work when tabsetPanel and hideTab are inside an observer in R shiny

I'm traying to create an app that reads some user and password and then create a tabsetPanel inside a renderUI.
The app is supposed to read a code and type number from a data base and if the type is 1 then hides some tabPanel, however all the tabpanels are always shown.
library(shiny)
library(RPostgreSQL)
con=dbConnect(........)
ui <- fluidPage(
textInput("user","User:"),
passwordInput("password", "Password:"),
actionButton("go", "Go",class = "btn-primary"),
uiOutput("panel")
)
server <- function(input, output, session) {
observeEvent({input$go}, {
code<-dbGetQuery(con,"SELECT type FROM table")[[1]]
#code is a number
if(dim(code)[1]==1){
type=reactive(dbGetQuery(con,"SELECT type FROM table2")[[1]])
#type() is a number
output$panel=renderUI(
tabsetPanel(id = "tab",
tabPanel("Tab1"),
tabPanel("Tab2")
)
)
observe({
if(type()==1){
hideTab(inputId = "tab", target = "Tab1")
}
})
}
})
}
shinyApp(ui, server)
The problem is that de observer is executed before the renderUI and doesn't re-execute, I think.
Generally speaking, you've mixed up 3 different processes.
Checking the user has access can be put in a separate function, outside of the scope of server, simply returning TRUE or FALSE (and possibly an error).
Dynamically loading the tabs. If this must only occur after the user has logged in, you can simply opt to not display Tab1. If the tabs has to be loaded regardless (but still dynamically), put it outside of the scope of observeEvent({input$go}, {...}). Consider, just for now, to setup the tabsetpanel with tabs in the ui.
Showing/hiding the tab.
Within a reactive/observe, you do not need to use additional reactives. They already are set to run. So type should be just be type = dbGetQuery(...), and the observe nested within an observe/observeEvent makes no sense.
Lastly, to debug why the tab is not hidden, use the good ol' fashioned print and look at your console. Try updating to
observe({
cat('Testing type: ', type(), '\n')
if(type()==1){
cat('Hiding tab...\n')
hideTab(inputId = "tab", target = "Tab1")
}
})
and watch out for those messages in your console. Are they printed? Then the fault might be on the client-side (perhaps you mispelled something). Are the messages missing? Then you know the code never executed, and you'll have to investigate why.
Update:
Looking further into the matters, try using the browsers Inspect-function. For the viewer in Rstudio (and Chrome), you can right-click and select "Inspect element". A new window appears (or is docked within the window), which allows you to inspect the HTML DOM and view the JavaScript console. Here, we notice an important message:
Uncaught There is no tabsetPanel (or navbarPage or navlistPanel) with id equal to 'tab'
Simply put, the hideTab command is sent before the client has finished loading the tabpanels.
One solution, that did not work, is as follows:
server <- function(input, output, session) {
type <- reactiveVal(0)
type_delayed <- debounce(type, Inf)
observeEvent({input$go}, {
code<-data.frame(code=1)
#code is a number
if(dim(code)[1]==1){
#type(dbGetQuery(con,"SELECT type FROM table2")[[1]])
type(1)
#type() is a number
output$panel=renderUI(
tabsetPanel(id = "tab",
tabPanel("Tab1"),
tabPanel("Tab2")
)
)
}
})
observe({
cat('Testing type: ', type_delayed(), '\n')
if( type_delayed() ==1){
cat('Hiding tab...\n')
hideTab(inputId = "tab", target = "Tab1")
}
})
}
I.e., we delay the execution of hiding the tab. Except it's a bad solution, because you have to choose a timing that is as soon as possible, but not so soon that the client isn't ready.
I suggest the following solution: Instead of hiding the panel, don't add it until you need it:
ui <- fluidPage(
textInput("user","User:"),
passwordInput("password", "Password:"),
actionButton("go", "Go",class = "btn-primary"),
actionButton("add", "Add tab"),
uiOutput("panel")
)
server <- function(input, output, session) {
i <- 1
observeEvent({input$go}, {
code<-data.frame(code=1)
#code is a number
if(dim(code)[1]==1){
#type <- dbGetQuery(con,"SELECT type FROM table2")[[1]]
type <- 1
#type() is a number
output$panel=renderUI({
if (type == 1) {
i <<- 1
tabsetPanel(
id = "tab",
tabPanel("Tab1")
)
} else {
i <<- 2
tabsetPanel(
id = "tab",
tabPanel("Tab1"),tabPanel("Tab2")
)
}
})
}
})
observeEvent(input$add, {
i <<- i + 1
appendTab('tab', tabPanel(paste0('Tab', i)))
})
}

Shiny input update fails with nested outputs

The following minimal example demonstrates a problem with updating nested outputs followed by updating the inputs with the content they had before the update of the outputs. This minimal example comes from a much larger project, so solutions to the problem like "don't nest the outputs" aren't viable unless Shiny doesn't actually allow nested outputs.
When you run the example, you'll see two radio buttons and a text input. Enter some text and change the radio button. You will see that after the radio button event is handled, the text input has indeed been updated. But if you change the radio button a second time, the text input at the observeEvent() will be blank.
It's like the updateTextInput() function works fine for updating what you see in the browser, but not at all for what Shiny gets back from the browser for that input on the next event.
To add a bit of strange, also note that if you change the updated text input in any way (e.g., add or remove one character to/from it), then it works as expected. Is the problem here my code, my expectations, or a bug in Shiny?
ui <- fluidPage(fluidRow(column(12, tagList(
uiOutput("outer_div")
))))
server <-function(input, output, session) {
updateScreen = function() {
output$outer_div = renderUI(uiOutput("inner_div"))
output$inner_div = renderUI(tagList(
radioButtons(inputId = "button", label="", choices = c("A", "B")),
textInput("text", label="", value="")
))
}
updateScreen() #initialize screen
observeEvent(input$button, {
print(paste0("Text input is ", input$text))
updateTextInput(session, inputId = "text" , value=input$text)
updateRadioButtons(session, inputId="button", selected=input$button)
updateScreen()
})
}
shinyApp(ui = ui, server = server)
Does this work for you?
ui <- fluidPage(fluidRow(column(12, tagList(
uiOutput("outer_div")
))))
server <-function(input, output, session) {
updateScreen = function() {
output$outer_div = renderUI(uiOutput("inner_div"))
output$inner_div = renderUI(tagList(
radioButtons(inputId = "button", label="", choices = c("A", "B")),
textInput("text", label="", value="")
))
}
updateScreen() #initialize screen
data <- isolate(input$text)
observeEvent(input$button, {
updateTextInput(session, inputId = "text" , value=data)
updateRadioButtons(session, inputId="button", selected=input$button)
print(paste0("Text input is ", input$text))
#updateScreen()
})
}
shinyApp(ui = ui, server = server)

Possible to show console messages (written with `message`) in a shiny ui?

I don't understand R's message vs cat vs print vs etc. too deeply, but I'm wondering if it's possible to capture messages and show them in a shiny app?
Example: the following app can capture cat statements (and print statements as well) but not message statements
runApp(shinyApp(
ui = fluidPage(
textOutput("test")
),
server = function(input,output, session) {
output$test <- renderPrint({
cat("test cat")
message("test message")
})
}
))
Cross post from the shiny-discuss Google group since I got 0 answers.
Yihui suggested I use withCallingHandlers, and that indeed let me to a solution. I wasn't quite sure how to use that function in a way that would do exactly what I needed because my problem was that I had a function that printed out several messages one at a time and using a naive approach only printed the last message. Here is the my first attempt (which works if you only have one message to show):
foo <- function() {
message("one")
message("two")
}
runApp(shinyApp(
ui = fluidPage(
actionButton("btn","Click me"),
textOutput("text")
),
server = function(input,output, session) {
observeEvent(input$btn, {
withCallingHandlers(
foo(),
message = function(m) output$text <- renderPrint(m$message)
)
})
}
))
Notice how only two\n gets outputted. So my final solution was to use the html function from shinyjs package (disclaimer: I wrote that package), which lets me change or append to the HTML inside an element. It worked perfectly - now both messages got printed out in real-time.
foo <- function() {
message("one")
Sys.sleep(0.5)
message("two")
}
runApp(shinyApp(
ui = fluidPage(
shinyjs::useShinyjs(),
actionButton("btn","Click me"),
textOutput("text")
),
server = function(input,output, session) {
observeEvent(input$btn, {
withCallingHandlers({
shinyjs::html("text", "")
foo()
},
message = function(m) {
shinyjs::html(id = "text", html = m$message, add = TRUE)
})
})
}
))
I know this isn't nearly as elegant, but I worked around a bit similar problem using capture.output; sadly sink doesn't allow simultaneous capture of messages and output though. You don't get them in the original order, but you can extract both streams at least (here turned to HTML):
runApp(shinyApp(
ui = fluidPage(
uiOutput("test")
),
server = function(input,output, session) {
output$test <- renderUI({
HTML(
paste(capture.output(type = "message", expr = {
message(capture.output(type = "output", expr = {
cat("test cat<br>")
message("test message")
cat("test cat2<br>")
message("test message2")
}))
}), collapse="<br>")
)})
})
)
Output:
test message
test message2
test cat
test cat2
Perhaps in the case if user wants to capture both but also separate them, this will provide a handy work-around. (Your shinyjs package seems neat, need to take a look at it!)
This can now be done with the high-level function shinyCatch from the spsComps package.
Basic usage
library(shiny)
library(spsComps)
ui <- fluidPage(
actionButton("msg", "msg"),
actionButton("warn", "warn"),
actionButton("err", "err"),
)
server <- function(input, output, session) {
observeEvent(input$msg, {
shinyCatch({message("a message")}, prefix = '')
})
observeEvent(input$warn, {
shinyCatch({warning("a warning")}, prefix = '')
})
observeEvent(input$err, {
shinyCatch({stop("an error")}, prefix = '')
})
}
shinyApp(ui, server)
Choose blocking level
If exceptions happened, we can choose to continue the code or block downstream code in the reactive context. For example, we want to stop downstream code if an error/warning/message happens:
library(shiny)
library(spsComps)
ui <- fluidPage(
actionButton("err", "code not blocked after error"),
actionButton("err_block", "code blocked after error"),
)
server <- function(input, output, session) {
observeEvent(input$err, {
shinyCatch({stop("an error")}, prefix = '')
print("error does not block ")
})
observeEvent(input$err_block, {
shinyCatch({stop("an error")}, prefix = '', blocking_level = "error")
print("you can't see me if error happens")
})
}
shinyApp(ui, server)
More advanced use
check website and demo
cat and print
There is still no good method to catch real-time info from cat and print. I will come back to update this answer if I found a fix for this.

Resources