Search code examples
javascriptrshinyattributesshinyjs

Hot to use shinyjs to get the id of a clickable <div> element?


I'm making a ShinyApp that uses plain HTML to order clickable <div> sub-elements inside another <div> element.

I want the click event on the sub-elements to trigger a reactiveVal() in my server logic. I could do so by using shinyjs::onclick("<div>.id", reactiveVal(id)), but I would appreciate a better way of using the .id attribute of my sub<div> to directly modify my reactiveVal(), hopefully saving my from writting 118 onclick()s...

Below is the MWE of what I tried so far:

library(shiny)
library(shinyjs)

ui <- fluidPage(
  useShinyjs(),
  fluidRow(
    column(
      2, offset = 1, h3('List of elements:'),
      HTML(
        '<div class = "periodic-table">
            <div class = "element" style = "cursor: pointer;" id = "Hydrogen"> Hydrogen </div>
            <div class = "element" style = "cursor: pointer;" id = "Helium"> Helium </div>
            <div class = "element" style = "cursor: pointer;" id = "Lithium"> Lithium </div>
            ... <br> (115 more chemical elements)
         </div>'
      )
    ),
    column(2, h3('Selected element:'), textOutput('SelectedElem'))
  )
)

server <- function(input, output, session, devMode = TRUE) {
  SelectedElem <- reactiveVal()
  
  onclick("Hydrogen", SelectedElem("Hydrogen"))
  onclick("Helium", SelectedElem("Helium"))
  onclick("Lithium", SelectedElem("Lithium"))
  
  output$SelectedElem <- renderText(SelectedElem())
}

shinyApp(ui = ui, server = server, enableBookmarking = "URL")

Desired behaviour:

GIF of the working app


Solution

  • Essentially, what you want to do is add an event listener to each of these divs that will update a common input. This can be accomplished with the javascrip function Shiny.onInputChange(documentation).

    There are multiple ways to add the event listener but this is one approach. In a separate file (in this case I'll use "www/sendID.js"):

    $(document).ready(function() {
      const elements = document.querySelectorAll('.element')
      
      elements.forEach(element => {
        element.addEventListener('click', () => Shiny.onInputChange("selected", element.id))
      })
      
    })
    

    Then update your UI to include this script:

    ui <- fluidPage(
      useShinyjs(),
      fluidRow(
        column(
          2, offset = 1, h3('List of elements:'),
          HTML(
            '<div class = "periodic-table">
                <div class = "element" style = "cursor: pointer;" id = "Hydrogen"> Hydrogen </div>
                <div class = "element" style = "cursor: pointer;" id = "Helium"> Helium </div>
                <div class = "element" style = "cursor: pointer;" id = "Lithium"> Lithium </div>
                ... <br> (115 more chemical elements)
             </div>'
          )
        ),
        column(2, h3('Selected element:'), textOutput('SelectedElem'))
      ),
      includeScript("www/sendID.js")
    )
    

    Then in the server you will only need to listen for a single input

    server <- function(input, output, session, devMode = TRUE) {  
      output$SelectedElem <- renderText(input$selected)
    }