I have a complex Shiny app that needs to use custom JavaScript code. The app is made of modules that are called in multiple places with different namespaces. I need some pieces of JavaScript code to be "modularized" along with the R code, that is to use module namespaces. I was able to make it work by creating a customized string containing JS code and executing it with shinyjs::runjs()
function (example below). For given example this is a fair solution. However, putting more complex over hundred line JavaScript code into a string that is pasted with the identifiers seem to be very error prone and suboptimal solution (lack of highlighting, painful formatting etc.). Is there a better way to achieve the same effect?
library(shiny)
library(shinyJS)
myModuleUI <- function(id) {
ns <- NS(id)
tagList(
div(id = ns("clickableElement"), class = "btn btn-primary", "Click Me"),
div(id = ns("highlightableElement"), "This (and only this!) text should be highlighted on click")
)
}
myModule <- function(input, output, session) {
ns <- session$ns
shinyjs::runjs(paste0("
$('#", ns("clickableElement"), "').click(function() {
$('#", ns("highlightableElement"), "').css('background', 'yellow');
})
"))
}
ui <- fluidPage(
useShinyjs(),
tabsetPanel(
tabPanel(
"Instance 1",
myModuleUI("one")
),
tabPanel(
"Instance 2",
myModuleUI("two")
)
)
)
server <- function(input, output) {
callModule(myModule, "one")
callModule(myModule, "two")
}
shinyApp(ui = ui, server = server)
For future reference I decided to share the solution that I finally implemented. I ended up creating a single JS file per module containing one function taking the namespace as the only argument. This function creates all the needed objects and bindings using this namespace. I then invoke that single function using shinyjs
on the beginning of the module. This allows me to keep JS code in a separate file which solves the initial problems and keeps the code easily manageable (especially if you have a lot of JS code).
app.R
library(shiny)
library(shinyjs)
myModuleUI <- function(id) {
ns <- NS(id)
tagList(
div(id = ns("clickableElement"), class = "btn btn-primary", "Click Me"),
div(id = ns("highlightableElement"), "This (and only this!) text should be highlighted on click")
)
}
myModule <- function(input, output, session) {
ns <- session$ns
shinyjs::runjs(paste0("myModuleJS('", ns(""), "');"))
}
ui <- fluidPage(
useShinyjs(),
tags$head(
tags$script(src = "myModuleJS.js")
),
tabsetPanel(
tabPanel(
"Instance 1",
myModuleUI("one")
),
tabPanel(
"Instance 2",
myModuleUI("two")
)
)
)
server <- function(input, output) {
callModule(myModule, "one")
callModule(myModule, "two")
}
shinyApp(ui = ui, server = server)
www/myModuleJS.js
function myModuleJS(ns) {
$('#' + ns + 'clickableElement').click(function() {
$('#' + ns + 'highlightableElement').css('background', 'yellow');
});
}
I think there are two ways to address this although they are not very elegant solutions.
First solution would be to define a couple of global functions, such as getJavascriptSelector <- function(id, session){paste0("'#", session$ns(id), "'")}
. This way you still need to call this function every time when you use a selector, and you can't define the js inside a separate file.
Since the javascript you are running are all strings, another solution is you can define a function say wrapJavascriptWithNamespace(script, ns)
which takes the script string(or a js file) and the ns object. The function will use regexp to match and replace the selectors with the session-namespaced ones. This way you will be able to re-use your javascript code as well but needs more work and has potential issues like what if this code needs to refer a global scope element(this can be solved by marking your javascript with some keywords to make it a template). Say if you put the code you used in the example in a string:
$('#[shiny-namespace]clickableElement').click(function() {
$('#[shiny-namespace]highlightableElement').css('background', 'yellow');
})
You can easily parse this string and replace shiny-namespace
with ns(). When you need this string to be regular javascrip vs a template, you just need to write another function to strip out the [shiny-namespace]
tags.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With