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.
library(shinysnap)
snap_restorers()
#> name scope
#> 1 shiny.checkboxGroupInput builtin
#> 2 shiny.radioInput builtin
#> 3 shiny.selectInput builtin
#> 4 shiny.dateInput builtin
#> 5 shiny.dateRangeInput builtin
#> 6 shiny.sliderInput builtin
#> 7 shiny.passwordInput builtin
#> 8 shiny.actionButtonInput builtin
#> 9 shiny.fileInputBinding builtin
#> 10 bslib.accordion builtin
#> 11 bslib.sidebar builtin
#> 12 bslib.task-button builtin
#> 13 bslib.card builtin
#> 14 shinyMatrix.matrixNumeric builtin
#> 15 shinyMatrix.matrixCharacter builtinEverything 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.
A restorer is a function of four arguments that returns the message
to send as a list, or NULL to skip the input:
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.
Open the input’s update*() function and keep the
part that carries the value. For
shinyMatrix::updateMatrixInput() that is:
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.
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.
Write the restorer. The built-in one for shinyMatrix is:
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.
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.
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):
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).
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.