shiny
shiny copied to clipboard
Date ranges can go backward
Date ranges can go backward with no indication of error... Maybe even better would be to make it impossible to do so? (though that could break weird existing code?)
Repro
library(shiny)
ui <- fluidPage(
dateRangeInput("daterange", "Date range:",
start = "2010-01-01",
end = "2001-12-31"),
verbatimTextOutput("res")
)
server <- function(input, output, session) {
output$res <- renderPrint({
paste("Start at", input$daterange[1], "and end at", input$daterange[2])
})
}
shinyApp(ui, server)
Just stumbled upon this behavior causing bugs in an app I've been building. You'd expect the user to be disciplined with this, but errors happen, notably when users think they are in a specific year but in fact they are not.
So I can see two things here:
- prevent developers from launching a dateRange with end < start
newDateRange <- function (inputId, label, start = NULL, end = NULL, min = NULL,
max = NULL, format = "yyyy-mm-dd", startview = "month", weekstart = 0,
language = "en", separator = " to ", width = NULL, autoclose = TRUE)
{
#browser()
if (inherits(start, "Date"))
start <- format(start, "%Y-%m-%d")
if (inherits(end, "Date"))
end <- format(end, "%Y-%m-%d")
if (inherits(min, "Date"))
min <- format(min, "%Y-%m-%d")
if (inherits(max, "Date"))
max <- format(max, "%Y-%m-%d")
restored <- restoreInput(id = inputId, default = list(start,
end))
start <- restored[[1]]
end <- restored[[2]]
if (start > end){
stop(paste("Error at input", inputId, ":`start` can't be posterior to `end`."), call. = FALSE)
}
attachDependencies(div(id = inputId, class = "shiny-date-range-input form-group shiny-input-container",
style = if (!is.null(width))
paste0("width: ", validateCssUnit(width), ";"), controlLabel(inputId, label), div(class = "input-daterange input-group", tags$input(class = "input-sm form-control", type = "text", `data-date-language` = language, `data-date-week-start` = weekstart, `data-date-format` = format, `data-date-start-view` = startview, `data-min-date` = min, `data-max-date` = max, `data-initial-date` = start, `data-date-autoclose` = if (autoclose) "true"else "false"), span(class = "input-group-addon", separator), tags$input(class = "input-sm form-control", type = "text", `data-date-language` = language, `data-date-week-start` = weekstart, `data-date-format` = format, `data-date-start-view` = startview, `data-min-date` = min, `data-max-date` = max, `data-initial-date` = end, `data-date-autoclose` = if (autoclose) "true"else "false"))), datePickerDependency)
}
ui <- fluidPage(
newDateRange("daterange", "Date range:",
start = "2010-01-01",
end = "2001-12-31"),
verbatimTextOutput("res")
)
Error: Error at input daterange :`start` can't be posterior to `end`.
- Prevent the user of the app from selecting end < start. I wonder what would be the expected behavior here. Should start automatically update end and vice versa (which seems to me hard to implement / use)? Should an error be thrown?
Any news on that one?
So, I've been thinking around this yesterday, as it's problematic in an app I need to send to production.
The issue being that users can select start > end.
In my app, the user need to be able to select a start
which can be any date before the end
. And they need to be able to select any end
but after start
.
The current implementation of dateRangeInput()
does not allow to easily implement that. A simple way to do that would be to wrap something that says: if the user changes the start
, update the min
of end
, and if the user changes end
, update the max
of start
. But currently, this approach is not possible as there is just one input sent to server, so you can't either observe one of the two, nor update only one min or max.
Idea around that:
- Implement this if dance in JS straight in
dateRangeInput()
. - Implement a second version of
dateRangeInput()
that has two set of min/max and that sends two inputs to the server so it's easier to handle.
For anyone coming there looking for a solution, here is a piece of code to handle that:
library(shiny)
ui <- fluidPage(
shinyalert::useShinyalert(),
dateRangeInput("daterange1", "Date range:",
start = "2010-12-01",
end = "2010-12-31")
)
server <- function(input, output, session) {
r <- reactiveValues(
start = lubridate::ymd("2010-12-01"),
end = lubridate::ymd("2010-12-31")
)
observeEvent( input$daterange1 , {
start <- lubridate::ymd(input$daterange1[[1]])
end <- lubridate::ymd(input$daterange1[[2]])
if (start >= end){
shinyalert::shinyalert("start > end", type = "error")
updateDateRangeInput(
session,
"daterange1",
start = r$start,
end = r$end
)
} else {
r$start <- input$daterange1[[1]]
r$end <- input$daterange1[[2]]
}
}, ignoreInit = TRUE)
}
shinyApp(ui, server)
An alternative to prevent user from selecting start > end can be found in shinyWidgets:
library(shiny)
ui <- fluidPage(
shinyWidgets::airDatepickerInput("daterange", "Date range:",
range = TRUE,
value = c("2010-01-01", "2001-12-31")),
verbatimTextOutput("res")
)
server <- function(input, output, session) {
output$res <- renderPrint({
paste("Start at", input$daterange[1], "and end at", input$daterange[2])
})
}
shinyApp(ui, server)
Hey @eaurele,
Thanks for pointing.
I can't use airDatepickerInput
in some apps because it's causing some namespace conflicts with the rest of the code.
Cheers, C.
Just wanted to drop another potential solution in case anyone else finds this issue using shinyvalidate
:
# To be placed in your app_server() referencing an existing dateRangeInput() from your UI
iv <- shinyvalidate::InputValidator$new()
iv$add_rule(
"dateRangeInput",
~if(.[1] >= .[2]) "Start date cannot be greater than or equal to end date."
)
iv$enable()
For my use case, this at least stops a user from initiating elements of my shiny app that would break if a start date was greater than an end date.