Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
63895b0
buildignore inst/tmp/
gadenbuie Jun 1, 2020
72a9b39
Remove inst/tmp files
gadenbuie Jun 2, 2020
16f302b
Maintain search params and hash components of URL
gadenbuie Jun 2, 2020
b842737
Document paths(), export paths(), blaze() and as_path() S3 methods
gadenbuie Jun 2, 2020
248d965
Bump dev version, update authors
gadenbuie Jun 2, 2020
c525610
Remove search and hash components of URL when sending browser state u…
gadenbuie Jun 2, 2020
733d41a
Update documentation to use roxygen function links
gadenbuie Jun 2, 2020
e9ee710
Fix function signature of .onLoad() to appease R CMD check
gadenbuie Jun 2, 2020
01aa30b
Adds {utils} to dependencies for packageVersion()
gadenbuie Jun 2, 2020
13a3a14
Unexport param(), it's not ready yet
gadenbuie Jun 2, 2020
c355ff7
Add at least one basic test
gadenbuie Jun 2, 2020
56ba9dc
Don't update path if same as current, also send updates back to server
gadenbuie Jun 2, 2020
d2b70aa
Add app_path argument to paths()
gadenbuie Jun 8, 2020
1019a39
Redirect pages need to redirect to /app_path
gadenbuie Jun 8, 2020
e8b168c
Ensure that getPath() returns "/" when app_path is set
gadenbuie Jun 8, 2020
8d0c7cd
Prepend app_path after checking path input in pushPath
gadenbuie Jun 8, 2020
249eaae
Add path_app() and ensure pathLink() respects global app_path
gadenbuie Jun 8, 2020
7938bf2
Reset app_path if app_path = NULL when calling paths() in same session
gadenbuie Jun 8, 2020
99a4265
observePath() correctly handles leading route path slash
gadenbuie Jun 8, 2020
e8da98d
Add option to push or replace path in pushPath()
gadenbuie Jul 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
^.*\.Rproj$
^\.Rproj\.user$
^LICENSE\.md$
^inst/tmp$
10 changes: 6 additions & 4 deletions DESCRIPTION
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
Expand All @@ -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
6 changes: 5 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# Generated by roxygen2: do not edit by hand

S3method(as_paths,character)
S3method(as_paths,list)
S3method(as_paths,yml)
export(blaze)
export(getPath)
export(observePath)
export(param)
export(pathLink)
export(paths)
export(pushPath)
importFrom(fs,dir_create)
importFrom(fs,dir_delete)
Expand Down
11 changes: 6 additions & 5 deletions R/blaze.R
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,27 @@ NULL
#'
#' Using path link elements (a variation of standard hyperlinks) users can
#' browse to different URL paths. A shiny application can detect these changes
#' with `observePath()` allowing you to update tab sets or other dynamic
#' elements within the application. `pushPath()` lets you redirect the user from
#' with [observePath()] allowing you to update tab sets or other dynamic
#' elements within the application. [pushPath()] lets you redirect the user from
#' the server.
#'
#' Because of how shiny handles URL paths be sure to run the
#' `paths()` function before launching an application.
#' [paths()] function before launching an application.
#'
#' @name blaze
"_PACKAGE"

#' @section Including in a shiny app:
#'
#' To use blaze with a shiny application the `blaze()` function must be called
#' To use blaze with a shiny application the [blaze()] function must be called
#' inside the application's UI.
#'
#' @rdname blaze
#' @export
blaze <- function() {
htmltools::htmlDependency(
name = "blaze",
version = packageVersion("blaze"),
version = utils::packageVersion("blaze"),
src = c(
file = path_package("blaze", "www", "js"),
href = "blaze/js"
Expand Down
4 changes: 2 additions & 2 deletions R/link.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
#' let the user browse to different parts of your application without refreshing
#' the current page.
#'
#' @param ... Arguments passed to `htmltools::a()`.
#' @param ... Arguments passed to [htmltools::a()].
#'
#' @param href A character string specifying the URL path to navigate to.
#'
#' @export
pathLink <- function(href, ...) {
stopifnot(is.character(href))

htmltools::a(..., href = href, `data-blaze` = NA)
htmltools::a(..., href = path_app(href), `data-blaze` = NA)
}
107 changes: 102 additions & 5 deletions R/paths.R
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -34,28 +100,43 @@ paths <- function(...) {
old <- setwd(tmp)
on.exit(setwd(old))

.globals$app_path <- if (!is.null(app_path)) {
Copy link
Owner

Choose a reason for hiding this comment

The 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
))
})

Expand All @@ -66,6 +147,7 @@ as_paths <- function(x, ...) {
UseMethod("as_paths", x)
}

#' @export
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to keep as_path and its implementations internal.

Copy link
Author

Choose a reason for hiding this comment

The 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.

Copy link
Owner

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of R 4.0 it's actually required. The #' @export tag on S3 methods doesn't export the method, it registers the method with S3method() in NAMESPACE and this step is now required, even for internal generics, for the methods to be found.

This thread has a bit more discussion: r-lib/devtools#2293

Copy link
Owner

Choose a reason for hiding this comment

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

Expand All @@ -81,6 +163,7 @@ as_paths.character <- function(x, ...) {
}, character(1))
}

#' @export
as_paths.list <- function(x, ...) {
x <- unlist(x)

Expand All @@ -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
}
35 changes: 26 additions & 9 deletions R/url.R
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -86,8 +94,6 @@ mask_params <- function(path, url) {
})
}

#' @rdname observePath
#' @export
Copy link
Owner

Choose a reason for hiding this comment

The 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)
Expand All @@ -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()) {
Copy link
Owner

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was primarily for parity with mode in shiny::updateQueryString(). Secondarily, it's conceivable that an app dev might want to use replace to update the URL during some initialization process. That was my use case, it was possible to land on one page with a set of inputs that would redirect to another page and I didn't want the first state to be on the history stack.

Copy link
Owner

Choose a reason for hiding this comment

The 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 replacePath().

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
}
2 changes: 1 addition & 1 deletion R/zzz.R
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.onLoad <- function(pkg, lib) {
.onLoad <- function(lib, pkg) {
Copy link
Owner

Choose a reason for hiding this comment

The 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"))
}
1 change: 0 additions & 1 deletion inst/tmp/hello/index.html

This file was deleted.

10 changes: 0 additions & 10 deletions inst/tmp/index.html

This file was deleted.

10 changes: 0 additions & 10 deletions inst/tmp/world/index.html

This file was deleted.

Loading