Skip to content

Instantly share code, notes, and snippets.

@alexbbrown
Last active February 13, 2024 01:05
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save alexbbrown/6e77383b48a044191771 to your computer and use it in GitHub Desktop.
Save alexbbrown/6e77383b48a044191771 to your computer and use it in GitHub Desktop.
Shiny Alias of URL (#) with Input Parameters DEMO - for HTML5

URL as Input/Output

This is a sample project to test use of URL as an output and input for shiny scripts.

It presents the data in 3 ways:

  • in the URL
  • in the Input controls
  • as text in the output

You can copy and share the URL You can edit the URL (and press enter) to update. The URL is live updated. It only works on browsers which support hashchange - so probably webkit & FF.

Goals

The goal is to use the URL as a duplicate of the input state of a shiny app, with the following features:

* URL gets updated as input gets updated
* input gets updated as URL gets updated
* some mechanism to avoid circuits
* If URL is provided to a new browser, page state is reproduced

Technology Warning

Initially it will use the 'hashchange' event, which is NOT universally available.

There is an option to use various query.js plugins later.

Also I haven't thought carefully about feedback loops - it might explode in flames.

Version Log

  • Fields to sync is now a variable - not hardcoded in the server
  • Supports textinput fields as well (included in example)
library(shiny)
url_fields_to_sync <- c("beverage","milk","sugarLumps","customer");
# Define server logic required to respond to d3 requests
shinyServer(function(input, output) {
# Generate a plot of the requested variable against mpg and only
# include outliers if requested
output$order <- reactiveText(function() {
paste(input$beverage,
if(input$milk) "with milk" else ", black",
"and",
if (input$sugarLumps == 0) "no" else input$sugarLumps,
"sugar lumps",
"for",
if (input$customer == "") "next customer" else input$customer)
})
firstTime <- TRUE
output$hash <- reactiveText(function() {
newHash = paste(collapse=",",
Map(function(field) {
paste(sep="=",
field,
input[[field]])
},
url_fields_to_sync))
# the VERY FIRST time we pass the input hash up.
return(
if (!firstTime) {
newHash
} else {
if (is.null(input$hash)) {
NULL
} else {
firstTime<<-F;
isolate(input$hash)
}
}
)
})
})
library(shiny)
hashProxy <- function(inputoutputID) {
div(id=inputoutputID,class=inputoutputID,tag("div",""));
}
# Define UI for shiny d3 chatter application
shinyUI(pageWithSidebar(
# Application title
headerPanel("Shiny Persisting Input in URL",
"Demo of how to persist inputs in browser URL"),
sidebarPanel(
tags$p("This widget is a demonstration of how to preserve input state across sessions, using the URL hash."),
selectInput("beverage", "Choose a beverage:",
choices = c("Tea", "Coffee", "Cocoa")),
checkboxInput("milk", "Milk"),
sliderInput("sugarLumps", "Sugar Lumps:",
min=0, max=10, value=3),
textInput("customer", "Your Name:")
),
mainPanel(
includeHTML("URL.js"),
h3(textOutput("order")),
hashProxy("hash")
)
))
<script type="text/javascript">
(function(){
this.countValue=0;
var changeInputsFromHash = function(newHash) {
// get hash OUTPUT
var hashVal = $(newHash).data().shinyInputBinding.getValue($(newHash))
if (hashVal == "") return
// get values encoded in hash
var keyVals = hashVal.substring(1).split(",").map(function(x){return x.split("=")})
// find input bindings corresponding to them
keyVals.map(function(x) {
var el=$("#"+x[0])
if (el.length > 0 && el.val() != x[1]) {
console.log("Attempting to update input " + x[0] + " with value " + x[1]);
if (el.attr("type") == "checkbox") {
el.prop('checked',x[1]=="TRUE")
el.change()
} else if(el.attr("type") == "radio") {
console.log("I don't know how to update radios")
} else if(el.attr("type") == "slider") {
// This case should be setValue but it's not implemented in shiny
el.slider("value",x[1])
//el.change()
} else {
el.data().shinyInputBinding.setValue(el[0],x[1])
el.change()
}
}
})
}
var HashOutputBinding = new Shiny.OutputBinding();
$.extend(HashOutputBinding, {
find: function(scope) {
return $(scope).find(".hash");
},
renderError: function(el,error) {
console.log("Shiny app failed to calculate new hash");
},
renderValue: function(el,data) {
console.log("Updated hash");
document.location.hash=data;
changeInputsFromHash(el);
}
});
Shiny.outputBindings.register(HashOutputBinding);
var HashInputBinding = new Shiny.InputBinding();
$.extend(HashInputBinding, {
find: function(scope) {
return $(scope).find(".hash");
},
getValue: function(el) {
return document.location.hash;
},
subscribe: function(el, callback) {
window.addEventListener("hashchange",
function(e) {
changeInputsFromHash(el);
callback();
}
, false);
}
});
Shiny.inputBindings.register(HashInputBinding);
})()
</script>
@amackey
Copy link

amackey commented Aug 1, 2013

My version simply changing commas to semicolons for delimiting key/value pairs is available here: https://gist.github.com/6841cf03e54d021175f0.git

@fozy81
Copy link

fozy81 commented May 26, 2014

This looks great, I couldn't get it working with my shiny app. Could tabPanels upset this?
I've shoehorned your code into my riverflyTest branch ui.R and server.R:
https://github.com/fozy81/Riverfly/tree/riverflyTest

Maybe I've missed something while transferring it but the gist above run perfectly. Any advice appreciated but sure I'll figure it out eventually. Thanks.

@vojtechhuser
Copy link

what is the purpose of the URL.js and where do I place it in the folder structure? Is it compatible with with http://shinyapps.io hosting as well?

@avilella
Copy link

I got it working by just placing URL.js at the same level as ui.R and server.R

@RAPLER
Copy link

RAPLER commented Aug 17, 2014

Very nice example. I was looking for how to bind a selection of data that I'm going to offer from an ordinary web page to my shiny application. Your solution does very well the trick.
Many thanks for this contribution.

@fozy81
Copy link

fozy81 commented Sep 7, 2014

Hi, just to say I've implemented the above here https://github.com/fozy81/Riverfly/tree/riverflyTest
It doesn't consider tabs or navbar at the moment. But really useful - thanks for the url.js script. I did find that any commas in the values messed it up. So I removed the commas from the variable. In this case the variable was the name of the sample site.

@jen0liu
Copy link

jen0liu commented Jun 10, 2015

Hi alex, i couldn't get this to work. I'm expecting to have something like this right?
localhost:8888/?selectionA=123&selectionB=something
And when i change the input params in shiny app, the URL string will change, correct? Similarly, if I change the URL parameters, the results will be updated with the new params (via URL not via the selection panel).

This is what I did:

  1. I coped URL.js into a separate file, and put it in the same directory as server.R and ui.R
  2. in server.R, copied pretty much everything except the part with output$order because I have my variables etc.
  3. in ur.R, I added hashProxy section,as well as includeHTML("URL.js"), hashProxy("hash")

Looks like I was able to create newHash variable correctly based on input params. But it just doesn't get updated to the URL (if I change an input param), and when I change the URL, it doesn't change the actual input parameters that are used in all subsequent calculations. Thanks Alex!

@TSyndrome
Copy link

There is a problem in the slider, I set it to 8 and then open a new browser, paste the URL. The slider resets to 3 and so does milk. Only dropdown and text input remain same

@tholor
Copy link

tholor commented Jul 12, 2016

Anybody knows of an easy way to make it work with tabs/navbar as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment