---
title: "Restoring custom inputs"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Restoring custom inputs}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

shinysnap restores an input by handing its client-side binding the same
message the input's `update*()` function would send. For most inputs that
message is `{value: x}`, and that is the default. Some bindings want more,
or something else, and for those a *restorer* says what to send. This
vignette shows how to write one, using the shinyMatrix input that ships
with the package as the worked example, and how to do the same on the
JavaScript side with an *adapter*.

## Which bindings are covered

```{r}
library(shinysnap)
snap_restorers()
```

Everything not listed uses the default `list(value = value)`. Inputs of
shinyWidgets and other packages are therefore restored with the default
too, which is right for those whose `receiveMessage()` accepts `{value}`
and silently wrong for the others. Rather than guess, register a restorer
for the bindings you use; the recipe below takes a few minutes per input.

## The restorer contract

A restorer is a function of four arguments that returns the message to send
as a list, or `NULL` to skip the input:

```{r, eval = FALSE}
function(id, value, binding, session) list(value = value)
```

- `id` is the fully namespaced input id.
- `value` is the value stored in the snapshot, exactly what `input$id`
  returned when the snapshot was taken.
- `binding` is the name the client-side binding was registered under
  (`"shiny.sliderInput"`, `"shinyMatrix.matrixNumeric"`, ...), as recorded
  in the snapshot's `bindings` section.
- `session` is the session being restored.

Register it for a binding name, or for one input id, with
`snap_restorer()`. A registration with `session = NULL` (the default) is
global, which is what a package or an app's `global.R` wants; a
registration with a session applies to that session only. Resolution goes
from the most specific to the least: a session restorer for the id, a
session restorer for the binding, a global restorer for the id or the
binding, the built-in one, the default.

A restorer must not call `update*()` functions itself. Those go through
`session$sendInputMessage()`, which drops messages for inputs that are not
on the page yet; the whole point of a restorer is to return the payload so
that shinysnap can deliver it when the input exists.

## Finding the payload: the recipe

1. Open the input's `update*()` function and keep the part that carries
   the value. For `shinyMatrix::updateMatrixInput()` that is:

   ```{r, eval = FALSE}
message <- list(value = list(
  data = value,
  rownames = rownames(value),
  colnames = colnames(value)
))
session$sendInputMessage(inputId, message)
   ```

2. Open the binding's `receiveMessage()` in the package's JavaScript to
   confirm the shape it reads. shinyMatrix's reads `data.value.data`,
   `data.value.rownames`, and `data.value.colnames`, and treats missing
   names as empty arrays.

3. Check the binding's `getValue()`, because shinysnap compares it with
   the expected value after applying the message and reports `mismatched`
   when they differ. shinyMatrix's returns
   `{data, rownames, colnames}` with the names as arrays. When that shape
   differs from the message's `value`, attach the expected value as the
   `expect` attribute of the returned list; when the message has no `value`
   key at all, no comparison is made.

4. Write the restorer. The built-in one for shinyMatrix is:

   ```{r, eval = FALSE}
restore_matrix <- function(id, value, binding, session) {
  if (is.null(value)) {
    return(NULL)
  }
  if (!is.matrix(value)) value <- as.matrix(value)
  rn <- rownames(value)
  cn <- colnames(value)
  data <- value
  dimnames(data) <- NULL
  payload <- list(value = list(data = data, rownames = rn, colnames = cn))
  attr(payload, "expect") <- list(list(
    data = data,
    rownames = as.list(if (is.null(rn)) character() else rn),
    colnames = as.list(if (is.null(cn)) character() else cn)
  ))
  payload
}

snap_restorer("shinyMatrix.matrixNumeric", restore_matrix)
snap_restorer("shinyMatrix.matrixCharacter", restore_matrix)
   ```

   Note the binding names: shinyMatrix registers its binding without a
   name, so the client script falls back to the type the binding reports
   for the element. Look at the `bindings` section of a snapshot taken
   from your app to see the name to register for.

5. Restore a snapshot and read the report. `applied` means the message was
   accepted and the widget shows the value; `mismatched` shows in `detail`
   what the widget reports instead; `failed` carries the JavaScript error.

Payloads are serialized with the same settings `session$sendInputMessage()`
uses: length-one vectors become scalars, `NULL` becomes `null`, dates become
`"YYYY-MM-DD"` strings, and matrices become row-major nested arrays. A
binding that wants an array even for a single value needs `as.list(value)`;
`radioButtons`, for instance, wants a scalar, while `checkboxGroupInput`
accepts either.

## Adapters: the JavaScript side

Component authors who own the JavaScript can transform the message in the
browser instead. An adapter receives the message, the element, the binding,
and the whole record, and returns the message to pass to `receiveMessage()`
(or `null` to skip the input):

```js
window.shinysnap.registerAdapter("mypkg.fancyInput", function (message, el, binding, record) {
  // fancyInput's receiveMessage() wants {selected: [...]}, and its
  // getValue() returns the same array.
  return { selected: [].concat(message.value) };
});
```

Adapters run after the R-side restorer and before the `shiny:updateinput`
event, which is triggered exactly as Shiny's own message handler triggers
it; a handler that calls `preventDefault()` on that event skips the input
(reported as `skipped`).

## Candidates

These inputs are known to need a restorer or an adapter and are not
covered yet; contributions with a verified payload are welcome:
`shinyWidgets::pickerInput()`, `shinyWidgets::airDatepickerInput()`,
`shinyWidgets::numericRangeInput()`, `shinyWidgets::sliderTextInput()`,
and `shinyWidgets::virtualSelectInput()`. Inputs that are not bound
elements at all (`plotly` events, `DT` row selections, values set from
JavaScript with `Shiny.setInputValue()`) cannot be restored through a
binding and are not captured.
