Restoring custom inputs

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

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 builtin

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:

function(id, value, binding, session) list(value = value)

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:

    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:

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

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.