Skip to content

vue htmlwidget container #11

Open
Open
@timelyportfolio

Description

@timelyportfolio

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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions