-
Notifications
You must be signed in to change notification settings - Fork 2
Handle search/hash URL components and custom app locations #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
63895b0
72a9b39
16f302b
b842737
248d965
c525610
733d41a
e9ee710
01aa30b
13a3a14
c355ff7
56ba9dc
d2b70aa
1019a39
e8b168c
8d0c7cd
249eaae
7938bf2
99a4265
e8da98d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| ^.*\.Rproj$ | ||
| ^\.Rproj\.user$ | ||
| ^LICENSE\.md$ | ||
| ^inst/tmp$ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| Package: blaze | ||
| Type: Package | ||
| Title: Observe URL Paths with 'shiny' | ||
| Version: 0.0.1 | ||
| Version: 0.0.1.9000 | ||
| Author: c( | ||
| person("Nathan", "Teetor", email = "[email protected]", role = c("aut", "cre"))) | ||
| person("Nathan", "Teetor", email = "[email protected]", role = c("aut", "cre")), | ||
| person("Garrick", "Aden-Buie", email = "[email protected]", role = c("ctb"), comment = c(ORCID = "0000-0002-7111-0077"))) | ||
| Maintainer: Nathan Teetor <[email protected]> | ||
| Description: Simulate routing within a 'shiny' application by observering | ||
| and reacting to URL changes. 'shiny' applications are considered single page | ||
|
|
@@ -12,12 +13,13 @@ Description: Simulate routing within a 'shiny' application by observering | |
| License: MIT + file LICENSE | ||
| Encoding: UTF-8 | ||
| LazyData: true | ||
| RoxygenNote: 7.0.2 | ||
| RoxygenNote: 7.1.1 | ||
| Imports: | ||
| fs, | ||
| htmltools, | ||
| rematch2, | ||
| rlang, | ||
| shiny | ||
| shiny, | ||
| utils | ||
| Suggests: | ||
| testthat | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,73 @@ paths_remove <- function(p) { | |
| .globals$paths[[p]] <- NULL | ||
| } | ||
|
|
||
| paths <- function(...) { | ||
| #' Declare Paths for Use with Shiny | ||
| #' | ||
| #' Declare path endpoints that will be available inside your Shiny app. This | ||
| #' function should be called before the call to [shiny::shinyApp()] in your | ||
| #' `app.R` file or inside your `server.R` script before the server function. | ||
| #' This function makes it possible for users to enter your app URL with a path, | ||
| #' e.g. `<myapp.com>/about`, and be directed to the `"about"` page within your | ||
| #' Shiny app. | ||
| #' | ||
| #' @examples | ||
| #' \dontrun{ | ||
| #' library(shiny) | ||
| #' library(blaze) | ||
| #' | ||
| #' options(shiny.launch.browser = TRUE) | ||
| #' | ||
| #' blaze::paths( | ||
| #' "home", | ||
| #' "about", | ||
| #' "explore" | ||
| #' ) | ||
| #' | ||
| #' shinyApp( | ||
| #' ui = fluidPage( | ||
| #' blaze(), | ||
| #' tags$nav( | ||
| #' pathLink("/home", "Home"), | ||
| #' pathLink("/about", "About"), | ||
| #' pathLink("/explore", "Explore") | ||
| #' ), | ||
| #' uiOutput("page") | ||
| #' ), | ||
| #' server = function(input, output, session) { | ||
| #' state <- reactiveValues(page = NULL) | ||
| #' | ||
| #' observePath("/home", { | ||
| #' state$page <- "Home is where the heart is." | ||
| #' }) | ||
| #' | ||
| #' observePath("/about", { | ||
| #' state$page <- "About this, about that." | ||
| #' }) | ||
| #' | ||
| #' observePath("/explore", { | ||
| #' state$page <- div( | ||
| #' p("Curabitur blandit tempus porttitor."), | ||
| #' p("Vivamus sagittis lacus augue rutrum dolor auctor.") | ||
| #' ) | ||
| #' }) | ||
| #' | ||
| #' output$page <- renderUI(state$page) | ||
| #' } | ||
| #' ) | ||
| #' } | ||
| #' | ||
| #' @param ... Path names as character strings that will be valid entry points | ||
| #' into your Shiny app. | ||
| #' | ||
| #' @param app_path The name of the sub-directory where your Shiny app is hosted, | ||
| #' e.g. `host.com/<app_path>/`. | ||
| #' | ||
| #' @return Invisibly writes temporary HTML files to be hosted by Shiny to | ||
| #' redirect users to the requested path within your Shiny app. The [paths()] | ||
| #' function returns the temporary folder used by \pkg{blaze}. | ||
| #' | ||
| #' @export | ||
| paths <- function(..., app_path = NULL) { | ||
| args <- lapply(list(...), as_paths) | ||
| routes <- unique(unlist(args)) | ||
| tmp <- path(tempdir(check = TRUE), "blaze") | ||
|
|
@@ -34,28 +100,43 @@ paths <- function(...) { | |
| old <- setwd(tmp) | ||
| on.exit(setwd(old)) | ||
|
|
||
| .globals$app_path <- if (!is.null(app_path)) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer not to use this sort of short-hand. |
||
| app_path <- gsub("^/|/$", "", app_path) | ||
| dir_create(app_path, recurse = TRUE) | ||
| app_path | ||
| } | ||
|
|
||
| lapply(routes, function(route) { | ||
| if (!is.null(app_path)) route <- path(app_path, route) | ||
| dir_create(route) | ||
| }) | ||
|
|
||
| dirs <- dir_ls(tmp, recurse = FALSE, type = "directory") | ||
| app_dir <- if (is.null(app_path)) tmp else path(tmp, app_path) | ||
| dirs <- dir_ls(app_dir, recurse = FALSE, type = "directory") | ||
| prefixes <- path_file(dirs) | ||
|
|
||
| Map(p = prefixes, dir = dirs, paths_add) | ||
|
|
||
| dir_walk(recurse = TRUE, type = "directory", fun = function(d) { | ||
| dir_walk(app_dir, recurse = TRUE, type = "directory", fun = function(d) { | ||
| index <- file_create(path(d, "index.html")) | ||
|
|
||
| if (!grepl("^/", d)) { | ||
| d <- paste0("/", d) | ||
| } | ||
|
|
||
| app_redirect <- if (is.null(app_path)) "" else paste0("/", app_path) | ||
|
|
||
| cat(file = index, sprintf(" | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <head><script>window.location.replace(\"/?redirect=%s\")</script></head> | ||
| <head><script> | ||
| // blaze: redirect /<pathname> to Shiny app using URL search query | ||
| let {origin, pathname, search, hash} = window.location | ||
| search = (search ? search + '&' : '?') + `redirect=${pathname}` | ||
| window.location.replace(origin + '%s' + search + hash) | ||
| </script></head> | ||
| <body>Redirecting</body> | ||
| </html>", d | ||
| </html>", app_redirect | ||
| )) | ||
| }) | ||
|
|
||
|
|
@@ -66,6 +147,7 @@ as_paths <- function(x, ...) { | |
| UseMethod("as_paths", x) | ||
| } | ||
|
|
||
| #' @export | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like to keep
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This exports the S3 methods but not the generic, following the advice in R Packages.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see the second bullet, thank you for the link. I'm not wholly convinced and the wording is rather vague. Do you know anymore about why this is good practice?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As of R 4.0 it's actually required. The This thread has a bit more discussion: r-lib/devtools#2293
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the discussion link. I had not known the change would affect internal generics in this way. |
||
| as_paths.character <- function(x, ...) { | ||
| n <- names(x) | ||
|
|
||
|
|
@@ -81,6 +163,7 @@ as_paths.character <- function(x, ...) { | |
| }, character(1)) | ||
| } | ||
|
|
||
| #' @export | ||
| as_paths.list <- function(x, ...) { | ||
| x <- unlist(x) | ||
|
|
||
|
|
@@ -91,6 +174,20 @@ as_paths.list <- function(x, ...) { | |
| as_paths.character(x) | ||
| } | ||
|
|
||
| #' @export | ||
| as_paths.yml <- function(x, ...) { | ||
| as_paths.list(unclass(x)) | ||
| } | ||
|
|
||
|
|
||
| path_app <- function(path) { | ||
| if (!grepl("^/", path)) { | ||
| path <- paste0("/", path) | ||
| } | ||
|
|
||
| if (!is.null(.globals$app_path)) { | ||
| path <- paste0("/", .globals$app_path, path) | ||
| } | ||
|
|
||
| path | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,7 @@ | |
| #' @param ... Additional arguments passed to `grepl()`. | ||
| #' | ||
| #' @param domain A reactive context, defaults to | ||
| #' `shiny::getDefaultReactiveDomain()`. | ||
| #' [shiny::getDefaultReactiveDomain()]. | ||
| #' | ||
| #' @export | ||
| observePath <- function(path, handler, env = parent.frame(), quoted = FALSE, | ||
|
|
@@ -58,6 +58,14 @@ as_route <- function(x) { | |
| x <- gsub("/:([^/]*)", "/(?<\\1>[^/]+)", x) | ||
| } | ||
|
|
||
| if (!grepl("^/", x)) { | ||
| x <- paste0("/", x) | ||
| } | ||
|
|
||
| if (!is.null(.globals$app_path)) { | ||
| x <- paste0("/", .globals$app_path, x) | ||
| } | ||
|
|
||
| if (!grepl("^[\\^]", x)) { | ||
| x <- paste0("^", x) | ||
| } | ||
|
|
@@ -86,8 +94,6 @@ mask_params <- function(path, url) { | |
| }) | ||
| } | ||
|
|
||
| #' @rdname observePath | ||
| #' @export | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realize the function does nothing at the moment. This is a change I especially want to exclude from a feature pull request. |
||
| param <- function(x, params = peek_params()) { | ||
| sym_x <- rlang::ensym(x) | ||
| name_x <- rlang::as_name(sym_x) | ||
|
|
@@ -103,25 +109,36 @@ peek_params <- function() { | |
| #' Push a new URL path or get the current path. | ||
| #' | ||
| #' @param path A character string specifying a new URL path | ||
| #' @param mode Either `"push"` or `"replace"`. If `"push"`, the default, the | ||
| #' path is pushed onto the history stack and pressing the back button in the | ||
| #' browser will redirect to the current path (before pushing the path). If | ||
| #' `"replace"`, then the pushed path will replace the current path without | ||
| #' changing the next page in the browser's back button stack. | ||
| #' | ||
| #' @param session A reactive context, defaults to | ||
| #' `shiny::getDefaultReactiveDomain()`. | ||
| #' | ||
| #' @export | ||
| pushPath <- function(path, session = getDefaultReactiveDomain()) { | ||
| if (!grepl("^/", path)) { | ||
| path <- paste0("/", path) | ||
| } | ||
| pushPath <- function(path, mode = c("push", "replace"), session = getDefaultReactiveDomain()) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to not support replace. The replace behaviour feels unexpected or non-standard. What's the motivation for including a replace option?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was primarily for parity with
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, thank you for outlining a use case. I might introduce a second function then |
||
| path <- path_app(path) | ||
| mode <- match.arg(mode) | ||
|
|
||
| path <- utils::URLencode(path) | ||
|
|
||
| session$sendCustomMessage("blaze:pushstate", list( | ||
| path = path | ||
| path = path, | ||
| mode = mode | ||
| )) | ||
| } | ||
|
|
||
| #' @rdname pushPath | ||
| #' @export | ||
| getPath <- function(session = getDefaultReactiveDomain()) { | ||
| session$clientData$url_state | ||
| url <- session$clientData$url_state | ||
| if (is.null(.globals$app_path)) { | ||
| return(url) | ||
| } | ||
| url <- sub(paste0("/", .globals$app_path), "", url, fixed = TRUE) | ||
| if (!length(url) || !grepl("^/", url)) url <- paste0("/", url) | ||
| url | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| .onLoad <- function(pkg, lib) { | ||
| .onLoad <- function(lib, pkg) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another one for a cleanup pull request. |
||
| shiny::addResourcePath("blaze", path_package("blaze", "www")) | ||
| } | ||
This file was deleted.
This file was deleted.
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.