vueR
vueR copied to clipboard
vue htmlwidget container
reactable
has an underappreciated WidgetContainer
that handles htmlwidgets
(see lines ). I think vueR
should have a similar structure but without the tags
for data and options. Here is a very rough draft example that needs significant improvement, iteration, and testing.
library(htmltools)
library(htmlwidgets)
library(shiny)
library(vueR)
library(listviewer)
library(plotly)
# handle non-standard behaviors by some widgets
get_widget_data <- function(widget) {
as.tags(widget)[[2]]$children[[1]]
}
p <- plot_ly(palmerpenguins::penguins, x = ~bill_length_mm, y = ~body_mass_g)
tl <- tagList(
crosstalk::crosstalkLibs(), # necessary for g2
vueR::html_dependency_vue(minified = FALSE),
htmlDependency(
"htmlwidgets",
packageVersion("htmlwidgets"),
src = system.file("www", package = "htmlwidgets"),
script = "htmlwidgets.js"
),
p$dependencies, # this is far from ideal but plotly works differently; in most cases do not need to add since *Output handles
tags$div(
tags$button("update data", onclick = "updateData()")
),
tags$div(
id = "app",
tag('html-widget', list(
jsoneditOutput("je"),
`:x` = 'x',
`name` = 'jsonedit' # ideally we find a way to avoid this
)),
tag('html-widget', list(
plotlyOutput("pl"),
`:x` = 'x',
`name` = 'plotly' # ideally we find a way to avoid this
))
),
tags$script(HTML(
sprintf("
Vue.component(
'html-widget',
{
props: ['x', 'name'],
template: '<div><slot></slot></div>',
methods: {
// Copied from HTMLWidgets code
// Implement a vague facsimilie of jQuery's data method
elementData: function(el, name, value) {
if (arguments.length == 2) {
return el['htmlwidget_data_' + name];
} else if (arguments.length == 3) {
el['htmlwidget_data_' + name] = value;
return el;
} else {
throw new Error('Wrong number of arguments for elementData: ' +
arguments.length);
}
},
updateWidget: function() {
var component = this;
// use HTMLWidgets.widgets to give us a list of available htmlwidget bindings
var widgets = HTMLWidgets.widgets;
// assume there might be lots, so filter for the one we want
// in this case, we want jsonedit
var widget = widgets.filter(function(widget){
return widget.name === component.name
})[0];
// get our htmlwidget DOM element
var el = this.$el.querySelector('.html-widget');
var instance = this.elementData(el, 'init_result')
widget.renderValue(
el,
this.x,
instance
);
}
},
mounted: function() {
if(typeof(this.x) === 'undefined' || this.x === null) { return }
var component = this;
// use HTMLWidgets.widgets to give us a list of available htmlwiget bindings
var widgets = HTMLWidgets.widgets;
// assume there might be lots, so filter for the one we want
// in this case, we want jsonedit
var widget = widgets.filter(function(widget){
return widget.name === component.name
})[0];
// get our htmlwidget DOM element
var el = this.$el.querySelector('.html-widget');
// get our htmlwidget instance with initialize
var instance = widget.initialize(el);
this.elementData(el, 'init_result', instance);
widget.renderValue(
el,
this.x,
instance
);
},
// updated not working since does not watch deep
// but if the expectation is that data and options are replaced completely
// then updated will trigger
updated: function() {
this.updateWidget()
},
watch: {
x: {
handler: function() {console.log('updating');this.updateWidget()},
deep: true
}
}
}
)
var app = new Vue({
el: '#app',
data: () => (%s)
})
function updateData() {
app.x.data[0].y = app.x.data[0].y.map(d => Math.random())
}
",
get_widget_data(p)
)
))
)
browsable(tl)
Gior Test
library(htmltools)
library(htmlwidgets)
library(shiny)
library(vueR)
# remotes::install_github("JohnCoene/gior")
library(gior)
data("country_data")
gi <- country_data %>%
gior() %>%
g_data(from, to, value)
# handle non-standard behaviors by some widgets
get_widget_data <- function(widget) {
as.tags(widget)[[2]]$children[[1]]
}
tl <- tagList(
vueR::html_dependency_vue(minified = FALSE),
htmlDependency(
"htmlwidgets",
packageVersion("htmlwidgets"),
src = system.file("www", package = "htmlwidgets"),
script = "htmlwidgets.js"
),
tags$div(
id = "app",
tag('html-widget', list(
giorOutput('gi'),
`:x` = 'x',
`name` = 'gior' # ideally we find a way to avoid this
))
),
tags$script(HTML(
sprintf("
Vue.component(
'html-widget',
{
props: ['x', 'name'],
template: '<div><slot></slot></div>',
methods: {
// Copied from HTMLWidgets code
// Implement a vague facsimilie of jQuery's data method
elementData: function(el, name, value) {
if (arguments.length == 2) {
return el['htmlwidget_data_' + name];
} else if (arguments.length == 3) {
el['htmlwidget_data_' + name] = value;
return el;
} else {
throw new Error('Wrong number of arguments for elementData: ' +
arguments.length);
}
},
updateWidget: function() {
var component = this;
// use HTMLWidgets.widgets to give us a list of available htmlwidget bindings
var widgets = HTMLWidgets.widgets;
// assume there might be lots, so filter for the one we want
// in this case, we want jsonedit
var widget = widgets.filter(function(widget){
return widget.name === component.name
})[0];
// get our htmlwidget DOM element
var el = this.$el.querySelector('.html-widget');
var instance = this.elementData(el, 'init_result')
widget.renderValue(
el,
this.x,
instance
);
}
},
mounted: function() {
if(typeof(this.x) === 'undefined' || this.x === null) { return }
var component = this;
// use HTMLWidgets.widgets to give us a list of available htmlwiget bindings
var widgets = HTMLWidgets.widgets;
// assume there might be lots, so filter for the one we want
// in this case, we want jsonedit
var widget = widgets.filter(function(widget){
return widget.name === component.name
})[0];
// get our htmlwidget DOM element
var el = this.$el.querySelector('.html-widget');
// get our htmlwidget instance with initialize
var instance = widget.initialize(el);
this.elementData(el, 'init_result', instance);
widget.renderValue(
el,
this.x,
instance
);
},
// updated not working since does not watch deep
// but if the expectation is that data and options are replaced completely
// then updated will trigger
updated: function() {
this.updateWidget()
},
watch: {
x: {
handler: function() {console.log('updating');this.updateWidget()},
deep: true
}
}
}
)
var app = new Vue({
el: '#app',
data: () => (%s)
})
",
get_widget_data(gi)
)
))
)
browsable(tl)
Leaflet test
note: example does not update data yet.
library(htmltools)
library(htmlwidgets)
library(shiny)
library(vueR)
library(dplyr)
library(leaflet)
get_widget_data <- function(widget) { as.tags(widget)[[2]]$children[[1]] }
# functions from leaflet example
rand_lng <- function(n = 10) rnorm(n, -93.65, .01)
rand_lat <- function(n = 10) rnorm(n, 42.0285, .01)
p <- leaflet() %>%
addTiles() %>%
addCircles(rand_lng(50), rand_lat(50), radius = runif(50, 50, 150))
tl <- tagList(
vueR::html_dependency_vue(minified = FALSE),
htmlDependency("htmlwidgets", packageVersion("htmlwidgets"), src = system.file("www", package = "htmlwidgets"), script = "htmlwidgets.js" ),
p$dependencies,
tags$div(
tags$button("update data", onclick = "updateData()")
),
tags$div(id = "app", tag('html-widget', list(leafletOutput("pl"), `:x` = 'x', `name` = 'leaflet' )) ),
tags$script(HTML(
sprintf("
Vue.component(
'html-widget',
{
props: ['x', 'name'],
template: '<div><slot></slot></div>',
methods: {
elementData: function(el, name, value) {
if (arguments.length == 2) {
return el['htmlwidget_data_' + name];
} else if (arguments.length == 3) {
el['htmlwidget_data_' + name] = value;
return el;
} else {
throw new Error('Wrong number of arguments for elementData: ' +
arguments.length);
}
},
updateWidget: function() {
var component = this;
var widgets = HTMLWidgets.widgets;
var widget = widgets.filter(function(widget){
return widget.name === component.name
})[0];
var el = this.$el.querySelector('.html-widget');
var instance = this.elementData(el, 'init_result')
widget.renderValue(
el,
this.x,
instance
);
}
},
mounted: function() {
var component = this;
var widgets = HTMLWidgets.widgets;
var widget = widgets.filter(function(widget){
return widget.name === component.name
})[0];
var el = this.$el.querySelector('.html-widget');
var instance = widget.initialize(el);
this.elementData(el, 'init_result', instance);
widget.renderValue( el, this.x, instance);
},
updated: function() {
this.updateWidget()
},
watch: {
x: {
handler: function() {console.log('updating');this.updateWidget()},
deep: true
}
}
}
)
var app = new Vue({
el: '#app',
data: () => (%s)
})
function updateData() {
console.log('data', app)
}
",
get_widget_data(p)
)
))
)
browsable(tl)
@FrissAnalytics nice to see that leaflet
works. In a shiny
context I'd like to see if we can take advantage of the proxy
methods provided by leaflet
.
yep! Tried to get the leaflet data in the example above. Turned out it's a pretty complicated object with a highly non-trivial structure, unless you are familiar how the shiny leaflet implementation works.
Would be much cleaner to have access to the proxy and to manipulate the widget instance from server.R.
@FrissAnalytics Here is a little more complicated example with a plotly
htmlwidget in Vuetify table cells. It is a mess but does prove that it can be done.
# plotly htmlwidgets in Vuetify table cells
library(htmltools)
library(vueR)
library(plotly)
library(dplyr)
iris_tbl <- iris %>%
group_by(Species) %>%
summarize(
plot_data = get_widget_data(
plot_ly(x = ~Sepal.Width, y = ~Sepal.Length, data = cur_data())
)
)
get_widget_data <- function(widget) { htmltools::as.tags(widget)[[2]]$children[[1]] }
tl <- tagList(
plot_ly()$dependencies,
htmlwidgets:::getDependency("plotly"),
vueR::html_dependency_vue(minified = FALSE),
tags$head(
tags$link(href="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.min.css", rel="stylesheet"),
tags$script(src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.js")
),
tags$div(
id = "app",
tag(
"v-data-table",
list(
`:headers` = "headers",
`:items` = "tbl_data",
tag(
"template",
list(
`v-slot:item.plot_data` = "{ item }",
tag(
"html-widget",
list(
style = "width: 100%;",
`:x` = "JSON.parse(item.plot_data).x",
`name` = "plotly",
tags$div(class="html-widget plotly", style="width:100%; height:400px;", `:id`="'cell-' + item.Species")
)
)
)
)
)
)
),
tags$script(HTML(
"
Vue.component(
'html-widget',
{
props: ['x', 'name'],
template: '<div class=\"html-widget\"><slot></slot></div>',
methods: {
// Copied from HTMLWidgets code
// Implement a vague facsimilie of jQuery's data method
elementData: function(el, name, value) {
if (arguments.length == 2) {
return el['htmlwidget_data_' + name];
} else if (arguments.length == 3) {
el['htmlwidget_data_' + name] = value;
return el;
} else {
throw new Error('Wrong number of arguments for elementData: ' +
arguments.length);
}
},
updateWidget: function() {
// see comments in mounted which is nearly identical except in update we do not initialize or attach
// initial data to the element. we could clean this up and make one function.
var component = this
// use HTMLWidgets.widgets to give us a list of available htmlwidget bindings
var widgets = window.HTMLWidgets.widgets
// assume there might be lots, so filter for the one we want
// in this case, we want jsonedit
var widget = widgets.filter(function(widget){
return widget.name === component.name
})[0]
// get our htmlwidget DOM element
var el = this.$el.querySelector('.html-widget');
var instance = this.elementData(el, 'init_result')
if(typeof(instance) === 'undefined') {
// get our htmlwidget instance with initialize
instance = widget.initialize(el);
this.elementData(el, 'init_result', instance);
widget.renderValue(
el,
this.x,
instance
)
}
widget.renderValue(
el,
this.x,
instance
)
}
},
mounted: function() {
if(typeof(this.x) === 'undefined' || this.x === null) { return }
var component = this;
// use HTMLWidgets.widgets to give us a list of available htmlwiget bindings
var widgets = HTMLWidgets.widgets;
// assume there might be lots, so filter for the one we want
// in this case, we want jsonedit
var widget = widgets.filter(function(widget){
return widget.name === component.name
})[0];
// get our htmlwidget DOM element
var el = this.$el.querySelector('.html-widget');
// get our htmlwidget instance with initialize
var instance = widget.initialize(el);
this.elementData(el, 'init_result', instance);
widget.renderValue(
el,
this.x,
instance
);
},
// updated not working since does not watch deep
// but if the expectation is that data and options are replaced completely
// then updated will trigger
updated: function() {
this.updateWidget()
},
watch: {
x: {
handler: function() {console.log('updating');this.updateWidget()},
//deep: true
}
}
}
)
"
)),
tags$script(HTML(
sprintf(
'
const app = new Vue({
el: "#app",
vuetify: new Vuetify(),
data: () => ({
headers: %s,
tbl_data: %s
}),
});
',
jsonlite::toJSON(lapply(colnames(iris_tbl), function(x){list(text=x,value=x)}), auto_unbox=TRUE),
jsonlite::toJSON(iris_tbl, auto_unbox = TRUE)
)
))
)
browsable(tl)
@FrissAnalytics I probably should have started with a simpler example and then built from there. Here is a Vuetify data table with iris
data.
# plotly htmlwidgets in Vuetify table cells
library(htmltools)
library(vueR)
tl <- tagList(
vueR::html_dependency_vue(minified = FALSE),
tags$head(
tags$link(href="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.min.css", rel="stylesheet"),
tags$link( href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900", rel="stylesheet"),
tags$link(href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css", rel="stylesheet"),
tags$script(src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.js")
),
tags$div(
id = "app",
ref = "app",
tag(
"v-app",
list(
tag(
"v-main",
list(
tag(
"v-data-table",
list(
`:headers` = "headers",
`:items` = "tbl_data"
)
)
)
)
)
)
),
tags$script(HTML(
sprintf(
'
const app = new Vue({
el: "#app",
vuetify: new Vuetify(),
data: () => ({
headers: %s,
tbl_data: %s
}),
});
',
jsonlite::toJSON(lapply(colnames(iris), function(x){list(text=toupper(x),value=x)}), auto_unbox=TRUE),
jsonlite::toJSON(iris, auto_unbox = TRUE)
)
))
)
browsable(tl)