--- title: "VanillaCalendar in Shiny" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{VanillaCalendar in Shiny} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE) ``` The code here is not run when the vignette is built, because it needs a running Shiny app. Both apps it draws on ship with the package, so you can run them before writing anything yourself: ```{r} # the smallest useful app, reproduced in full below shiny::runApp(system.file("examples/minimal", package = "VanillaCalendar")) # a tour of everything else on this page shiny::runApp(system.file("examples/gallery", package = "VanillaCalendar")) ``` ## The smallest app that works There are three steps, and they are the same three as for any htmlwidget: 1. `VanillaCalendarOutput("cal")` in the UI, to say where the calendar goes. 2. `renderVanillaCalendar()` in the server, to build it. 3. Read what the user picked from `input$cal_selected`. That is the whole app: ```{r} library(shiny) library(VanillaCalendar) ui <- fluidPage( titlePanel("Pick a date"), VanillaCalendarOutput("cal", height = "400px"), textOutput("chosen") ) server <- function(input, output) { output$cal <- renderVanillaCalendar(VanillaCalendar()) output$chosen <- renderText({ if (length(input$cal_selected) == 0) "Nothing picked yet." else format(input$cal_selected, "%A, %d %B %Y") }) } shinyApp(ui, server) ``` Nothing above is specific to this widget except the two function names. What the calendar *does* is decided by the options you pass, and the structure stays as it is: ```{r} # let the user drag out a range output$cal <- renderVanillaCalendar( VanillaCalendar(list(selectionDatesMode = "multiple-ranged")) ) # or make it a text box with a popup, for a form output$cal <- renderVanillaCalendar( VanillaCalendar(list(inputMode = TRUE), height = "auto") ) # or add a time picker under the dates output$cal <- renderVanillaCalendar( VanillaCalendar(list(selectionTimeMode = 24), height = "440px") ) ``` ## Reading what the user did The widget reports its state through inputs named after the output id. For an output called `"cal"`: | Input | Type | Set when | |---|---|---| | `input$cal_selected` | `Date` vector | A date is clicked | | `input$cal_selected_month` | integer, 1-12 | A month is chosen | | `input$cal_selected_year` | integer | A year is chosen | | `input$cal_displayed` | `Date`, first of the month | The arrows are used | | `input$cal_time` | character, e.g. `"14:30"` | The time changes | | `input$cal_week` | list of `week` and `year` | A week number is clicked | | `input$cal_ready` | `TRUE` | The calendar has initialised | `input$cal_selected` is always a `Date` vector, and is `Date(0)` — not `NULL`, not `list()` — when the user has selected nothing. That means the obvious code works without guards: ```{r} output$summary <- renderText({ dates <- input$cal_selected if (length(dates) == 0) return("Nothing selected.") paste(length(dates), "date(s), the first being", format(min(dates))) }) ``` ![Selecting a range of dates and reading it in R](../man/figures/range-selection.gif) Selections are sent with event priority, so clicking the same date twice, or re-picking a date you had just cleared, reaches the server both times rather than being swallowed as an unchanged value. ## A date picker instead of a calendar Forms usually want a date field, not a permanent block of calendar. That is `inputMode`: ```{r} VanillaCalendarOutput("when", height = "auto") output$when <- renderVanillaCalendar( VanillaCalendar(list(inputMode = TRUE, selectionDatesMode = "single"), height = "auto") ) ``` The widget renders a text box, opens the calendar as a popup when the box is clicked, and writes the chosen date into it. `input$when_selected` updates as usual. ![A popup date picker filling its text box](../man/figures/input-mode.gif) Use `height = "auto"` for input mode, and `positionToInput` to say where the popup goes: `"auto"`, one of `"left"`, `"center"` and `"right"`, or a vertical and horizontal pair such as `c("bottom", "left")`. ## Changing a calendar without re-rendering it Re-running `renderVanillaCalendar()` builds a whole new calendar: the selection is lost, the displayed month jumps back, and the widget visibly flickers. To change something about a calendar that is already on the page, use a proxy. ```{r} observeEvent(input$theme, { vcSet(VanillaCalendarProxy("cal"), list(selectedTheme = input$theme)) }) ``` ![Switching the theme from the server, in place](../man/figures/themes.gif) The verbs map onto the library's [instance methods](https://vanilla-calendar.pro/docs/reference/methods): ```{r} proxy <- VanillaCalendarProxy("cal") vcSet(proxy, list(dateMin = input$start)) # apply new options vcUpdate(proxy) # re-render with current options vcShow(proxy) # show a popup calendar vcHide(proxy) # hide it again vcDestroy(proxy) # remove it entirely ``` Only the parts you are actually setting are reset, so the theme change above leaves the selection and the displayed month exactly as the user left them. When you want to clear something you are *not* setting, say so with `reset` — any of `year`, `month`, `dates`, `time`, `locale`: ```{r} # change the minimum date and drop the selection that no longer fits it vcSet(proxy, list(dateMin = Sys.Date()), reset = list(dates = TRUE)) ``` Inside a Shiny module, build the proxy with the *unnamespaced* id; it uses the session to work out the full one: ```{r} calendarServer <- function(id) { moduleServer(id, function(input, output, session) { output$cal <- renderVanillaCalendar(VanillaCalendar()) observeEvent(input$go, vcHide(VanillaCalendarProxy("cal"))) }) } ``` ## Reacting to another input The two pieces together — options from R, applied by proxy — give the common "end date cannot be before start date" behaviour without any re-rendering: ```{r} server <- function(input, output, session) { output$start <- renderVanillaCalendar( VanillaCalendar(list(inputMode = TRUE), height = "auto") ) output$end <- renderVanillaCalendar( VanillaCalendar(list(inputMode = TRUE), height = "auto") ) observeEvent(input$start_selected, { vcSet(VanillaCalendarProxy("end"), list(dateMin = input$start_selected)) }) } ``` ## Dropping down to JavaScript Any option that takes a function takes one here too, through `htmlwidgets::JS()`. Your callback runs in addition to the built-in one, so the Shiny inputs above keep working: ```{r} VanillaCalendar(list( selectionDatesMode = "multiple", onClickDate = htmlwidgets::JS( "function(self) { console.log(self.context.selectedDates); }" ), onCreateDateEls = htmlwidgets::JS( "function(self, dateEl) { dateEl.title = 'Custom tooltip'; }" ) )) ``` The callback signatures, and the `self.context` fields they can read, are in the [upstream reference](https://vanilla-calendar.pro/docs/reference/actions). ## Theming with bslib `selectedTheme = "system"` follows the page rather than a fixed choice, reading the attribute named by `themeAttrDetect`. bslib writes `data-bs-theme`, so: ```{r} VanillaCalendar(list( selectedTheme = "system", themeAttrDetect = "html[data-bs-theme]" )) ```