how to display htmlwidgets inside openCPU apps? - r

We want to display an htmlwidget inside an OpenCPU application.The html is generated by Leaflet without problems, however we have some troubles to display it within the OpenCPU app. We used the following function to generate the Leaflet Map:
leafmap1 <- function(ecoregion='10105',wdpa_id='1500'){
require(leaflet)
require(shiny)
require(htmlwidgets)
m <- leaflet() %>%
addTiles() %>% # Add default OpenStreetMap map tiles
addMarkers(lng=174.768, lat=-36.852, popup="The birthplace of R")
m
}
The JavaScript is as follows:
function SochiCtrl($scope){
$scope.ecoregions = ['10105']
$scope.wdpa_ids = ["1500"]
$scope.ecoregion = $scope.ecoregions[0]
$scope.wdpa_id = $scope.wdpa_ids[0]
$scope.makeChart = function(){
var req = ocpu.rpc("leafmap1",
{"ecoregion": $scope.ecoregion, "wdpa_id": $scope.wdpa_id}, function(output){
$('#map').html(output)
}).fail(function(text){
alert("Error: " + req.responseText);
});
}
$scope.$watchCollection('[ecoregion, wdpa_id]', function(newValues){
console.log(newValues)
$scope.makeChart({ecoregion: newValues[0], wdpa_id: newValues[1]})
})
}
Now the app shows the Leaflet frame but I have some problems getting the json from OpenCPU I got the following error No method asJSON S3 class: htmlwidget. I also tried with:
m <- toJSON(m, force= TRUE)
but it doesn't seem to work.
The full code is available at: https://github.com/Arevaju/ocpuleaflet.
Thanks a lot for your help and congratulations for your great work!!

Sorry as this is not a tested answer, but this is easier to explain a proposed approach here than in a comment.
What I propose is to have your function leafmap1 return plain text (HTML) instead of the leaflet object.
You can see that the leaflet object inherits the class htmlwidget.
For this class, there exists a method for the generic function toHTML that would allows retrieving such HTML code.
Assumed a leaflet object:
m = leaflet() %>% addTiles()
Let's have a look at it's class:
class(m)
[1] "leaflet" "htmlwidget"
Get the underlying generated html:
> (out <- unclass(htmlwidgets:::toHTML(m)))
[[1]]
<div id="htmlwidget-7863" style="width:100%;height:400px;" class="leaflet html-widget"></div>
[[2]]
<script type="application/json" data-for="htmlwidget-7863">{"x":{"calls":[{"method":"addTiles","args": ["http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",null,null,{"minZoom":0,"maxZoom":18,"maxNativeZoom":null,"tileSize":256,"subdomains":"abc","errorTileUrl":"","tms":false,"continuousWorld":false,"noWrap":false,"zoomOffset":0,"zoomReverse":false,"opacity":1,"zIndex":null,"unloadInvisibleTiles":null,"updateWhenIdle":null,"detectRetina":false,"reuseTiles":false,"attribution":"© <a href=\"http://openstreetmap.org\">OpenStreetMap\u003c/a> contributors, <a href=\"http://creativecommons.org/licenses/by-sa/2.0/\">CC-BY-SA\u003c/a>"}]}]},"evals":[],"jsHooks":[]}</script>
[[3]]
NULL
attr(,"html_dependencies")
attr(,"html_dependencies")[[1]]
[...]
Third slot contains dependancies (javascript+css) so I guess those are already loaded in your report.
You may return the concatenation of the first two components (function result):
return(paste(out[[1]], out[[2]], sep="\n"))

Related

Why Do "rstudio:run:" Hyperlinks Work Only for Functions Exported by "rlang"?

Background
I've often aspired to prototype an R package which serves as a source of truth for styling conventions in (say) diagnostic output, and which enables central updates to those conventions.
After many failed experiments with crayon::hyperlink(), I was excited to stumble across Console hyperlinks to functions like `rlang::last_error()`
which preview documentation when hovered
and execute the code when clicked!
Attempt
On a whim, I prototyped a family of functions (see Code section below) that make universal provisions for such functionality. By inspecting the source code, I was able to replicate the functionality above for other rlang functions. Here we pipe (|>) my function style_run_call0() through cat()
rlang::quo("test") |> style_run_call0() |> cat()
to display in the Console a hyperlink to live code
which previews documentation when hovered
and executes the code when clicked:
Problem
This all works well enough for rlang functions. But for functions from other packages
base::sum(1:10) |> style_run_call0() |> cat()
it wrongly displays a "broken" link:
Even with rlang, the links are broken for any arguments that are calls themselves
rlang::quo(rlang::as_string("test")) |> style_run_call0() |> cat()
and for all private functions
rlang:::ansi_alert() |> style_run_call0() |> cat()
though the links do work for "simple" arguments with operators:
rlang::quo(TRUE || FALSE) |> style_run_call0() |> cat()
#> `rlang::quo(TRUE || FALSE)`
style_run_expr0(TRUE || FALSE) |> cat()
#> `TRUE || FALSE`
Question
I am 99% sure this problem boils down to the rstudio:run: format
style_rlang_run <- function(code) {
style_hyperlink(
paste0("rlang::", code),
paste0("rstudio:run:rlang::", code)
)
}
and its limitations for hyperlinking code.
But why would rlang:::style_rlang_run() need to specify "rlang::" if the "rstudio:run:" accommodated only rlang functions and nothing else?
Code
Text Links
These links are generated from character strings.
# Hyperlink to a URL.
style_hyperlink_url <- function (url, text = NULL, params = NULL) {
# Display text defaults to URL.
if (is.null(text))
text <- url
# Underline the link in blue for classic URL style.
crayon::underline$blue(rlang:::style_hyperlink(text, url, params))
}
# Hyperlink to run code (given as text) interactively in RStudio.
style_run_code <- function(code, text = NULL) {
# Display text defaults to code in backticks.
if (is.null(text))
text <- paste0("`", code, "`")
# Underline the hyperlink in silver to designate code link.
crayon::underline$silver(rlang:::style_hyperlink(text, paste0("rstudio:run:", code)))
}
Live Code
These code links are generated from R language itself.
# Hyperlink to run a (simple) 'call' object as an interactive command in RStudio.
style_run_call <- function(call, text = NULL) {
call_expr <- rlang::get_expr(call)
call_qual <- call_qualify(call_expr)
style_run_code(base::deparse1(call_qual), text)
}
# Hyperlink to run a (simple) literal call as an interactive command in RStudio.
style_run_call0 <- function(call, text = NULL) {
call_quo <- rlang::enquo0(call)
style_run_call(call_quo, text)
}
# Hyperlink to run a (simple) 'expression' object as an interactive command in RStudio.
style_run_expr <- function(expr, text = NULL) {
expr_expr <- rlang::get_expr(expr)
call_quo <- rlang::quo(rlang::eval_bare(!!expr_expr))
# Text defaults to the expression itself, not the code evaluating it.
if (is.null(text))
text <- paste0("`", base::deparse1(expr_expr), "`")
style_run_call(call_quo, text)
}
# Hyperlink to run a (simple) literal 'expression' as an interactive command in RStudio.
style_run_expr0 <- function(expr, text = NULL) {
expr_quo <- rlang::enquo0(expr)
style_run_expr(expr_quo, text)
}
Call Qualification
This function qualifies a call to fn() as pkg::fn().
call_qualify <- function(call) {
if (!rlang::is_call(call))
rlang::abort("`call` must be a call")
if (!rlang::is_call_simple(call))
rlang::abort("`call` must be a simple call")
# Check the namespace that qualifies the function.
call_ns_name <- rlang::call_ns(call)
# Qualify if necessary.
if (is.null(call_ns_name)) {
call_fn <- rlang.call_fn(call)
call_fn_name <- rlang::call_name(call)
call_ns_name <- rlang::ns_env_name(call_fn)
call_ns_sym <- rlang::sym(call_ns_name)
call_fn_sym <- rlang::sym(call_fn_name)
# TODO: Check if namespace exports the function.
if (fn_is_exported(call_fn_name, call_ns_name)) {
qual_sym <- quote(`::`)
} else {
qual_sym <- quote(`:::`)
}
# Assemble the qualified function name.
qual_expr <- quote(`::`(pkg = NULL, name = NULL))
qual_expr[[1]] <- qual_sym
qual_expr$pkg <- call_ns_sym
qual_expr$name <- call_fn_sym
# Assemble the qualified call.
call_expr <- rlang::get_expr(call)
call_expr[[1]] <- qual_expr
call <- rlang::set_expr(call, call_expr)
}
# Return the qualified call.
call
}
Helper Functions
# Function to check if a function is exported (TRUE) from its namespace, or internal (FALSE).
fn_is_exported <- function(fn_name, ns_name) {
# Placeholder.
TRUE
# TODO: Figure out an efficient algorithm.
# Since a function object may be assigned to a new name, perhaps we should match by bytecode instead?
}
# Current styler from "rlang".
.rlang.style_hyperlink <- rlang:::style_hyperlink
# Function to extract the function from a call. Deprecated in "rlang" and reconstructed here.
.rlang.call_fn <- function(call, env = caller_env()) {
expr <- rlang::get_expr(call)
env <- rlang::get_env(call, env)
if (!rlang::is_call(expr)) {
rlang:::abort_call_input_type("call")
}
switch(rlang:::call_type(expr),
recursive = rlang::abort("`call` does not call a named or inlined function"),
inlined = rlang:::node_car(expr),
named = , namespaced = ,
rlang::eval_bare(rlang:::node_car(expr), env)
)
}
Executing base:: functions is explicitly forbidden, see the RStudio PR and discussion in the issue.
If I understand the test code correctly, you can run code of your own package with:
cli::style_hyperlink("show style code", "ide:run:yourpackage::style()")
If yourpackage is not installed, it should just be copied into the console, but not executed.
I'm not really sure what your use case is, but maybe another option would be to generate a link to a help page or vignette in your package?
cli::style_hyperlink("help page", "ide:help:yourpackage::correct_style")

shiny downgrade fontawesome 5 to 4

I work on a shiny project quite entangled with fontawesome 4.7, and it has brought us great value. As a free user of fontawesome, I don't see we have any advantage of upgrading to 5.3.1. Many of the free icons have become uglier/cruder, and one would have to pay for the pro version to get the icon styles similar to 4.7.
Example table available in 4.7 with 9 cells
in 5.3 table is onle free as 4 cells and rather chubby lines. The old 9 cell format is only available for pro users
From my own simple perspective, it seems the fontawesome team intends to strongly nudge their free users to go pro.
Rstudio shiny 1.1 links to fontawesome 4.7.1
Rstudio shiny 1.2 links to fontawesome 5.3.1
Are there any easy ways to both have shiny 1.2 and fontawesome 4.7.1?
EDIT
Link by pork chop seems very relevant, I will try it out and update...
Download fontawesome 4.7.1 & unzip
insert code below in global.R
update path to unzipped fontawesome
.... and then shiny can do both fontawesome 4.7.1 and +5. This specific solution copies as suggested by Pork Chop old version of font-awesome in installed shiny library. Also I updated the icon()-function so it is possible to have fontawesome versions to coexist and to ensure correct linking. In this solution a new icon() function is placed in globalEnv hence in top of search()-path. That saved my code base legacy issues without changing anything else.
However for making a new shiny-application, I would name icon-function icon_legacy() to avoid relying on search()-path or implement in a support R-package for shiny-application.
##install new shiny version
install.packages("shiny") #install newest shiny
library(shiny)
library(htmltools)
#source in this function to globalEnv
#' Legacy means good old iconic times
#'
#' #param local_path_fa_4.7.1
#' #param shiny_path
#'
#' #return
#' #export
#' #import shiny htmltools
#' #details #this installs legacy font-awesome and return a function similar to icon
#'
#' #examples
#'
#' install.packages("shiny") #install newest shiny
#' library(shiny)
#' library(htmltools)
#' my_fa_path = "./misc/global_source/fa_shiny_4.7.1/font-awesome"
#' icon_legacy = activate_icon_legacy(my_fa_path) #tadaaa use icon_legacy now
#' #btw css pseudo-elements seem to work out-of-the-box also
#'
#' icon = icon_legacy #you may also feel like placing icon in global env to override shiny::icon
activate_icon_legacy = function(
local_path_fa_4.7.1,
shiny_path = system.file(package="shiny")
) {
#find out what version of shiny is installed
uses_fontawesome5 = packageVersion("shiny")>=1.2 #because implemented since 1.2
shiny_resource_path = paste0(shiny_path,"/www/shared")
misses_fontawesome4 = !"font-awesome" %in% list.files(shiny_resource_path) #because new fa dir is called 'fontawesome'
#if legacy dir is missing from library copy into installed library
if(uses_fontawesome5 && misses_fontawesome4) {
file.copy(
from = local_path_fa_4.7.1,
to = shiny_resource_path,
recursive = TRUE,copy.mode = FALSE
)
}
#import minor dependency from shiny library into closure
font_awesome_brands = shiny:::font_awesome_brands
tags = htmltools::tags
#source this modified icon() function from library/shiny/R/bootstrap.R
#notice the legacy feature if true will use old fa 4.7.1 else new
icon_legacy <- function(name, class = NULL, lib = "font-awesome",legacy=TRUE) {
prefixes <- list(
"font-awesome" = "fa",
"glyphicon" = "glyphicon"
)
prefix <- prefixes[[lib]]
# determine stylesheet
if (is.null(prefix)) {
stop("Unknown font library '", lib, "' specified. Must be one of ",
paste0('"', names(prefixes), '"', collapse = ", "))
}
# build the icon class (allow name to be null so that other functions
# e.g. buildTabset can pass an explicit class value)
iconClass <- ""
if (!is.null(name)) {
prefix_class <- prefix
if (prefix_class == "fa" && name %in% font_awesome_brands) {
prefix_class <- "fab"
}
iconClass <- paste0(prefix_class, " ", prefix, "-", name)
}
if (!is.null(class))
iconClass <- paste(iconClass, class)
iconTag <- tags$i(class = iconClass)
# font-awesome needs an additional dependency (glyphicon is in bootstrap)
if (lib == "font-awesome") {
if(legacy) {
htmlDependencies(iconTag) <- htmlDependency(
"fontwesome","4.7.1", "www/shared/font-awesome", package = "shiny",
stylesheet = c("css/font-awesome.css","font-awesome.min.css"))
} else {
htmlDependencies(iconTag) <- htmlDependency(
"font-awesome", "5.3.1", "www/shared/fontawesome", package = "shiny",
stylesheet = c("css/all.min.css","css/v4-shims.min.css")
)
}
}
htmltools::browsable(iconTag)
}
return(icon_legacy)
}
#download extract fontawesome 4.7.1 and write path here
my_fa_path = "./misc/global_source/fa_shiny_4.7.1/font-awesome"
icon_legacy = activate_icon_legacy(my_fa_path) #tadaaa use icon_legacy now
#btwcss pseudos seem to work out-of-the-box also
#one may also feel like placing icon_legacy() as icon() in globalEnv to override shiny::icon
#if youre too lazy change all your original code. This will work any code in ui.R and server.R
#however packages with explicit namespaces are likely not overridden by this.
icon = icon_legacy
#now shiny code will behave like this
icon("table",legacy=TRUE) # old style 9 cell table
icon("table",legacy=FALSE) # new fat 4 cell table
#...one may feel like opting for more explicit and strict namespace solution wrapped in some package.
#but that would be a lot more boiler plate code not relevant for this answer
#this solution also fixed my fontawesome CSS pseudo-elements issues

Error: "HTML widgets cannot be represented in plain text"

When I try to run this by Jupyter:
library(leaflet)
m <- leaflet() %>%
addTiles() %>% # Add default OpenStreetMap map tiles
addMarkers(lng=174.768, lat=-36.852, popup="The birthplace of R")
m # Print the map
I get this error:
HTML widgets cannot be represented in plain text (need html).
As suggested here I have tried:
library(plotly)
embed_notebook(m)
but I get:
Error in UseMethod("embed_notebook"): no applicable method for 'embed_notebook' applied to an object of class "c('leaflet', 'htmlwidget')
How could I plot this kind of graph?
embed_notebook is specifically defined for plotly objects. I would look through the documentation to see if leaflet has its own equivalent function.
Alternatively, since it's an html widget, you can save it as an html file, then embed that file inside of an iframe in your notebook. This can be accomplished with something like
library(IRdisplay)
htmlwidgets::saveWidget(m, "m.html")
display_html('<iframe src="m.html" width=100% height=450></iframe>')
If you don't want to keep a bunch of html files in your folder, you can also enter the raw html of your widget into your iframe then delete it using
rawHTML = base64enc::dataURI(mime = "text/html;charset=utf-8", file = "m.html")
display_html(paste("<iframe src=", rawHTML, "width=100% height=450></iframe>", sep = "\""))
unlink("m.html")
But I've found that this generates an error with the most recent version of Chrome.
If it helps, I cobbled together the following function from the source code of embed_notebook
embed = function(x, height) {
library(IRdisplay)
tmp = tempfile(fileext = ".html")
htmlwidgets::saveWidget(x, tmp)
rawHTML = base64enc::dataURI(mime = "text/html;charset=utf-8", file = tmp)
display_html(paste("<iframe src=", rawHTML, "width=100% height=", height, "id=","igraph", "scrolling=","no","seamless=","seamless", "frameBorder=","0","></iframe>", sep = "\""))
unlink(tmp)
}
But again, this may not work for Chrome.

Pulsing marker plugin with the R leaflet package

I want to add a pulsing marker to the map I built with the R leaflet package
Here is the plugin I want to use. In order to do it, I wrote the following code from this from this github account
library(leaflet)
library(htmltools)
library(htmlwidgets)
# This tells htmlwidgets about our plugin name, version, and
# where to find the script. (There's also a stylesheet argument
# if the plugin comes with CSS files.)
esriPlugin <- htmlDependency("leaflet-icon-pulse",version = "1.0",
src = c(href = "https://raw.githubusercontent.com/mapshakers/leaflet-icon-pulse/master/src/"),
script = "L.Icon.Pulse.js",stylesheet ="L.Icon.Pulse.css")
# A function that takes a plugin htmlDependency object and adds
# it to the map. This ensures that however or whenever the map
# gets rendered, the plugin will be loaded into the browser.
registerPlugin <- function(map, plugin) {
map$dependencies <- c(map$dependencies, list(plugin))
map
}
leaflet() %>% setView(-52.520, 13.185, zoom = 5) %>%
# Register ESRI plugin on this map instance
registerPlugin(esriPlugin) %>%
# Add your custom JS logic here. The `this` keyword
# refers to the Leaflet (JS) map object.
onRender("function(el,x) {
var pulsingIcon = L.icon.pulse({iconSize:[20,20],color:'red'});
var marker = L.marker([52.9167,13.9333],{icon: pulsingIcon}).addTo(this);
}")
However, it does not work. I got a grey rectangle instead of a beautiful map with a beautiful pulsing marker. Anyone see something wrong in my code?
This code works with three remarks:
the js and css file are stored locally
the icon is displayed correctly in RStudio viewer but it does not pulsate
With the "Show in new window" option in the Viewer everything works fine (tested in Firefox 48.0 and Chrome 53.0.2785.116 (64-bit))
This is the code (adjust the src parameter to match your file location):
library(leaflet)
library(htmltools)
library(htmlwidgets)
# This tells htmlwidgets about our plugin name, version, and
# where to find the script. (There's also a stylesheet argument
# if the plugin comes with CSS files.)
esriPlugin <- htmlDependency("leaflet-icon-pulse",version = "1.0",
src = "/home/valter/Desktop/test",
script = "L.Icon.Pulse.js",stylesheet ="L.Icon.Pulse.css")
# A function that takes a plugin htmlDependency object and adds
# it to the map. This ensures that however or whenever the map
# gets rendered, the plugin will be loaded into the browser.
registerPlugin <- function(map, plugin) {
map$dependencies <- c(map$dependencies, list(plugin))
map
}
leaflet() %>% addTiles() %>% setView(-52.520, 13.185, zoom = 5) %>%
# Register ESRI plugin on this map instance
registerPlugin(esriPlugin) %>%
# Add your custom JS logic here. The `this` keyword
# refers to the Leaflet (JS) map object.
onRender("function(el,x) { var pulsingIcon = L.icon.pulse({iconSize:[20,20],color:'red'});
var marker = L.marker([13.185,-52.520],{icon: pulsingIcon}).addTo(this); }")

How to display (advanced) customed popups for leaflet in Shiny?

I am using R shiny to build web applications, and some of them are leveraging the great leaflet features.
I would like to create a customed and advanced popup, but I do not know how to proceed.
You can see what I can do in the project I created for this post on github, or directly in shinyapp.io here
The more complex the popup is, the weirdest my code is, as I am sort of combining R and html in a strange way (see the way I define my custompopup'i' in server.R)..
Is there a better way to proceed? What are the good practices to build such popups? If I plan to display a chart depending on the marker being clicked, should I build them all in advance, or is that possible to build them 'on the fly'? How can I do that?
Many thanks in advance for your views on this, please do not hesitate to share your answer here or to directly change my github examples!
Regards
I guess this post still has some relevance. So here is my solution on how to add almost any possible interface output to leaflet popups.
We can achieve this doing the following steps:
Insert the popup UI element as character inside the leaflet standard popup field. As character means, it is no shiny.tag, but merely a normal div. E.g. the classic uiOutput("myID") becomes <div id="myID" class="shiny-html-output"><div>.
Popups are inserted to a special div, the leaflet-popup-pane. We add an EventListener to monitor if its content changes. (Note: If the popup disappears, that means all children of this div are removed, so this is no question of visibility, but of existence.)
When a child is appended, i.e. a popup is appearing, we bind all shiny inputs/outputs inside the popup. Thus, the lifeless uiOutput is filled with content like it's supposed to be. (One would've hoped that Shiny does this automatically, but it fails to register this output, since it is filled in by Leaflets backend.)
When the popup is deleted, Shiny also fails to unbind it. Thats problematic, if you open the popup once again, and throws an exception (duplicate ID). Once it is deleted from the document, it cannot be unbound anymore. So we basically clone the deleted element to a disposal-div where it can be unbound properly and then delete it for good.
I created a sample app that (I think) shows the full capabilities of this workaround and I hope it is designed easy enough, that anyone can adapt it. Most of this app is for show, so please forgive that it has irrelevant parts.
library(leaflet)
library(shiny)
runApp(
shinyApp(
ui = shinyUI(
fluidPage(
# Copy this part here for the Script and disposal-div
uiOutput("script"),
tags$div(id = "garbage"),
# End of copy.
leafletOutput("map"),
verbatimTextOutput("Showcase")
)
),
server = function(input, output, session){
# Just for Show
text <- NULL
makeReactiveBinding("text")
output$Showcase <- renderText({text})
output$popup1 <- renderUI({
actionButton("Go1", "Go1")
})
observeEvent(input$Go1, {
text <<- paste0(text, "\n", "Button 1 is fully reactive.")
})
output$popup2 <- renderUI({
actionButton("Go2", "Go2")
})
observeEvent(input$Go2, {
text <<- paste0(text, "\n", "Button 2 is fully reactive.")
})
output$popup3 <- renderUI({
actionButton("Go3", "Go3")
})
observeEvent(input$Go3, {
text <<- paste0(text, "\n", "Button 3 is fully reactive.")
})
# End: Just for show
# Copy this part.
output$script <- renderUI({
tags$script(HTML('
var target = document.querySelector(".leaflet-popup-pane");
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if(mutation.addedNodes.length > 0){
Shiny.bindAll(".leaflet-popup-content");
};
if(mutation.removedNodes.length > 0){
var popupNode = mutation.removedNodes[0].childNodes[1].childNodes[0].childNodes[0];
var garbageCan = document.getElementById("garbage");
garbageCan.appendChild(popupNode);
Shiny.unbindAll("#garbage");
garbageCan.innerHTML = "";
};
});
});
var config = {childList: true};
observer.observe(target, config);
'))
})
# End Copy
# Function is just to lighten code. But here you can see how to insert the popup.
popupMaker <- function(id){
as.character(uiOutput(id))
}
output$map <- renderLeaflet({
leaflet() %>%
addTiles() %>%
addMarkers(lat = c(10, 20, 30), lng = c(10, 20, 30), popup = lapply(paste0("popup", 1:3), popupMaker))
})
}
), launch.browser = TRUE
)
Note: One might wonder, why the Script is added from the server side. I encountered, that otherwise, adding the EventListener fails, because the Leaflet map is not initialized yet. I bet with some jQuery knowledge there is no need to do this trick.
Solving this has been a tough job, but I think it was worth the time, now that Leaflet maps got some extra utility. Have fun with this fix and please ask, if there are any questions about it!
The answer from K. Rohde is great, and the edit that #krlmlr mentioned should also be used.
I'd like to offer two small improvements over the code that K. Rohde provided (full credit still goes to K. Rohde for coming up with the hard stuff!). Here is the code, and the explanation of the changes will come after:
library(leaflet)
library(shiny)
ui <- fluidPage(
tags$div(id = "garbage"), # Copy this disposal-div
leafletOutput("map"),
div(id = "Showcase")
)
server <- function(input, output, session) {
# --- Just for Show ---
output$popup1 <- renderUI({
actionButton("Go1", "Go1")
})
observeEvent(input$Go1, {
insertUI("#Showcase", where = "beforeEnd",
div("Button 1 is fully reactive."))
})
output$popup2 <- renderUI({
actionButton("Go2", "Go2")
})
observeEvent(input$Go2, {
insertUI("#Showcase", where = "beforeEnd", div("Button 2 is fully reactive."))
})
output$popup3 <- renderUI({
actionButton("Go3", "Go3")
})
observeEvent(input$Go3, {
insertUI("#Showcase", where = "beforeEnd", div("Button 3 is fully reactive."))
})
# --- End: Just for show ---
# popupMaker is just to lighten code. But here you can see how to insert the popup.
popupMaker <- function(id) {
as.character(uiOutput(id))
}
output$map <- renderLeaflet({
input$aaa
leaflet() %>%
addTiles() %>%
addMarkers(lat = c(10, 20, 30),
lng = c(10, 20, 30),
popup = lapply(paste0("popup", 1:3), popupMaker)) %>%
# Copy this part - it initializes the popups after the map is initialized
htmlwidgets::onRender(
'function(el, x) {
var target = document.querySelector(".leaflet-popup-pane");
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if(mutation.addedNodes.length > 0){
Shiny.bindAll(".leaflet-popup-content");
}
if(mutation.removedNodes.length > 0){
var popupNode = mutation.removedNodes[0];
var garbageCan = document.getElementById("garbage");
garbageCan.appendChild(popupNode);
Shiny.unbindAll("#garbage");
garbageCan.innerHTML = "";
}
});
});
var config = {childList: true};
observer.observe(target, config);
}')
})
}
shinyApp(ui, server)
The two main changes:
The original code would only work if the leaflet map is initialized when the app first starts. But if the leaflet map is initialized later, or inside a tab that isn't initially visible, or if the map gets created dynamically (for example, because it uses some reactive value), then the popups code won't work. In order to fix this, the javasript code needs to be run in htmlwidgets:onRender() that gets called on the leaflet map, as you can see in the code above.
This isn't about leaflet, but more of a general good practice: I wouldn't use makeReactiveBinding() + <<- generally. In this case it's being used correctly, but it's easy for people to abuse <<- without understanding what it does so I prefer to stay away from it. An easy almost drop-in replacement for that can be to use text <- reactiveVal(), which would be a better approach in my opinion. But even better than that in this case is instead of using a reactive variable, it's simpler to just use insertUI() like I do above.

Resources