I'm writing a shiny app in which I want the user to be able to add and remove tabs. I know how to add tabs if the user clicks on a tab specifically made for that (thanks to the answer here) but I can't figure out how to remove the lastly created tab.
Here's a reproducible example:
library(shiny)
library(shinyWidgets)
ui <- navbarPage(position = "static-top",
title = "foo",
id = "tabs",
tabPanel(title = "Name 1",
fluidRow()),
tabPanel(title = "More",
icon = icon("plus"),
fluidRow()),
tabPanel(title = "Less",
icon = icon("minus"),
fluidRow())
)
server <- function(input, output) {
count <- reactiveVal(1)
observeEvent(input$tabs, {
if (input$tabs == "More"){
count(count()+1)
id = paste0("Name ", count())
insertTab(inputId = "tabs",
tabPanel(title = id,
fluidRow(column(
width = 12))
), target = "More", position = "before",
select = TRUE)}
if (input$tabs == "Less"){
count(count()+1)
id = paste0("Name ", count())
removeTab(inputId = "tabs",
target = id
)}
})
}
shinyApp(ui = ui, server = server)
Here, you can see that clicking on the tab More adds a tab named Name i with i the number of clicks made on the tab More. However, clicking on the tab Less does nothing.
What I would like is the following:
if the user clicks at least once on More, then clicking on Less removes the last created tab (therefore placed before More)
if the user does not click on More then clicking on Less does nothing
imagine that I click twice on More then there will be two additional tabs named Name 2 and Name 3. Clicking on Less will remove Name 3 but if I click again on More, the additional tab will be again called Name 3 (therefore clicking on Less should not prevent the re-use of the name of the tab removed).
Does anybody know how to do it?
I've done this with adding selectors and having a button remove then reuse the selector indices. To do this using reactive values that count each time the individual button is pressed or logging when an interaction is done.
Give the below code a try:
library(shiny)
library(shinyWidgets)
ui <- navbarPage(position = "static-top",
title = "foo",
id = "tabs",
tabPanel(title = "Name 1",
fluidRow()),
tabPanel(title = "More",
icon = icon("plus"),
fluidRow()),
tabPanel(title = "Less",
icon = icon("minus"),
fluidRow())
)
server <- function(input, output) {
count <- reactiveValues(value = 1)
observeEvent(input$tabs, {
if (input$tabs == "More"){
count$value <- count$value + 1
id = paste0("Name ", count$value)
insertTab(inputId = "tabs",
tabPanel(title = id,
fluidRow(column(
width = 12))
), target = "More", position = "before",
select = TRUE)}
if (input$tabs == "Less"){
print(count$value)
id = paste0("Name ", count$value)
removeTab(inputId = "tabs",
target = id
)
count$value <- count$value -1
}
})
}
shinyApp(ui = ui, server = server)
Related
I am new to shiny and am currently trying to develop my first shinyapp.
This apps contains multiple actionButtons and nested observeEvents statements, which I think are the cause of my problem.
The app should allow the user to add observations of species by clicking on a add button, that updates the UI. Within each observation, more details can be asked, but I only showed the species name in the REPREX below (textinput).
Each observation can be deleted individually via a delete button.
Until here, it works! However, I also want a modal dialog to confirm the deletion when the delete button is clicked. To do this, I used a nested observeEvent and it doesn't seem to work (or maybe only for the first time). What am I doing wrong ?
Thanks in advance to anyone who tries to help me.
library(shiny)
library(random)
ui <- fluidPage(
fluidRow(br(), br(), actionButton("adder",
label = "Add an observation"),
align="center")
)
server <- function(input, output,session) {
rv <- reactiveValues()
rv$GridId_list <- c()
observeEvent(input$adder,{
# create random ID for each added species
GridId <- as.character(randomStrings(1, 10))
# store the new ID
rv$GridId_list <- c(rv$GridId_list,GridId)
# ID for the textinput
SpId <- paste(GridId, "sp", sep="_")
# ID of the button used to remove this species
removeSpeciesId <- paste(GridId,'remover', sep="_")
#Update of the UI
insertUI(
selector = '#adder',
where = "beforeBegin",
ui = tags$div(
id = GridId,
fluidRow(
column(6,
h5("Species name : "),
textInput(SpId,label = NULL)
),
column(6, align = "center",
br(),br(),
actionButton(removeSpeciesId,
label = "Delete")
)
)
)
)
# Remove an observation when the "delete" button is clicked (and after confirmation)
observeEvent(input[[removeSpeciesId]], {
#Confirmation modal
showModal(
modalDialog(
"Are you sure ?",
title = "Delete",
footer = tagList(
actionButton("cancel", "Cancel"),
actionButton("confirm", "Confirm", class = "btn btn-danger")
)
)
)
# Delete observation if user confirms
observeEvent(input$confirm, {
id_to_remove <- substring(removeSpeciesId,1, nchar(removeSpeciesId)-8)
rv$GridId_list <- rv$GridId_list[rv$GridId_list!=id_to_remove]
removeUI(selector = paste("#", id_to_remove, sep = ""))
showNotification("Observation deleted !")
removeModal()
})
# Just remove the modal if user cancels
observeEvent(input$cancel, {
removeModal()
})
})
})
}
shinyApp(ui = ui, server = server, options = list(launch.browser = T))
Referencing dynamic input id's is a pain. I find it best to add a last clicked input identifier to reference. You can add a class to those inputs to just listen to them and not others in your app:
tags$head(tags$script(HTML("$(document).on('click', '.needed', function () {
Shiny.onInputChange('last_btn',this.id);
});")))
That little piece of code will allow you to get an input$last_btn id, that you can use for your event listeners. In this case you don't need to nest your event listeners; it is better to think about the events in sequence and program those reactions. So, with some tweakings in your code, your app now looks like this:
library(shiny)
library(random)
ui <- fluidPage(
tags$head(tags$script(HTML("$(document).on('click', '.needed', function () {
Shiny.onInputChange('last_btn',this.id);
});"))),
fluidRow(br(), br(), actionButton("adder",
label = "Add an observation"),
align="center")
)
server <- function(input, output,session) {
rv <- reactiveValues()
rv$GridId_list <- c()
observeEvent(input$adder,{
# create random ID for each added species
GridId <- as.character(randomStrings(1, 10))
# store the new ID
rv$GridId_list <- c(rv$GridId_list,GridId)
# ID for the textinput
SpId <- paste(GridId, "sp", sep="_")
# ID of the button used to remove this species
removeSpeciesId <- paste(GridId,'remover', sep="_")
#Update of the UI
insertUI(
selector = '#adder',
where = "beforeBegin",
ui = tags$div(
id = GridId,
fluidRow(
column(6,
h5("Species name : "),
textInput(SpId,label = NULL)
),
column(6, align = "center",
br(),br(),
actionButton(removeSpeciesId,
label = "Delete", class="needed")
)
)
)
)
})
# Remove an observation when the "delete" button is clicked (and after confirmation)
observeEvent(input$last_btn, {
observeEvent(input[[input$last_btn]] > 0,{#We want the modal to show when any "remover" id is clicked
#Confirmation modal
showModal(
modalDialog(
"Are you sure ?",
title = "Delete",
footer = tagList(
actionButton("cancel", "Cancel"),
actionButton("confirm", "Confirm", class = "btn btn-danger")
)
)
)
})
}, ignoreNULL = TRUE, ignoreInit = TRUE)
# Delete observation if user confirms
observeEvent(input$confirm, {
#The following selector is for the parent id of the parent id of the last_btn id
removeUI(selector = paste0("div:has(>div:has(>#", input$last_btn, "))"))
showNotification("Observation deleted !")
removeModal()
})
# Just remove the modal if user cancels
observeEvent(input$cancel, {
removeModal()
})
}
shinyApp(ui = ui, server = server, options = list(launch.browser = T))
In my Shiny application, I have the functionality to insert a new line of information, this is done via InsertUI. Then for every added line, the source of that line can be updated via an actionButton. The actionButton leads to a modalDialog, allowing the user to insert the text for the source there.
To prevent multiple lines getting the same source after updating, a moduleServer is used for the part around the modalDialog. That works well, with the only exception that after the first time the modalDialog is used (with inside an action button that updates the text after insertion), the modalDialog closes directly after clicking. Reclicking does give the pop-up again, but is somewhat annoying.
Any suggestions to prevent this?
Example UI and server with this behaviour:
UI.R
library(shiny)
shinyUI(fluidPage(
div(id = "input_add_div_block"),
br(),
# Input for the new line
div(style="display:inline-block", # div inline-block is used to get two input boxes on the same line
textInput(inputId = "input_new_line",
label = "Give description of new line")
),
# Adding a new line
div(style="display:inline-block", # div inline-block is used to get two input boxes on the same line
id = "input_add_div",
actionButton("input_add", "Add new line")
)
))
Server.R
library(shiny)
shinyServer(function(input, output, session) {
data <- reactiveValues(
lines = list()
)
observeEvent(input$input_add, {
current_lines <- length(data$lines)
n <- current_lines + 1
insertUI(selector = "#input_add_div_block",
where = "beforeEnd", # insert after last element
ui = fluidPage(
div(style="display:inline-block"), # div inline-block is used to get two input boxes on the same line
# Adding input field
numericInput(inputId = paste0("input_", n),
label = paste0(input$input_new_line, " - % of previous line"),
value = 10,
min = 0,
max = 100
),
actionButton(inputId = paste0("input_", n,"_source"),
label = "No source")
)
)
observeEvent(input[[paste0("input_", current_lines+1,"_source")]],{
update_source_UI(id = paste0("source",n), data = data, n = n)
update_source_Server(id = paste0("source",n), data = data, n = n, original_session = session)
})
})
})
update_source_UI <- function(id, data, label = "Update UI", n){
ns <- NS(id)
showModal(
modalDialog(
title = "Update source",
textInput(inputId = ns("new_source"), label = "Update the source",
value = "Assumption"),
actionButton(inputId = ns("add_source"),
label = "Update the source"),
"\n",
actionButton(inputId = ns("cancel"),
label = "Cancel"),
easyClose = TRUE,
footer = NULL
)
)
}
# Pop-up to change the source of the funnel line input
update_source_Server <- function(id, data, n, original_session){
moduleServer(
id,
function(input, output, session) {
ns <- session$ns
# When the user clicks "Update the source", the source is updated
observeEvent(input$add_source,{
updateActionButton(session = original_session,
inputId = paste0("input_", n,"_source"),
label = paste0("Source: ", input$new_source))
removeModal()
data$button_status[[1]] <- FALSE
})
# When the user clicks "Cancel", the modal is removed without changes
observeEvent(input$cancel,{
removeModal()
})
}
)
}
A few points:
I've never seen showModule in the UI part, normally you use it in the server; it's interesting that it works nevertheless
at least in your example, n is always 1, therefore you don't have unique ids which leads to problems
I find passing session objects to modules to refer to something defined in the main server quite complicated
I propose to pack everything for one line into a module and then handle the logic there. I think it's easier and you already work with modules.
My take:
library(shiny)
one_line_UI <- function(id, input_new_line){
ns <- NS(id)
tagList(
div(style="display:inline-block"), # div inline-block is used to get two input boxes on the same line
# Adding input field
numericInput(inputId = ns("input_number"),
label = paste0(input_new_line(), " - % of previous line"),
value = 10,
min = 0,
max = 100
),
actionButton(inputId = ns("input_source"),
label = "No source")
)
}
# Pop-up to change the source of the funnel line input
one_line_Server <- function(id, data){
moduleServer(
id,
function(input, output, session) {
ns <- session$ns
observeEvent(input$input_source, {
showModal(
modalDialog(
title = "Update source",
textInput(inputId = ns("new_source"), label = "Update the source",
value = "Assumption"),
actionButton(inputId = ns("add_source"),
label = "Update the source"),
"\n",
actionButton(inputId = ns("cancel"),
label = "Cancel"),
easyClose = TRUE,
footer = NULL
)
)
})
# When the user clicks "Update the source", the source is updated
observeEvent(input$add_source,{
updateActionButton(inputId = "input_source",
label = paste0("Source: ", input$new_source))
removeModal()
data$button_status[[1]] <- FALSE
})
# When the user clicks "Cancel", the modal is removed without changes
observeEvent(input$cancel,{
removeModal()
})
}
)
}
ui <- fluidPage(
div(id = "input_add_div_block"),
br(),
# Input for the new line
div(style="display:inline-block", # div inline-block is used to get two input boxes on the same line
textInput(inputId = "input_new_line",
label = "Give description of new line")
),
# Adding a new line
div(style="display:inline-block", # div inline-block is used to get two input boxes on the same line
id = "input_add_div",
actionButton("input_add", "Add new line")
)
)
server <- function(input, output, session) {
data <- reactiveValues(
lines = list(),
n = 0
)
observeEvent(input$input_add, {
n <- data$n + 1
insertUI(selector = "#input_add_div_block",
where = "beforeEnd", # insert after last element
ui = one_line_UI(id = n,
input_new_line = reactive({input$input_new_line}))
)
one_line_Server(id = n, data = data)
data$n <- n
})
}
shinyApp(ui, server)
If you want more information, you can also check out my tutorial how to dynamically add modules.
In the following example, I use an actionLink() in "Tab1" to switch the display to "Tab2", then to update the text in the textInput() with "iris" and finally to click the button to display the actual data.
For an unknown reason, this switch is executed after the Sys.sleep() which is after. This lead to a non-desired behavior since shinyjs::click() for instance try to trigger a button that does not exist yet and thereby the 'iris' table is not displayed. I would have expected the tab to be switched first, then to get the Sys.sleep().
Can someone explain to me why Shiny is behaving like that and how to perform the expected behavior please?
library(shiny)
library(shinydashboard)
library(shinydashboardPlus) # shinydashboardPlus_0.7.5 // devtools::install_version("shinydashboardPlus", version = "0.7.5", repos = "http://cran.us.r-project.org")
library(shinyjs)
available_data_sets <- c("mtcars", "iris")
ui_welcome <- fluidPage(actionLink(inputId = "switch_tab", label = "Switch tab, update text with 'iris' & click button", icon = icon("hand-point-right")))
ui_search <- fluidPage(
textInput(inputId = "dataset_name", label = "Select data set", value = ""),
actionButton(inputId = "display_tab", label = "Display selected set"),
br(),
tableOutput('table'))
ui <- dashboardPagePlus(
collapse_sidebar = TRUE,
title = "Minimal",
header = dashboardHeaderPlus(
title = "Test",
enable_rightsidebar = FALSE,
rightSidebarIcon = "gears",
fixed = FALSE),
sidebar = dashboardSidebar(
sidebarMenu(
id = "left_sidebar",
menuItem("Tab1", tabName = "tab1", icon = icon("home")),
menuItem("Tab2", tabName = "tab2", icon = icon("search")))
),
body = dashboardBody(
useShinyjs(),
tabItems(tabItem(tabName = "tab1", ui_welcome),
tabItem(tabName = "tab2", ui_search)))
)
server <- function(input, output, session) {
# RENDER DATA
observeEvent(input$display_tab, {
print("Within observeEvent 'display_tab'")
selected <- input$dataset_name
print(paste0("Value of 'input$dataset_name' received: ", selected))
if(!isTruthy(selected)) return(NULL)
if(!selected %in% available_data_sets) return(NULL)
output$table <- renderTable(if(selected == "iris") iris else mtcars)
})
# SWITCH TAB & TRIGGER BTN
observeEvent(input$switch_tab, {
print("Within observeEvent 'switch_tab'")
print("updateTabItems"); updateTabItems(session, inputId = "left_sidebar", selected = "tab2")
print("updateTextInput 1"); updateTextInput(inputId = "dataset_name", value = "")
print("updateTextInput 2"); updateTextInput(inputId = "dataset_name", value = "iris")
print("Sys.sleep"); Sys.sleep(3)
print("shinyjs::click"); shinyjs::click(id = "display_tab")
})
}
shinyApp(ui, server)
Shiny reactivity is based on reaction on single events. You are basically trying to do two actions in reaction to a single event (reconfigure interface then act on it). It won't work as the reconfiguration will actually be done in between reaction code. You are trying to circumvent this by waiting using Sys.sleep function. This will only pause the whole application and it will wake up as if nothing happened. Try to put a longer sleep time and you will notice that the UI will be non responsive.
The solution is to refactor your code to be data centric and not user centric. What you want is to be able to select and display a dataset in two different ways. One "automatically" by clicking on a link the other manually by providing the dataset name and click on a button.
Here I give some flesh to the dataset that will be able to redisplay on change and provide two ways to set it's state.
server <- function(input, output, session) {
# reactive value to store selected dataset
state <- reactiveValues(selected=NULL)
# AUTO RENDER DATA on selected dataset change
output$table <- renderTable({
if(!is.null(state$selected)) {
if(state$selected == "iris") iris else mtcars
}
})
# manual select dataset
# already on tab so no need to switch
observeEvent(input$display_tab, {
if(!isTruthy(input$dataset_name) || !(input$dataset_name %in% available_data_sets)) {
state$selected <- NULL
} else {
state$selected <- input$dataset_name
}
})
# automatic selection through actionlink
# select dataset and switch tab
observeEvent(input$switch_tab, {
state$selected <- "iris"
updateTabItems(session, inputId = "left_sidebar", selected = "tab2")
})
}
here is part of code I have written, and the problem I am having is that after selecting both player 1 and player, if I wish to change player 1 again, player 2 also resets. I had to make Player 2's input dependent on Player1 because I do not want the user to be able to select the same player for both the dropdowns. Here is my code:
server.R
shinyServer <- function(input, output) {
# Creates the first drop down menu through which the user can select the first player.
output$firstdropdown <- renderUI({
selectizeInput("player1", label = "Choose Player 1:", choices = winrateRole(input$role)$player,
selected = NULL, multiple = FALSE, options = NULL)
})
# Creates the second drop down menu. User cannot select the same player he/she select as the first player.
output$seconddropdown <- renderUI({
all.choices <- winrateRole(input$role)$player
without.player1 <- all.choices[which(all.choices != input$player1)]
selectizeInput("player2", label = "Choose Player 2:", choices = without.player1,
selected = NULL, multiple = FALSE, options = NULL)
})
ui.R
shinyUI <- fluidPage(
navbarPage(strong("Test1"),
tabPanel("Overview"),
tabPanel("Tab1",
sidebarLayout(
sidebarPanel(
uiOutput("firstdropdown"),
br(),
uiOutput("seconddropdown")
)
))
)
winrateRole
winrateRole <- function(role) {
blue.role <- paste0("blue", role)
blue <- league.data %>%
group_by_(blue.role) %>%
summarise(winrate.blue = round((sum(bResult) / n() * 100), digits = 2)) %>%
arrange_(blue.role) %>%
select(player = blue.role, winrate.blue)
red.role <- paste0("red", role)
red <- league.data %>%
group_by_(red.role) %>%
summarise(winrate.red = round((sum(rResult) / n() * 100), digits = 2)) %>%
arrange_(red.role) %>%
select(player = red.role, winrate.red)
return (left_join(blue, red))
}
your problem is that you recreate the whole input everytime the value of the first select changes.
In this case you want to use the updateInput method which only changes the values you want but leaves the rest unchanged. Unfortunatly updateSelectizeInput seems to reset selected every time choises are changed so you need to save the value of the input in a reactivevariable.
server <- function(input, output, session) {
current_selection <- reactiveVal(NULL)
observeEvent(input$player2, {
current_selection(input$player2)
})
# update player 2
observeEvent({
input$player1
},{
all.choices <- winrateRole
without.player1 <- all.choices[which(all.choices != input$player1)]
updateSelectInput(
inputId = "player2",
session = session,
selected = current_selection(),
choices = without.player1
)
},
ignoreInit = FALSE
)
output$firstdropdown <- renderUI({
selectizeInput(
"player1",
label =
"Choose Player 1:",
choices = winrateRole,
selected = NULL,
multiple = FALSE,
options = NULL) })
# Creates the second drop down menu. User cannot select the same player he/she select as the first player.
output$seconddropdown <- renderUI({
selectizeInput(
"player2",
label = "Choose Player 2:",
choices = winrateRole,
selected = NULL,
multiple = FALSE,
options = NULL)})
}
** ui.R**
shinyUI <- fluidPage(
navbarPage(strong("Test1"),
tabPanel("Overview"),
tabPanel("Tab1",
sidebarLayout(
sidebarPanel(
uiOutput("firstdropdown"),
br(),
uiOutput("seconddropdown")
))
)
My shiny application has multiple tabs. In one of the tabs I have plot output which I want to use to create reports in another tab. I have included a checkbox in the first tab for the user to select the output for reporting. In the second tab I am trying to update a check box group input based on the selection of the first tab. However I am getting only the first option selected.
The reproducible code is as follows: This is based on ifelse condition:
library(shiny)
library(shinydashboard)
ui <- dashboardPage(
dashboardHeader(
title = "MODULE",titleWidth = 225
),
dashboardSidebar(
width = 225,
sidebarMenu(id = "tabs",
menuItem("TOPLINES", tabName = "tplines", icon = shiny::icon("dashboard")),
menuItem("MY MONTHLY REPORTS", tabName = "myweeklyrep", icon = shiny::icon("compass"))
)),
dashboardBody(
tabItems(
tabItem(
tabName = "tplines",
fluidRow(
box(
checkboxInput(inputId = "inventorytop8metrocheck", "Add to reports", value = FALSE),
width = 6, status = "info", title = "Inventory information",
div(plotlyOutput("inventorytop8metro"), width = "100%", height = "400px", style = "font-size:80%;")
),
box(
checkboxInput(inputId = "top15categoriestplinescheck", "Add to reports", value = FALSE),
width = 6, status = "info", title = "Top 15 categories",
div(plotlyOutput("top15categoriestplines"), style = "font-size:90%")
))),
tabItem(
tabName = "myweeklyrep",
fluidRow(
h4("AVAILABLE ANALYSIS", align = 'center'),br(),
column(width = 12,
list(tags$div(align = 'left',
class = 'multicol',
checkboxGroupInput(inputId = 'analysisSelector',
label = "Select the analysis:",
choices = "",
selected = "",
inline = FALSE)))
))))))
server <- function(session,input,output){
observe({
updateCheckboxGroupInput(session, inputId = "analysisSelector", label = "", choices =
ifelse(!is.null(input$top15categoriestplinescheck) || length(input$top15categoriestplinescheck) != 0, "Inventory top 8 metros",
ifelse(!is.null(input$inventorytop8metrocheck) || length(input$inventorytop8metrocheck) != 0, "Top 15 categories - Topline", "No selection")),
selected = "",inline = FALSE)
})
}
shinyApp(ui,server)
I tried with if, else if also but they aren't working. Any thoughts?
The if, else if conditions:
updateCheckboxGroupInput(session, inputId = "analysisSelector", label = "", choices =
if(!is.null(input$top15categoriestplinescheck) || length(input$top15categoriestplinescheck) != 0){
"Inventory top 8 metros"
} else if (!is.null(input$inventorytop8metrocheck) || length(input$inventorytop8metrocheck) != 0){
"Top 15 categories - Topline"
} else {
return()
},
selected = "",inline = FALSE)
EDIT:
I tried the following option: which renders the checkboxes irrespective of whether they are selected or not.
getlist <- reactive({
if(!is.null(input$top15categoriestplinescheck) & !is.null(input$inventorytop8metrocheck)){
c("Top 15 categories - Topline","Inventory of top 8 metros - Topline")
} else if (!is.null(input$top15categoriestplinescheck)){
"ABC"
} else if (!is.null(input$inventorytop8metrocheck)){
"DEF"
} else {
return()
}
})
observe({
updateCheckboxGroupInput(session, inputId = "analysisSelector", label = "Select the analysis:", choices =
as.list(getlist()),
selected = "",inline = FALSE)
})
This is actually easier to handle with observeEvent as explained in the documentation of this function (see ?observeEvent). From what I understand, it actually wraps observe but in a more intuitive way.
You have to pass it two arguments: an event (in this case, the click on one of your checkboxGroupInputs) and the action to perform when this event occurs.
The server function thus becomes:
server <- function(session,input,output){
updateAnalysisSelector <- function(session) {
choices <- ifelse(input$top15categoriestplinescheck, "Inventory top 8 metros",
ifelse(input$inventorytop8metrocheck, "Top 15 categories - Topline", "No selection"))
updateCheckboxGroupInput(session,
inputId = "analysisSelector",
label = "Select the analysis:",
choices = choices,
selected = "",
inline = FALSE)
}
observeEvent(input$top15categoriestplinescheck, updateAnalysisSelector(session))
observeEvent(input$inventorytop8metrocheck, updateAnalysisSelector(session))
}
I'm sure this could be simplified if your UI did not have two separate checkbox groups but this works for your current implementation.