Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the best way to write custom JavaScript for R Shiny Module that uses module's namespace? [closed]

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)

Update

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');
    });
}
like image 640
Mikolaj Avatar asked Feb 22 '18 14:02

Mikolaj


1 Answers

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.

like image 184
lkq Avatar answered Oct 13 '22 02:10

lkq