shiny icon indicating copy to clipboard operation
shiny copied to clipboard

Date ranges can go backward

Open bborgesr opened this issue 6 years ago • 6 comments

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)

bborgesr avatar Apr 26 '18 23:04 bborgesr

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?

ColinFay avatar Mar 28 '19 09:03 ColinFay

Any news on that one?

ColinFay avatar Aug 26 '19 09:08 ColinFay

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)

ColinFay avatar Aug 28 '19 08:08 ColinFay

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)

eaurele avatar Aug 30 '19 10:08 eaurele

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.

ColinFay avatar Sep 06 '19 21:09 ColinFay

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.

rsh52 avatar Jan 29 '24 15:01 rsh52