#' @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]])
}
)
)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.