Reputation: 1467
I have a Shiny app with two dependent selectInputs: input$name
(corresponding to the Name field in the data) and input$date
(corresponding to the Contract_Start_Date field in the data). The input$date
options are dynamically updated based on the selected input$name
, and I need the data table output to always reflect the correct data based on synchronized selections from both selectInputs. The challenge is ensuring the data table isn’t generated based on an outdated input$date
before it is updated after input$name
changes.
To ensure synchronization between input$name
and input$date
, I used the following approach:
reactiveValues
to store synchronized inputs and their states.input$name
changes, I dynamically update input$date
options based on the selected input$name. This is done using an observeEvent that updates the input$date
choices and sets the selected date to the first available date from the new choices.input$name
and input$date
using an observe function. This function ensures that the selected date is valid for the selected name before updating the reactive values.Is this a correct approach to solve the synchronization problem? Is there a better way to do this?
I've tried several approaches to this and they all seem to work fine (one involving invalidateLater
, one involving freezeReactiveValue
, and several over approaches). No noticeable issues when actually using the app... And this is even with approaches I know are wrong that could lead to data synchronization issues and input$date
being used to generate the data table output before being invalidated once input$name
changes. On potential issue with my approach below is potential overlaps in date options. What if you change the value of input$name
and the new name has dates that overlap with the previous name?
Anyways thanks in advance for any help on this.
library(shiny)
library(dplyr)
library(lubridate)
data <- structure(list(Name = c("A", "A", "A", "A", "A", "A", "A", "A",
"A", "B", "B", "B", "B", "B", "B", "B", "B", "B"),
Contract_Start_Date = structure(c(18993, 18993, 18993, 19754,
19754, 19754, 20089, 20089, 20089, 18993,
18993, 18993, 19358, 19358, 19358, 20544, 20544, 20544),
class = "Date"),
Line_of_Business = c("L1", "L2", "L3", "L1", "L2",
"L3", "L1", "L2", "L3", "L1", "L2", "L3",
"L1", "L2", "L3", "L1", "L2", "L3"),
Members = c(589L, 342L, 235L, 100L, 212L,
235L, 335L, 456L, 567L, 687L, 982L, 123L, 145L, 167L, 231L,
234L, 1234L, 999L)), row.names = c(NA, -18L),
class = c("tbl_df", "tbl", "data.frame"))
lookup <- structure(list(Name = c("A", "A", "A", "B", "B", "B"), Contract_Start_Date = structure(c(18993,
19754, 20089, 18993, 19358, 20544), class = "Date"), Salesforce_ID = c("A1",
"A2", "A3", "B1", "B2", "B3")), class = c("tbl_df", "tbl", "data.frame"
), row.names = c(NA, -6L))
ui <- fluidPage(
titlePanel("Example App"),
sidebarLayout(
sidebarPanel(
selectInput("name", "Select Name:", choices = sort(unique(lookup$Name))),
selectInput("date", "Select Contract Start Date:", choices = NULL)
),
mainPanel(
dataTableOutput("dataOutput")
)
)
)
server <- function(input, output, session) {
# Reactive values to store synchronized inputs and state
rv <- reactiveValues(name = NULL, date = NULL)
# Reactive expression for date choices based on selected Name
date_choices <- reactive({
req(input$name)
lookup %>%
filter(Name == input$name) %>%
pull(Contract_Start_Date)
})
# Update date based on reactive expression for dates
observeEvent(input$name, {
dates <- date_choices()
updateSelectInput(session, "date", choices = dates, selected = dates[1]) # Update input$date with new valid choices
})
# Observe and update reactive values
observe({
req(input$name, input$date)
valid_dates <- date_choices()
if (as.character(input$date) %in% as.character(valid_dates)) {
rv$name <- input$name
rv$date <- input$date
}
})
# Reactive expression to filter the data based on synchronized inputs
filtered_data <- reactive({
req(rv$name, rv$date)
cat("ran\n") # should just print 'ran' once each time name or date is updated by user
data %>%
filter(Name == rv$name, Contract_Start_Date == as.Date(rv$date))
})
# Render the filtered data as a table
output$dataOutput <- renderDataTable({
req(filtered_data()) # Ensure filtered_data is valid before rendering
filtered_data()
})
}
shinyApp(ui = ui, server = server)
Upvotes: 0
Views: 58
Reputation: 535
Here is how I would approach this:
Hopefully it'll work for you!
library(shiny)
library(dplyr)
library(lubridate)
library(DT)
data <- structure(list(Name = c("A", "A", "A", "A", "A", "A", "A", "A",
"A", "B", "B", "B", "B", "B", "B", "B", "B", "B"),
Contract_Start_Date = structure(c(18993, 18993, 18993, 19754,
19754, 19754, 20089, 20089, 20089, 18993,
18993, 18993, 19358, 19358, 19358, 20544, 20544, 20544),
class = "Date"),
Line_of_Business = c("L1", "L2", "L3", "L1", "L2",
"L3", "L1", "L2", "L3", "L1", "L2", "L3",
"L1", "L2", "L3", "L1", "L2", "L3"),
Members = c(589L, 342L, 235L, 100L, 212L,
235L, 335L, 456L, 567L, 687L, 982L, 123L, 145L, 167L, 231L,
234L, 1234L, 999L)), row.names = c(NA, -18L),
class = c("tbl_df", "tbl", "data.frame"))
lookup <- structure(list(Name = c("A", "A", "A", "B", "B", "B"), Contract_Start_Date = structure(c(18993,
19754, 20089, 18993, 19358, 20544), class = "Date"), Salesforce_ID = c("A1",
"A2", "A3", "B1", "B2", "B3")), class = c("tbl_df", "tbl", "data.frame"
), row.names = c(NA, -6L))
# set initial options.
options = list(name = sort(unique(lookup$Name)))
options$date = lookup %>%
filter(Name == options$name[1]) %>%
pull(Contract_Start_Date)
ui <- fluidPage(
titlePanel("Example App"),
# hide spinner which causes screen flashing.
tags$style(HTML("
.dataTables_processing {
visibility: hidden !important;
}
")),
sidebarLayout(
sidebarPanel(
selectInput(
"name",
"Select Name:",
choices = options$name
),
selectInput(
"date",
"Select Contract Start Date:",
choices = options$date
)
),
mainPanel(
dataTableOutput("dataOutput")
)
)
)
server <- function(input, output, session) {
# observer to manage date options.
observe({
# get new options.
new_options = lookup %>%
filter(Name == input$name) %>%
pull(Contract_Start_Date)
# if the options have changed, update the input.
if(length(new_options) != length(options$date) || any(new_options != options$date)){
# if the current selection is no longer an option, we'll select a new choice.
# otherwise, just update the options.
if(!(isolate(input$date) %in% new_options)){
# this will trigger reactivity (a table update).
updateSelectizeInput('data', choices = new_options, selected = new_options[1])
} else {
# this should not trigger reactivity, because the selection did not change.
updateSelectizeInput(session = session, inputId = 'data', choices = new_options)
}
options$date <<- new_options # update global options object.
}
})
# Reactive expression to filter the data based on synchronized inputs
filtered_data <- reactive({
req(input$name, input$date)
cat("ran\n") # should just print 'ran' once each time name or date is updated by user
data %>%
filter(Name == input$name, Contract_Start_Date == as.Date(input$date))
})
# Render the filtered data as a table
output$dataOutput <- DT::renderDT({
req(isolate(filtered_data())) # Ensure filtered_data is valid before rendering
isolate(filtered_data()) # we'll update data using the observer below for a smoother UX.
})
# Update the data table as data changes.
observeEvent(filtered_data(), {
DT::replaceData(dataTableProxy('dataOutput'), filtered_data())
})
}
shinyApp(ui = ui, server = server)
Upvotes: 0