4

I know this is pretty close to previously aked questions, but after thorough study of those examples I haven't found a solution for my particular problemm yet.

I have a shiny App using Shiny Dashboard with this structure (*1). I can make a next or previous page button this way:

next_btn        <-    actionButton(   inputId ="Next1", 
                                      label = icon("arrow-right"))

with an observer :

  observeEvent(input$Next1, {
    updateTabItems(session, "tabs", "NAME")
  })               

where NAME is the tabItem ID. This version is simpler than the expamples I've found that use switch and or simply Navigate to particular sidebar menu item in ShinyDashboard?

However, this only works to switch from pagename1 to pagename2 with a specific button for it.

I have however, 10-20 tabItems in my app : ** <<- the reason for my problem**

The approach mentioned about would require me to write a actionbutton(next1, ... ac but next 2 , next 3 etc. 1 for each page, and also an separate observer for each.

What I am trying to make is this:

1 generic action button called "NEXTPAGE" with an observer that does updateTabItems(session, tabs, "current page +1"

to to the current page +1 in whatever way I'm lost. I could imagine making a list parameter of all tab names, find the current tabname in that list, grab it's position, shift one position up (previous), or down (next) for example. However, I do not know how to get a list variable of all tabItems present in my app, other than some very laborious manual typing of a list of strings.

*1 app structure:

library(shiny)
library(shinydashboard)

### create general button here like: 
### write a function that looks at what (nth) tabItem we are, and creates a ###  uiOutput for a next_n button (I can do this myself I think) 

dashboardHeader(title = "FLOW C.A.R.S."),
  dashboardSidebar(
    sidebarMenu(id = "tabs",
                menuItem("Home", tabName = "Home", icon = icon("home")),
                menuItem("My Page", tabName = "MyPage", icon =icon("download")),
                menuItem("Do math", tabName = "Math", icon=icon("folder-open")),
                menuItem("Results of something", tabName="Results", icon= 
 icon("file-text-o")),
              menuItem("Short Manual", tabName = "Manual", icon = icon("book"))
                )
    ),

  dashboardBody(
   tabItems(
    tabItem(tabName = "Home",  class = 'rightAlign',
    actionButton(   inputId ="Next1", label = icon("arrow-right"))),

    tabItem(tabName = "MyPage",  class = 'rightAlign',
    actionButton(   inputId ="Next2", label = icon("arrow-right")),
    actionButton(   inputId ="Previous2", label =  icon("arrow-left"))), 

    tabItem(tabName = "Math",  class = 'rightAlign',
    actionButton(   inputId ="Next3", label = icon("arrow-right")),
    actionButton(   inputId ="Previous3", label =  icon("arrow-left"))), 

    tabItem(tabName = "tabName",  class = 'rightAlign',
    actionButton(   inputId ="Next4", label = icon("arrow-right")),
    actionButton(   inputId ="Previous4", label =  icon("arrow-left"))), 

    tabItem(tabName = "Maual",  class = 'rightAlign',
    actionButton(   inputId ="Previous5", label =  icon("arrow-left")))
    ))


server: 

shinyServer = function(input, output, session) {


  observeEvent(input$Next1, {
    updateTabItems(session, "tabs", "MyPage)
  })

observeEvent(input$Previous2, {
    updateTabItems(session, "tabs", "Home")
  })

observeEvent(input$Next2, {
    updateTabItems(session, "tabs", "Math)
  })

 ### repeat for next2 and previous 2 , 3 etc 

}

Summary, I'm looking for a code that will give us the name of the Tab coming after of before the current tab, so that we can stuff the outcome of that query into updateTabItems(session, "tabs" .......)

so that we can make a more general observer that says for instance;

if Next[i] button is clicked go to tabItem[i+1]

but like I said, I can imagine myself writing such a code, if only if I knew how to access the list of tabItems with function (obviously I have the names in the ui page since I labelled all of them, but I'm trying to avoid all the redunant repetition of code by typing it all out for each page/button/observer)

only thing I discoverd so far is that paste(input$tabs) inside an observer will give you the current tab, but then what...

thanks for anny help!

If it's unclear, please feel free to contact me

Jonas
  • 121,568
  • 97
  • 310
  • 388
Mark
  • 2,789
  • 1
  • 26
  • 66
  • The easiest would be for sure to rewrite the code and have an array: `tabItemNames = c("Home", "MyPage",....)` and then name the tabs accordingly `tabItem(tabName = tabItemNames[1],...`, `tabItem(tabName = tabItemNames[2],...` etc. That i wouldnt call redundant repition of code,... – Tonio Liebrand Jun 01 '17 at 13:56
  • Nice suggestion! Still there must be a way I feel to retrieve the tab names once they exist. I mean practically everything has in input, output or value handle somehow right.. Besides the redundancy, more important was the possibility of errors if you write the names and list of names manually and mess up the order by later changing something. This suggestion of yours would solve that – Mark Jun 01 '17 at 14:52
  • well its a lot of effort for a small enhancement, but i think i found a way,...i will post it soon. – Tonio Liebrand Jun 01 '17 at 15:01
  • pls format the question and if possible next time provide a full working skeleton one can start with =) – Tonio Liebrand Jun 01 '17 at 15:27

2 Answers2

5

I will admit that this is not fully generalized. It requires that you place a vector in your server that has the names of the tabs from the UI. But, you really only need two buttons to make it work (not two buttons per tab). You only need to make sure that the tab_id vector has the correct names in the same order as the UI. You can probably get away with something like this if it is a small scale project where the tabs and tab names are not changing a lot.

library(shiny)
library(shinydashboard)
library(shinyjs)

### create general button here like: 
### write a function that looks at what (nth) tabItem we are, and creates a ###  uiOutput for a next_n button (I can do this myself I think) 

shinyApp(
  ui = 
    dashboardPage(
      dashboardHeader(title = "FLOW C.A.R.S."),
      dashboardSidebar(
        useShinyjs(),
        sidebarMenu(id = "tabs",
                    menuItem("Home", tabName = "Home", icon = icon("home")),
                    menuItem("My Page", tabName = "MyPage", icon =icon("download")),
                    menuItem("Do math", tabName = "Math", icon=icon("folder-open")),
                    menuItem("Results of something", tabName="Results", icon= 
                               icon("file-text-o")),
                    menuItem("Short Manual", tabName = "Manual", icon = icon("book"))
        )
      ),

      dashboardBody(
        hidden(actionButton(inputId ="Previous", label = icon("arrow-left"))),
        hidden(actionButton(inputId ="Next", label = icon("arrow-right")))
      )
    ),

  server = 
    shinyServer(function(input, output, session){

      tab_id <- c("MyPage", "Math", "Results", "Manual")

      observe({
        lapply(c("Next", "Previous"),
               toggle,
               condition = input[["tabs"]] != "Home")
      })

      Current <- reactiveValues(
        Tab = "Home"
      )

      observeEvent(
        input[["tabs"]],
        {
          Current$Tab <- input[["tabs"]]
        }
      )

      observeEvent(
        input[["Previous"]],
        {
          tab_id_position <- match(Current$Tab, tab_id) - 1
          if (tab_id_position == 0) tab_id_position <- length(tab_id)
          Current$Tab <- tab_id[tab_id_position]
          updateTabItems(session, "tabs", tab_id[tab_id_position]) 
        }
      )

      observeEvent(
        input[["Next"]],
        {
          tab_id_position <- match(Current$Tab, tab_id) + 1
          if (tab_id_position > length(tab_id)) tab_id_position <- 1
          Current$Tab <- tab_id[tab_id_position]
          updateTabItems(session, "tabs", tab_id[tab_id_position]) 
        }
      )
    })
)
Benjamin
  • 16,897
  • 6
  • 45
  • 65
  • I see you understood my question perfectly. Elegant way to do it! Similar to what I had in mind, but simpler, nice. Thanks a lot Benjamin. I will upvote, and accept it soon, still hoping to find a repons for the interactive extraction of the existing tabItem handles / tags – Mark Jun 01 '17 at 14:54
  • Actually never considered to put stuff in the dashboardBody itself because I was building pages 1 at a time until now. It raises one question though, is it possible to hide this from some pages? My start up page should not have these buttons for instance – Mark Jun 01 '17 at 15:04
  • 1
    Made some edits. I used `shinyjs` to hide the buttons when the "Home" tab first loads. Then I use observers to reinstate them when necessary. – Benjamin Jun 01 '17 at 15:12
  • i used your code as the foundation for the "dynamic" solution if thats fine for you? – Tonio Liebrand Jun 01 '17 at 15:28
  • i was asking more in the direction of Benjamin ;) – Tonio Liebrand Jun 01 '17 at 15:39
  • You better take Mark's answer. I'm horribly offended ;) – Benjamin Jun 01 '17 at 15:48
  • haha. Guys, great work from both of you! I can only accept 1 answer, so it goes to Big's. Upvoted both though. I'll study it a bit more so I actually learn from this. Grazie mille ! – Mark Jun 01 '17 at 18:55
  • I thought you actually meant the dynamic code from my previous question you were involved in last month BDS. hence the confusion – Mark Jun 01 '17 at 18:56
  • @Benjamin how to create the function of back and next button as a shiny module ? – user5249203 Mar 17 '22 at 22:29
  • 1
    @user5249203, I'm afraid I'm not sure. I've never quite grasped the full utility of modules, and would likely benefit from doing more study there myself. That's unlikely to happen in the near future, though. I'd suggest making a new question linking to this one to draw attention to this aspect of it. – Benjamin Mar 21 '22 at 11:36
3

As i wrote in the comment: The easiest would be for sure to rewrite the code and have an array: tabItemNames = c("Home", "MyPage",....) and then name the tabs accordingly tabItem(tabName = tabItemNames[1],...), tabItem(tabName = tabItemNames[2],... etc. That i wouldnt call redundant repition of code,...(see also Benjamin´s answer.

However, I appreciated the JS challenge and gave it a shot: You could use JS to read the tabItemNames. That would fulfill the bonus requirement of not having to hardcode them in the code.

  observe({
    runjs("
      function getAllElementsWithAttribute(attribute){
         var matchingElements = [];
         var allElements = document.getElementsByTagName('*');
         for (var i = 0, n = allElements.length; i < n; i++){
            if (allElements[i].getAttribute(attribute) !== null){
               matchingElements.push(allElements[i]);
            }
         }
         return matchingElements;
      };

      ahref = getAllElementsWithAttribute('data-toggle');
      var tabNames = [];
      var tabName = '';
      for (var nr = 0, n = ahref.length; nr < n; nr++){
         tabName = ahref[nr].hash.split('-')[2]
         if(tabName != 'Toggle navigation') tabNames.push(tabName)
      }
      Shiny.onInputChange('tabNames', tabNames);
      ")
  })

The assumption i make that you do not have any further element having a 'data-toggle' attribute. If this would not be fulfilled one would have to integrate further conditions in the code.

In the following a running example, build by the code above combined with the code provided by Benjamin:

library(shiny)
library(shinydashboard)
library(shinyjs)

app <- shinyApp(
  ui = 
    dashboardPage(
      dashboardHeader(title = "FLOW C.A.R.S."),
      dashboardSidebar(
        useShinyjs(),
        sidebarMenu(id = "tabs",
                    menuItem("Home", tabName = "Home", icon = icon("home")),
                    menuItem("My Page", tabName = "MyPage", icon =icon("download")),
                    menuItem("Do math", tabName = "Math", icon=icon("folder-open")),
                    menuItem("Results of something", tabName="Results", icon= 
                               icon("file-text-o")),
                    menuItem("Short Manual", tabName = "Manual", icon = icon("book"))
        )
      ),

      dashboardBody(
        actionButton(inputId ="Previous", label = icon("arrow-left")),
        actionButton(inputId ="Next", label = icon("arrow-right"))
      )
    ),

  server = 
    shinyServer(function(input, output, session){
      global <- reactiveValues(tab_id = "")
      tab_id <- c("Home", "MyPage", "Math", "Results", "Manual")

      Current <- reactiveValues(
        Tab = "Home"
      )

      observeEvent(
        input[["tabs"]],
        {
          Current$Tab <- input[["tabs"]]
        }
      )

      observeEvent(
        input[["Previous"]],
        {
          tab_id_position <- match(Current$Tab, input$tabNames) - 1
          if (tab_id_position == 0) tab_id_position <- length(input$tabNames)
          Current$Tab <- input$tabNames[tab_id_position]
          updateTabItems(session, "tabs", input$tabNames[tab_id_position]) 
        }
      )

      observeEvent(
        input[["Next"]],
        {
          tab_id_position <- match(Current$Tab, input$tabNames) + 1
          if (tab_id_position > length(input$tabNames)) tab_id_position <- 1
          Current$Tab <- input$tabNames[tab_id_position]
          updateTabItems(session, "tabs", input$tabNames[tab_id_position]) 
        }
      )

      observe({
        runjs("
          function getAllElementsWithAttribute(attribute){
             var matchingElements = [];
             var allElements = document.getElementsByTagName('*');
             for (var i = 0, n = allElements.length; i < n; i++){
                if (allElements[i].getAttribute(attribute) !== null){
                   matchingElements.push(allElements[i]);
                }
             }
             return matchingElements;
          };

          ahref = getAllElementsWithAttribute('data-toggle');
          var tabNames = [];
          var tabName = '';
          for (var nr = 0, n = ahref.length; nr < n; nr++){
             tabName = ahref[nr].hash.split('-')[2]
             if(tabName != 'Toggle navigation') tabNames.push(tabName)
          }
          Shiny.onInputChange('tabNames', tabNames);
          ")
      })


    })
)

runApp(app, launch.browser = TRUE)

The javascript function to read the elements I used from here: Get elements by attribute when querySelectorAll is not available without using libraries?

Tonio Liebrand
  • 17,189
  • 4
  • 39
  • 59