R/turbo.R

#' @title Turbo
#'
#' @description
#' Turbo uses complementary techniques to dramatically reduce the amount of
#' custom JavaScript that most web applications will need to write.
#'
#' [Documentation](https://turbo.hotwired.dev/)
#'
#' @param id       (str) client ID, leave blank to send to all clients
#' @param fragment (flg) whether to return a full page or fragment
#' @param env      (env) calling environment
#' @param turbo (opt) include turbo assets
#'   * `TRUE`   - include assets with global defaults (default)
#'   * `FALSE`  - do not include turbo assets
#'   * `list()` - a list that overrides the default config
#'     * root          (str) path of the turbo root
#'     * libs          (str) path of the libraries
#'     * force_reload  (flg) whether this page will force a reload
#'     * progress_bar  (flg) whether progress bar is visible
#'     * pb_height     (css) height of progress bar (top)
#'     * pb_delay      (int) milliseconds of delay before progress bar appears
#'     * pb_color      (css) color fo the progress bar
#'     * cache_control (str) whether to avoid previewing or caching the page
#'     * turbo_opt_in  (flg) if TRUE, then turbo is disabled across the page
#'     * turbo_socket  (flg) add the turbo web socket
#'     * app_version   (str) a string indicating the version, or NA to
#'                           disable; used to force reload when assets change
#'
#' @family Turbo
#' @name turbo
#' @export
Turbo <- R6Class(

  classname = "Turbo",
  inherit = Plugin,

  public = list(

    #' @description Create a new `TurboPlugin`
    #' @param turbo_root    (str) path of the turbo root
    #' @param progress_bar  (flg) whether progress bar is visible
    #' @param pb_height     (css) height of progress bar (top)
    #' @param pb_delay      (num) milliseconds before progress bar appears
    #' @param pb_color      (css) color fo the progress bar
    #' @param turbo_opt_in  (flg) if TRUE, then turbo is disabled in the page
    #' @param turbo_socket  (flg) add the turbo web socket
    #' @param app_version   (str) a string indicating the version, or NA to
    #'                            disable; used to force reload for new version
    initialize = function(turbo_root   = NULL,
                          progress_bar = TRUE,
                          pb_height    = "5px",
                          pb_color     = "#008080",
                          pb_delay     = 500,
                          turbo_opt_in = FALSE,
                          turbo_socket = FALSE,
                          app_version  = NA_character_) {

      # Initialize superclass
      super$initialize("turbo", "HTMLPlugin")

      # Set global configuration
      self$set_config(
        "turbo",
        root         = turbo_root,
        progress_bar = progress_bar,
        pb_height    = pb_height,
        pb_color     = pb_color,
        pb_delay     = pb_delay,
        turbo_opt_in = turbo_opt_in,
        turbo_socket = turbo_socket,
        app_version  = app_version
      )

    },

    #' @description Generates Turbo Assets
    assets = function(turbo = TRUE) {
      if (isFALSE(turbo)) {
        NULL
      } else {
        if (isTRUE(turbo)) {
          turbo <- self$config$turbo
        } else {
          assert_list(turbo)
          turbo <- modifyList(self$config$turbo, turbo)
        }
        exec(turbo_assets, !!!turbo)
      }
    },

    #' @description Create a Turbo enabled page
    #' @param path     (str) endpoint
    #' @param handler  (fun) handler; must have arguments
    #'                       `request`, `response`, `keys`, `...` and
    #'                       set the body to html tags
    #' @param ...      (arg) passed to `self$router_html()`
    #' @param lang     (str) language, defaults to `en`.
    router = function(path, handler, ...,
                      turbo    = TRUE,
                      lang     = NULL,
                      fragment = FALSE) {

      assert(test_flag(turbo), test_class(turbo, "turbo_config"))
      assert_route_handler(handler)
      turbo <- self$assets(turbo)

      # Add the router
      self$app$html$router(
        path = path,
        handler = function(request, response, keys, ...) {

          continue <- handler(request, response, keys, ...)
          response$body <- tagList(response$body, turbo)
          return(continue)

        },
        lang = lang,
        fragment = fragment,
        ...
      )

    },

    #' @description Turbo stream response with defaults
    #' @param path     (str) endpoint
    #' @param handler  (fun) handler; must have arguments
    #'                       `request`, `response`, `keys`, `...` and
    #'                       set the body to html tags
    #' @param ...      (arg) passed to `self$router_html()`
    #' @param lang     (str) language, defaults to `en`.
    #' @param type     (str) content type, defaults to turbo stream
    #' @param methods  (str) defaults to GET and POST
    router_stream = function(path, handler, ...,
                             methods = c("get", "post"),
                             type = "text/vnd.turbo-stream.html",
                             lang = NULL,
                             fragment = TRUE) {

      self$app$html$router(
        path = path,
        handler = function(request, response, keys, ...) {

          response$set_data("type", type)
          continue <- handler(request, response, keys, ...)
          return(continue)

        },
        lang = lang,
        fragment = fragment,
        methods = methods,
        ...
      )

    },

    #' @description Send a turbo websocket message
    #' @param name (str) name of the turbo message to send
    #' @param data (str) data for the event
    send = function(name, data, id = NULL) {

      self$app$ws_send(
        toJSON(
          list(
            name = assert_string(name),
            data = assert_list(data, names = "named")
          ),
          auto_unbox = TRUE
        ),
        id = paste0(
          assert_choice(id, self$clients, null.ok = TRUE) %||% self$clients,
          "+turbo"
        )
      )

    },

    #' @description Send a turbo log message
    #' @param type (str) type of the message
    #' @param ...  (chr) components of the message
    log = function(type, ...) {

      self$send("log", list(type = type, log = str_squish(...)))

    },

    #' @description Sends a visit turbo event from the server
    #' @param location (url) location (relative path) to visit
    #' @param action   (str) either:
    #'   * `advance` (default) - new entry entered into browser history
    #'   * `replace` - replaces the topmost history entry
    visit = function(location,
                     action = c("advance", "replace"),
                     id = NULL) {

      action <- match_arg(action) %||% "advance"

      self$send(
        id = id,
        name = "visit",
        data = list(
          location = assert_string(location),
          action = action
        )
      )

    },

    #' @description Clears the Turbo Drive page cache
    clear = function(id = NULL) {

      self$send(id = id, name = "clear", data = list())

    },

    #' @description Add a turbo listener
    #' @param event (str) event to listen for:
    #'   * `before-visit`  - before visiting a location, except when navigating
    #'                       by history.
    #'     * `event.detail.url` contains the URL
    #'   * `before-render` - pause rendering and prepare before fetching;
    #'                       useful for animations
    #'   * `before-fetch-request` - pause rendering and prepare the request;
    #'                              useful for attaching information to request.
    #'     * `event.detail.url` contains the URL
    #'     * `event.detail.fetchOptions` contains fetch options
    #'     * `event.target` - the element that triggered it
    #'     * `event.detail.resume()` - resumes the request
    #'   * `click` - when a Turbo-enabled link is clicked
    #'     * `event.detail.url` contains the URL
    #'     * cancelling will fall through to normal navigation
    #'   * `visit` - immediately after a visit starts
    #'     * `event.detail.url` contains the requested location
    #'     * `event.detail.action` contains the action
    #'   * `submit-start` - form submission
    #'     * `event.detail.formSubmission` - contains the `FormSubmission`
    #'   * `before-fetch-response` - when the network request completes
    #'     * `event.detail` - contains fetch options
    #'     * `event.target` - contains the firing element
    #'   * `submit-end` - fires after the form submission-initiaited network
    #'                    request completes.
    #'     * `event.detail.formSubmission` - contains the `FormSubmission`
    #'     * `event.detail` - `FormSubmissionResult`
    #'   * `before-cache` - before Turbo saves the current page to cache
    #'   * `before-render` - before rendering the page.
    #'     * `event.detail.newBody` - new body elemetn
    #'     * `event.detail.resume()` - canceling and continuing the rendering
    #'   * `before-stream-render` - fires before a page update is rendered
    #'   * `render` - fires after Turbo renders the page, if the page is cached
    #'                 then fires both when rendering cached and fresh version.
    #'   * `load` - fires after the initial page load and every visit
    #'     * `event.detail.timing` - visit timings
    #'   * `frame-render` - fires right after a turbo frame renders
    #'     * `event.target` - the frame
    #'     * `event.detail.fetchResponse` - the `FetchResponse` object
    #'   * `frame-load` - fires when Turbo frame is after `frame-render`
    #'     * `event.target` - the frame
    #' @param code (str) - JS code to be executed inside the handler.
    listen = function(event = c("before-visit",
                                "before-render",
                                "before-fetch-request",
                                "click",
                                "visit",
                                "submit-start",
                                "before-fetch_response",
                                "submit-end",
                                "before-cache",
                                "before-render",
                                "before-stream-render",
                                "render",
                                "load",
                                "frame-render",
                                "frame-load"),
                      code,
                      id = NULL) {

      assert_character(code, min.len = 1L)
      event <- match_arg(event)
      code <- paste0(c("((event) => {", code, "})"), collapse = "\n")
      assert_true(js_typeof(code) == "function")

      self$send(
        id = id,
        name = "listen",
        data = list(event = event, code  = code)
      )

    },

    #' @description Send a turbo stream
    #' @param ...    (tag) tag content of the stream
    #' @param action (str) the action to be taken for this stream element:
    #'   * `append`  - inserted last inside the element
    #'   * `prepend` - inserted first inside the element
    #'   * `replace` - replace the whole element
    #'   * `update`  - replace the element but keep handlers intact; only HTML
    #'   * `remove`  - remove the element
    #'   * `before`  - insert before the element
    #'   * `after`   - insert after the element
    #' @param target  (str) target of any links clicked in this:
    #'   * `NULL`   - the frame itself
    #'   * `"_top"` - the whole window
    #'   * `"<id>"` - a specific frame outside tof the current frame
    #' @param multiple (flg) if multiple targets, then `target` is CSS selector
    stream = function(action = c("append", "prepend", "replace", "update",
                                 "remove", "before", "after"),
                      target,
                      ...,
                      id = NULL,
                      multiple = FALSE,
                      env = parent.frame()) {

      self$send(
        name = "stream",
        data = list(
          stream = as.character(turbo_stream(
            action = action,
            target = target,
            multiple = multiple,
            ...,
            env = env
          )
          )),
        id = id
      )

    }

  ),

  active = list(

    #' @field clients client IDs for turbo websocket connections
    clients = function() {

      names(self$app$sockets) %>%
        str_split("\\+") %>%
        keep(~.[[2]] == "turbo") %>%
        map_chr(~.[[1]])

    }

  )

)
tjpalanca/hotwire.R documentation built on Dec. 23, 2021, 10:59 a.m.