Skip to content
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

Easier specification of external file dependencies #83

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
29 changes: 29 additions & 0 deletions R/fileDependency.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#' Create a file dependency that can be accessed by the client using javascript.
#' @export
fileDependency <- function(filename, version = '0.0.1'){
htmltools::htmlDependency(
name = basename(tools:::file_path_sans_ext(filename)),
version = version,
src = dirname(filename),
attachment = basename(filename)
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure htmlDependency is the right substrate to build this on. The reason for this is that htmltools and Shiny assume that once a dependency is satisfied, it's satisfied for good. So in your example, if in a Shiny app, pcs.json changes between one run and the next (and in fact if that's the reason it's changing) then the changes would be ignored.

You could fix this by using a random value for the htmlDependency's name, I guess? That would have an unfortunate side effect of never letting go of old values (or at least references to them)... though I think that's still better than not picking up new values.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder... would there be a way to have htmlwidgets instead of resolving the dependency, just insert a link directly into the page?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Good points @jcheng5 . I did not think from this angle. I assumed the external file dependencies to be static resources, and this PR was an attempt to package the boilerplate code into something more automatic.

What you suggest here, would make this mechanism even more powerful, by allowing dynamic external dependencies. Your first comment seems like a simple enough fix that would give us reasonable mileage. However, if there is a way to insert the link directly in the page, I think it would work even better.

}

#' Mark a string as an attachment
#' @export
attachment <- function(x){
if (!file.exists(x)){
stop("The attachment ", x, " does not exist")
Copy link
Contributor

Choose a reason for hiding this comment

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

How about just leaving it to normalizePath(mustWork = TRUE)?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Good suggestion! I will make the change.

}
structure(normalizePath(x), class = unique(c("ATTACHMENT", oldClass(x))))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why oldClass vs. class here?

Copy link
Owner Author

Choose a reason for hiding this comment

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

I just used the same code as used in htmlwidgets:::JS. I should admit I did not understand why oldClass was used there in the first place 😄

Copy link
Owner Author

Choose a reason for hiding this comment

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

I should also probably add a file.exists(x) check in the attachment function to ensure that incorrect paths are caught early on.

}

attachmentDeps <- function(list) {
attachments = rapply(list, function(y){y}, classes = 'ATTACHMENT')
Copy link
Contributor

Choose a reason for hiding this comment

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

identity == function(y){y}

Copy link
Owner Author

Choose a reason for hiding this comment

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

I keep forgetting that this function exists. Thanks for the reminder!

deps = lapply(attachments, fileDependency)
attachments = lapply(as.list(attachments), function(x){
basename(tools::file_path_sans_ext(x))
})
list(attachments = attachments, deps = deps)
}

10 changes: 7 additions & 3 deletions R/htmlwidgets.R
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ toHTML <- function(x, standalone = FALSE, knitrOptions = NULL) {
)
}
)
attachments = attachmentDeps(x$x)
html <- htmltools::attachDependencies(html,
c(widget_dependencies(class(x)[1], attr(x, 'package')),
x$dependencies)
x$dependencies, attachments$deps)
)

htmltools::browsable(html)
Expand Down Expand Up @@ -123,8 +124,9 @@ widget_dependencies <- function(name, package){
# to be picked up by htmlwidgets.js for static rendering.
widget_data <- function(x, id, ...){
evals <- JSEvals(x$x)
attachments = attachmentDeps(x$x)$attachments
tags$script(type="application/json", `data-for` = id,
HTML(toJSON(list(x = x$x, evals = evals), collapse = "", digits = 16))
HTML(toJSON(list(x = x$x, evals = evals, attachments = attachments), collapse = "", digits = 16))
)
}

Expand Down Expand Up @@ -271,12 +273,14 @@ shinyRenderWidget <- function(expr, outputFunction, env, quoted) {
}
x <- .subset2(instance, "x")
deps <- .subset2(instance, "dependencies")
attachments = attachmentDeps(x)
deps = c(deps, attachments$deps)
deps <- lapply(
htmltools::resolveDependencies(deps),
shiny::createWebDependency
)
evals = JSEvals(x)
list(x = x, evals = evals, deps = deps)
list(x = x, evals = evals, deps = deps, attachments = attachments$attachments)
}

# mark it with the output function so we can use it in Rmd files
Expand Down
30 changes: 30 additions & 0 deletions inst/www/htmlwidgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@
}
}
Shiny.renderDependencies(data.deps);
resolveAttachmentUrls(data)
superfunc(el, data.x, elementData(el, "init_result"));
};
});
Expand Down Expand Up @@ -472,6 +473,7 @@
for (var i = 0; data.evals && i < data.evals.length; i++) {
window.HTMLWidgets.evaluateStringMember(data.x, data.evals[i]);
}
resolveAttachmentUrls(data)
binding.renderValue(el, data.x, initResult);
}
}
Expand Down Expand Up @@ -566,6 +568,34 @@
}
return results;
}
// Set value of a property that is nested deep
// var dat = {a: {b: {c: 2}}}
// setDeepProperty(dat, "a.b", {d: 10})
// {a: {b: {d: 10}}}
function setDeepProperty(obj, path, value){
var path = path.split(".")
//var path = splitWithEscape(path, '.', '\\')
var path2 = path.slice(0, path.length - 1)
var x = path2.reduce(function(prev, cur){
return prev[cur]
}, obj)
if (typeof value === 'undefined'){
console.log('undefined')
return x[path[path.length - 1]]
} else {
x[path[path.length - 1]] = value
}
}
// Resolve attachment urls
function resolveAttachmentUrls(data){
if (data.attachments){
Object.keys(data.attachments).map(function(k){
setDeepProperty(data.x, k,
HTMLWidgets.getAttachmentUrl(data.attachments[k], 1)
)
})
}
}
// Function authored by Yihui/JJ Allaire
window.HTMLWidgets.evaluateStringMember = function(o, member) {
var parts = splitWithEscape(member, '.', '\\');
Expand Down