RShiny: Why does htmlOutput prevents JavaScript from working? - javascript

I am builing a RShiny app where I need to create vanilla/plain html buttons and give basic functionality to them using JavaScript. In my original app I have a htmlOutput (or uiOutput) element containing these buttons (they are generated dynamically based on user input). Unfortunately JavaScript is not working properly inside this htmlOutput element and I cant figure out why. Please see my minimal reproducible example (app.R):
library(shiny)
library(shinyjs)
# define ui
ui <- fluidPage(
useShinyjs(),
tags$head(
tags$script(
HTML(
"window.onload = function(){
var coll = document.getElementsByClassName('testclass');
var i;
console.log(coll);
for (i = 0; i < coll.length; i++) {
coll[i].addEventListener('click', function() {
alert('clicked');
});
};
};
")
)
),
mainPanel(
# normal button (working)
tags$button(
type="button",
class="testclass",
"Click to alert (button inside main panel)"
),
# html output button (not working)
htmlOutput("html_out")
)
)
# define server
server <- function(input, output) {
# generate html output button (problematic with JS)
output$html_out <- renderUI({
tags$button(
type="button",
class="testclass",
"Click to alert (button inside htmlOutput)"
)
})
}
# run app
shinyApp(ui = ui, server = server)
The tags$button() element is working without problem if it is added statically into the main panel. But if the same tags$button() element is added via the htmlOutput it is not working with the JavaScript code. Is there any explanation or workaround for that?
The only difference in the html output code is that the htmlOutput element is wrapped inside a div with class = "shiny-html-output shiny-bound-output". And I know that I usually shouls use actionButton() but in my case this is not possible.
Thanks for your help!

The problem is that the initial JS in head is run when the app starts but the second button isn't available immediately. you can add the JS code directly to the HTML code
library(shiny)
library(shinyjs)
# define ui
ui <- fluidPage(
useShinyjs(),
tags$head(
tags$script(
HTML(
"window.onload = function(){
var coll = document.getElementsByClassName('testclass');
var i;
console.log(coll);
for (i = 0; i < coll.length; i++) {
coll[i].addEventListener('click', function() {
alert('clicked');
});
};
};
")
)
),
mainPanel(
# normal button (working)
tags$button(
type="button",
class="testclass",
"Click to alert (button inside main panel)"
),
# html output button (not working)
htmlOutput("html_out")
)
)
# define server
server <- function(input, output) {
# generate html output button (problematic with JS)
output$html_out <- renderUI({
tags$button(
type="button",
class="testclass",
# ADD JS HERE
onclick = "alert('clicked');",
"Click to alert (button inside htmlOutput)"
)
})
}
# run app
shinyApp(ui = ui, server = server)

Related

Shiny plot not rendering in client-side dynamic UI

I'm trying to render some UI elements in Shiny on the client side using Javascript. I want the user to click a button which reveals a hidden panel. Once the panel is revealed, it should show a plot. When the button is clicked the panel becomes visible but the plot doesn't render.
I know this behaviour is achievable using server-side functions like observeEvent but the goal here is to leverage the client directly via JS.
Below is a reprex. For ease, I've inlined the JS and CSS.
library(shiny)
# Inline js to toggle display of element when button is clicked
js <- '
$(document).ready(function() {
document.getElementById("reveal").addEventListener("click", event => {
var panel = document.getElementById("init-hidden-panel");
panel.style.display = "block";
});
})
'
ui <- fluidPage(
tags$script(HTML(js)),
actionButton("reveal", "Reveal"),
div(
id = "init-hidden-panel",
plotOutput("plot"),
style = "display: none; background-color: darkgray" # initialize element as hidden
)
)
server <- function(input, output) {
output$plot <- renderPlot({
hist(iris$Sepal.Length)
})
}
# Run the application
shinyApp(ui = ui, server = server)
We can tell that the JS is in fact toggling the display because the gray background appears.
Perhaps you can use this method:
ui <- fluidPage(
actionButton("reveal", "Reveal"),
plotOutput("plot")
)
server <- function(input, output) {
plot_data <- reactiveValues(trigger = 0)
observe({
req(input$reveal)
isolate(plot_data$trigger <- plot_data$trigger + 1)
})
output$plot <- renderPlot({
input$reveal
if (plot_data$trigger %% 2 == 0) {
return(NULL)
} else {
hist(iris$Sepal.Length)
}
})
}
# Run the application
shinyApp(ui = ui, server = server)

How to use JS to count the number of clicks of a selectInput function?

In running the below code, each click of the actionButton() is correctly counted and output to the UI (using reactive value y). I'm trying to do the same with selectInput() (counting each time a selection is made)(using reactive value x), but it doesn't completely correctly count because when first invoking the App, selecting the default choice of "Cyl" is not included in the count (I'd like it to), and clicking the same choice more than once isn't included in the count (I'd like all clicks counted). One workaround is to include "multiple = TRUE" in the selectInput(), but I'd like to see if there's a JS solution instead so I don't have to change the UI the way including "multiple = TRUE" does.
As an aside, using "multiple = TRUE" also corrects the weird subtraction of 1 from (x) in output$clickSelInput...; hopefully a JS solution does the same.
Code:
library(shiny)
ui = fluidPage(hr(),
selectInput("selInput",label=NULL,c("Cyl"="cyl","Trans"="am","Gears"="gear"),selected=NULL),
actionButton("addBtn","Add"), hr(),
textOutput("clickSelInput"),
textOutput("clickAddBtn"),
tableOutput("data")
)
server = function(input, output) {
x = reactiveVal(0)
y = reactiveVal(0)
output$data <- renderTable({mtcars[1:10, c("mpg", input$selInput), drop = FALSE]})
observeEvent(input$selInput,{x(x()+1)})
observeEvent(input$addBtn,{y(y()+1)})
output$clickSelInput <- renderText({paste('Select Input clicks =',x()-1)})
output$clickAddBtn <- renderText({paste('Add Button clicks =',y())})
}
shinyApp(ui, server)
Here is a working solution, taking SamR's suggestion into account and using that example by analogy. I had to make other changes to make it work. I commented changes below from OP.
library(shiny)
library(shinyjs) # added
ui = fluidPage(hr(),
useShinyjs(), # added, this line is easy to forget to add
uiOutput("selInput"), # added, pulls renderUI output from server into UI
actionButton("addBtn","Add"), hr(),
textOutput("clickSelInput"),
textOutput("clickAddBtn"),
tableOutput("data")
)
server = function(input, output) {
x = reactiveVal(0)
y = reactiveVal(0)
output$data <- renderTable({mtcars[1:10, c("mpg", input$selInput), drop = FALSE]})
observeEvent(input$addBtn,{y(y()+1)})
# moved select input into renderUI so it can interact with JS in the observe further down:
output$selInput <- renderUI(
selectInput("selInput",label=NULL,c("Cyl"="cyl","Trans"="am","Gears"="gear"),selected=NULL)
)
output$clickSelInput <- renderText({paste('Select Input clicks =',input$rnd)}) # changed
output$clickAddBtn <- renderText({paste('Add Button clicks =',y())})
# added js:
observe({
if(is.null(input$rnd)){
runjs("
var click = 0;
Shiny.onInputChange('rnd', click)
var selInput = document.getElementById('selInput')
selInput.onclick = function() {click += 1; Shiny.onInputChange('rnd', click)};
")
}
})
}
shinyApp(ui, server)

shiny doesn't work after, containing javascript

I have a problem with this application, containing a javascript file for the user-interface part, which enables more tabs. However, the server part doesn't work when we have included the javascript file. I have a simple reactivity regarding the mtcars dataset here to showcase the problem. when I disable the "includeScript("script.js")", by merely put the # in front of it, the app works, so the problem is connected to this part. So my question would be, How can I fix this issue and also keep the javascript part in the shiny app.
Grateful for all your help.
Edit:
Now I have replaced includeScript("script.js") with tags$head(tags$script(src="./script.js")), and it appears to work, but with an extremely slow reactivity, I have to wait almost 1-2 min before seeing something. Any suggestion, or do you also experience this ?
library(shiny)
library(shinythemes)
library(shinymanager)
library(dplyr)
script.js
$(document).ready(function(){
$('.dropdown').on('click', function(e){
$(this).toggleClass('open');
e.stopPropagation();
e.preventDefault();
});
$('[data-toggle=tab]').on('click', function(e){
let dv = ($(this).attr('data-value'));
//Set active element in tabcontents
$('.tab-pane').removeClass('active');
$('.tab-pane[data-value="' + dv + '"]').addClass('active');
//Set active element in navbar
$('a[data-toggle=tab]').parent().removeClass('active');
$('a[data-value="' + dv + '"]').parent().addClass("active");
//Close the dropdowns
$('.dropdown').removeClass('open');
e.stopPropagation();
e.preventDefault();
});
});
Credentials
credentials <- data.frame(
user = c("Jhon", "Erik"), # mandatory
password = c("1", "1"), # mandatory
start = c("2022-02-14"), # optinal (all others)
expire = c(NA, "2022-12-31"),
admin = c(TRUE, TRUE),
comment = "Model Performance application",
stringsAsFactors = FALSE
)
Ui
ui <- fluidPage(
includeScript("script.js"),
navbarPage("Shiny",
collapsible = TRUE,
theme = shinytheme('yeti'),
tabPanel("Information" ,icon = icon("info"),
tags$h2("Information about the current user"),
verbatimTextOutput("auth_output")
),
tabPanel("Simulation 1",
tags$h2("Simulation"),
tags$hr(),
selectInput("vars", "Variables", names(mtcars), multiple = T),
tableOutput("data")
),
tabPanel("Upload",icon = icon("upload"),
tags$h2("Upload datasets"),
tags$hr(),
),
tabPanel("Simulation 2",
tags$h2("Simulation"),
tags$hr()
),
navbarMenu("Statistical outputs",
tabPanel("One"
),
tabPanel("Two"
),
tabPanel("Three"
),
tabPanel("Four"
),
tabPanel("Report"
),
navbarMenu("More",
tabPanel("Statistical", icon = icon("info")
),
tabPanel("Info",
icon = icon("info-circle")
),
tabPanel("Subpart 4", "Subpart 4"),
tabPanel("Subpart 5", "Subpart 5")
)
)
)
)
Wrap your UI with secure_app
ui <- secure_app(ui)
Server
server <- function(input, output, session) {
# call the server part
# check_credentials returns a function to authenticate users
res_auth <- secure_server(
check_credentials = check_credentials(credentials)
)
output$auth_output <- renderPrint({
reactiveValuesToList(res_auth)
})
output$data <-renderTable({
req(input$vars)
mtcars %>% select(all_of(input$vars))
})
}
shiny::shinyApp(ui, server)
Update:
Most important in addition to 1. and 2. from the first answer. The app works as desired only if split in ui and server part!
It seems that the server part is not working but after clicking on Statistical outputs the table appears!
First answer:
Put your script.js into a www folder. This should be in the same folder where your app is.
Change includeScript("script.js"), in ui part with tags$head(tags$script(src="script.js")),

Extract the duration of hosted audio files in R (without downloading the file)

I have a list of hosted audio files for which I would like to get the duration of, without having to download the files themselves.
I can do this by creating a html DOM element <audio> and extracting the duration with the duration attribute. This works nicely in the following shinyapp: When clicking on the button "Click me", the duration is returned in an alert.
shinyApp(
ui = fluidPage(
useShinyjs(),
actionButton("btn", "Click me"),
tags$audio(id = "myaudio",
src = "https://download.samplelib.com/mp3/sample-3s.mp3",
type = "audio/mp3", autoplay = NA, controls = NA)
),
server = function(input, output) {
observeEvent(input$btn, {
runjs("alert(myaudio.duration);")
})
}
)
This is the point where I am stuck: How can I leverage this method to extract the duration of multiple (> 1'000) mp3s?
How can I replace the src= and iterate of a list of mp3s?
How can I write out the duration so that I can further process this in R?
This answer only works for one audio file
To get the duration of the audio and print it, we need to use js, an environment in shinyjs as follows:
Define get_duration as our JS and specify what happens when the input changes via Shiny.onInputChange. Here I have also used reactiveValues to store durations on change.
library(shinyjs)
library(shiny)
get_duration <- 'shinyjs.aud_duration = function(params) {
var duration = myaudio.duration;
Shiny.onInputChange("aud_duration", duration);
}'
ui <- fluidPage(
useShinyjs(),
extendShinyjs(text = get_duration, functions = "aud_duration"),
actionButton("btn", "Click me"),
tags$audio(id = "myaudio",
src = "https://download.samplelib.com/mp3/sample-3s.mp3",
type = "audio/mp3", autoplay = NA, controls = NA),
verbatimTextOutput("aud_duration")
)
server <- function(input, output) {
js$aud_duration()
durations <- reactiveValues(duration = NA)
observeEvent(input$btn,
durations$duration <- input$aud_duration
)
output$aud_duration <- renderText(durations$duration)
}
shinyApp(ui, server)
Thanks to #NelsonGon's answer, which solved part of the problem, I was able to develop a solution to my problem which scales and thus works on multiple urls. It solves the following problems:
it does not wait for a user to click a button but tries to get the duration every second until it succeeds (since it takes a while for the metadata to get downloaded)
the shiny app is wrapped into a function, so that the url of the source can be input
upon success, the duration value is returned
get_duration <- function(src){
library(shinyjs)
library(shiny)
get_duration <- 'shinyjs.aud_duration = function(params) {
var duration = myaudio.duration;
Shiny.onInputChange("aud_duration", duration);
}'
ui <- fluidPage(
useShinyjs(),
extendShinyjs(text = get_duration, functions = "aud_duration"),
tags$audio(id = "myaudio",
src = src),
)
server <- function(input, output) {
js$aud_duration()
durations <- reactiveValues(duration = NA)
observe({
invalidateLater(1000)
if(!is.null(isolate(input$aud_duration))){
stopApp(input$aud_duration)
}
})
}
shiny::runGadget(ui, server)
}
it an be run as follows:
get_duration("https://download.samplelib.com/mp3/sample-3s.mp3")
[1] 3.239184

Trigger a reaction after change of textOutput() in shiny

I am trying to use JohnCoene/marker package to highlight sections of text in a shiny app. My intend is to first generate the text using some server logic and display it with textOutput. However, I am struggeling with how to trigger the marker after the text appeared on the website. Putting it in the same observeEvent() does not work.
Here is my reprex
# remotes::install_github("johncoene/marker")
library(shiny)
library(marker)
ui <- fluidPage(
use_marker(),
actionButton("click", "click"),
textOutput("text_to_mark")
)
server <- function(input, output) {
observeEvent(input$click,
{
output$text <- renderText("My house is yellow")
})
# observeEvent() below does not work. This is just for illustration
observeEvent(input$text_to_mark,
{
marker <- marker$new("#text_to_mark.shiny-text-output.shiny-bound-output")
marker$
unmark()$ # unmark all before we mark
mark("My house")
})
}
# Run the application
shinyApp(ui = ui, server = server)
Created on 2019-10-10 by the reprex package (v0.3.0)
For illustration: I can get the marker to work, by adding a second button as in the code below, but I am look for a solution where it gets triggered when the text appears.
# remotes::install_github("johncoene/marker")
library(shiny)
library(marker)
ui <- fluidPage(
use_marker(),
actionButton("click", "click"),
textOutput("text_to_mark"),
actionButton("mark", "Mark!")
)
server <- function(input, output) {
observeEvent(input$click,
{
output$text_to_mark <- renderText("My house is yellow")
})
observeEvent(input$mark,
{
marker <- marker$new("#text_to_mark.shiny-text-output.shiny-bound-output")
marker$
unmark()$ # unmark all before we mark
mark("My house")
})
}
# Run the application
shinyApp(ui = ui, server = server)
Created on 2019-10-10 by the reprex package (v0.3.0)
You could listen on DOM changes with javascript: Is there a JavaScript / jQuery DOM change listener?.
When a change happens you can check if your target element has text:
hasText = document.getElementById("text_to_mark").innerHTML != ""
Note that i assume that your element has the id "text_to_mark".
The result you can "send to R" with
Shiny.onInputChange("hasText", hasText);
On the R side you will know if the element has text via listening on input$hasText.
So you can add:
observeEvent(input$hasText,{
...
})
The javascript you can add to your app with tags$script(jsCode) or use shinyjs.
A reproducible example:
library(shiny)
library(marker)
jsCode <- '
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var observer = new MutationObserver(function(mutations, observer) {
console.log(mutations, observer);
hasText = document.getElementById("text_to_mark").innerHTML != ""
Shiny.onInputChange("hasText", hasText);
});
observer.observe(document, {
subtree: true,
attributes: true
});
'
ui <- fluidPage(
use_marker(),
tags$script(jsCode),
actionButton("click", "click"),
textOutput("text_to_mark"),
actionButton("mark", "Mark!")
)
server <- function(input, output) {
observeEvent(input$click, {
output$text_to_mark <- renderText("My house is yellow")
})
observeEvent(input$hasText,{
marker <- marker$new("#text_to_mark.shiny-text-output.shiny-bound-output")
marker$
unmark()$ # unmark all before we mark
mark("My house")
})
}
# Run the application
shinyApp(ui = ui, server = server)
Note that this only works on the first appearance of the text. If you also want to listen for changes of the text, one could send the text to R instead and check on the R side if the text was updated. Not sure if it is needed here.
Listening on DOM changes is one option, but your approach already shows that there is a pure shiny (non-custom-JS) solution, it only takes one click more, so the question is how to do it with only one click. I suggest using invalidateLater and wrap it in an if statement to prevent it from running over and over again like seen here.
The trick is to run your marker calls in an observe statement. Include the invalidateLater here and wrap it in an if condition with a counter which counts how many times the statement has been executed. Play around with the number of milliseconds and counts, in my case it works fine with if(isolate(val$cnt) < 1) and invalidateLater(1000). Don't forget to wrap your counter in an isolate otherwise it will get stuck in a loop.
Note also that input$click not only writes the text into a reactiveValue but also resets the counter val$cnt to 0 so that you can use the invalidateLater again on a new text. The same procedure will help you if you want to update your text using an observeEvent or the like. Just make sure to also reset the counter to 0 and the highlighting works on your new text part.
# remotes::install_github("johncoene/marker")
library(shiny)
library(marker)
ui <- fluidPage(
use_marker(),
actionButton("click", "click"),
textOutput("text_to_mark")
)
server <- function(input, output) {
val <- reactiveValues(cnt = 0,
text = NULL)
observeEvent(input$click, {
val$text <- "My house is yellow"
val$cnt <- 0
})
observe({
if(isolate(val$cnt) < 1) {
invalidateLater(1000)
}
marker <- marker$new("#text_to_mark.shiny-text-output.shiny-bound-output")
marker$
unmark()$ # unmark all before we mark
mark("My house")
val$cnt = isolate(val$cnt) + 1
})
output$text_to_mark <-renderText({
val$text
})
}
# Run the application
shinyApp(ui = ui, server = server)

Categories

Resources