rstudio / shiny

Easy interactive web applications with R
http://shiny.rstudio.com
Other
5.32k stars 1.87k forks source link

Date ranges can go backward #2043

Open bborgesr opened 6 years ago

bborgesr commented 6 years ago

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)
ColinFay commented 5 years ago

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:

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`.
ColinFay commented 4 years ago

Any news on that one?

ColinFay commented 4 years ago

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:

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)
eaurele commented 4 years ago

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)
ColinFay commented 4 years ago

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.

rsh52 commented 5 months ago

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()

image

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.