Giter Club home page Giter Club logo

vuer's Introduction

R-CMD-check CRAN status

Vue.js is a quiet, very popular JavaScript framework with an impressive set of features, a solid community, and MIT license. Don’t tell anybody, but I think I might even like it better than React. With all this, Vue deserves its own set of helpers for R, just like d3r and reactR.

vueR provides these helpers with its dependency function html_dependency_vue() and htmlwidget helper vue().

Installation

install.packages("vueR")

or for the latest if different from CRAN

remotes::install_github("vue-r/vueR")

Example

We’ll start with a recreation of the simple “Hello World” example from the Vue.js documentation. This is the hard way.

library(htmltools)
library(vueR)

browsable(
  tagList(
    html_dependency_vue(), # local and minimized by default
    tags$div(id="app","{{message}}"),
    tags$script(
    "
    var app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue!'
      }
    });
    "
    )
  )
)

vueR gives us an htmlwidget that can ease the code burden from above.

library(vueR)
library(htmltools)

# recreate Hello Vue! example
browsable(
  tagList(
    tags$div(id="app", "{{message}}"),
    vue(
      list(
        el = "#app",
        data = list(
          message = "Hello Vue!"
        )
      )
    )
  )
)

Also, please check out additional examples and experiments.

Build/Update

vueR is now part of a Github organization, and hopefully will be backed with interest by more than one (me) developer. For most vueR users, this section will not apply, but I would like to document the build/update step for new versions of Vue. In getvue.R, I created some functions for rapid download and deployment of new Vue versions. Running all of the code in getvue.R should update local minified and development versions of Vue and also update the version references in vueR.

Code of Conduct

I would love for you to participate and help with vueR, but please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

vuer's People

Contributors

d2s avatar seankross avatar timelyportfolio avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

vuer's Issues

link vue to crosstalk?

Playing around and here is a quick example. In reality, crosstalk can be subsumed by Vue as simply a shared, reactive state mechanism.

with crosstalk api

library(vueR)
library(crosstalk)
library(htmltools)

data(mtcars)
sd <- SharedData$new(mtcars)

browsable(
    tagList(
    html_dependency_vue(minified = FALSE),
    filter_select("gear", "Gears", sd, ~gear),
    filter_slider("mpg", "Miles per gallon", sd, "mpg"),
    filter_slider("cyl", "Cylinders", sd, "cyl"),
    tags$div(
      id = "app",
      tags$h3("Filter Handle"),
      "{{ filter.filteredKeys ? filter.filteredKeys : 'nothing filtered' }}",
      tags$h3("Selection Handle"),
      "{{ selection.value ? selection.value : 'nothing selected' }}"
    ),
    tags$script(HTML(
sprintf(
"
  const app = new Vue({
    el: '#app',
    data() {
      return {
        filter: new crosstalk.FilterHandle('%1$s'),
        selection: new crosstalk.SelectionHandle('%1$s')
      }
    }
  })
",
sd$groupName()
)
    ))
  )
)

with crosstalk api and plotly

library(crosstalk)
library(plotly)
library(vueR)
library(htmltools)

tx <- highlight_key(txhousing)
widgets <- bscols(
  widths = c(12, 12, 12),
  filter_select("city", "Cities", tx, ~city),
  filter_slider("sales", "Sales", tx, ~sales),
  filter_checkbox("year", "Years", tx, ~year, inline = TRUE)
)
bc <- bscols(
  widths = c(4, 8), widgets, 
  plot_ly(tx, x = ~date, y = ~median, showlegend = FALSE) %>% 
    add_lines(color = ~city, colors = "black")
)


browsable(
  tagList(
    html_dependency_vue(minified = FALSE),
    bc,
    tags$div(
      id = "app",
      tags$h3("Filter Handle"),
      "{{ filter.filteredKeys ? filter.filteredKeys : 'nothing filtered' }}",
      tags$h3("Selection Handle"),
      "{{ selection.value ? selection.value : 'nothing selected' }}"
    ),
    tags$script(HTML(
sprintf(
"
  const app = new Vue({
    el: '#app',
    data() {
      return {
        filter: new crosstalk.FilterHandle('%1$s'),
        selection: new crosstalk.SelectionHandle('%1$s')
      }
    }
  })
",
tx$groupName()
)
    ))
  )
)

library(crosstalk)
library(plotly)
library(vueR)
library(htmltools)

mh <- mtcars %>% highlight_key(~cyl)
pl <- mh %>%
  plot_ly(
    x = ~wt, y = ~mpg, text = ~cyl, mode = "markers+text", 
    textposition = "top", hoverinfo = "x+y"
  ) %>%
  highlight(on = "plotly_hover", off = "plotly_doubleclick")


browsable(
  tagList(
    html_dependency_vue(minified = FALSE),
    pl,
    tags$div(
      id = "app",
      tags$h3("Filter Handle"),
      "{{ filter.filteredKeys ? filter.filteredKeys : 'nothing filtered' }}",
      tags$h3("Selection Handle"),
      "{{ selection.value ? selection.value : 'nothing selected' }}"
    ),
    tags$script(HTML(
sprintf(
"
  const app = new Vue({
    el: '#app',
    data() {
      return {
        filter: new crosstalk.FilterHandle('%1$s'),
        selection: new crosstalk.SelectionHandle('%1$s')
      }
    }
  })
",
mh$groupName()
)
    ))
  )
)

direct (unadvisable)

library(vueR)
library(crosstalk)
library(htmltools)

data(mtcars)
sd <- SharedData$new(mtcars)

browsable(
    tagList(
    html_dependency_vue(minified = FALSE),
    filter_slider("mpg", "Miles per gallon", sd, "mpg"),
    filter_slider("cyl", "Cylinders", sd, "cyl"),
    tags$div(
      id = "app",
      "{{ $data.get()._value ? $data.get()._value : 'nothing filtered' }}"
    ),
    tags$script(HTML(
sprintf(
"
  const app = new Vue({
    el: '#app',
    data() {
      return crosstalk.group('%s').var('filterset')
    }
  })
",
sd$groupName()
)
    ))
  )
)

vue3 reactive input values

I have always wondered with both vue or mobx + Shiny how our workflows/architecture might change if Shiny JavaScript state in Shiny.shinyapp.$inputValues was reactive instead of a plain object. In earlier versions of JavaScript without proxy, this idea is very limited in potential usage since added and deleted object properties are not tracked. However, with proxy and the newest versions of mobx and vue, we can track added or effectively replace Shiny.shinyapp.$inputValues with a reactive version early in the session llife and reap the full benefits of JavaScript reactivity fairly cleanly.

Questions

  1. I have not tested real-life usage with large complex apps, but in theory it seems there is no impact. Am I missing something fundamental that would prove this idea is not as feasible as it seems.
  2. I doubt Shiny proper would ever pursue a reactive JS state since it would have to choose which reactive state engine it would use. Is there potential for Shiny to make the choice based on active community and well-tested, stable JS dependencies for Shiny to have a reactive JS engine as its input state? Which library would most likely meet the requirements? I would think choosing an existing library would be better than developing one from scratch.
  3. I feel like the concept of reactivity is quickly engrained in most Shiny developers and the reactivity concepts would translate easily to JavaScript. What would get in the way of quick understanding for R Shiny developers? What might be intimidating or difficult?
  4. How might we best communicate the improvement from incorporating reactive JS input state?
  5. What tools/tooling could we provide to ease the integration?

Code

library(htmltools)
library(vueR)
library(shiny)

# experiment with standalone vue reactivity in bare page
#   reference:
#     https://vuejs.org/v2/guide/reactivity.html
#     https://dev.to/jinjiang/understanding-reactivity-in-vue-3-0-1jni
browsable(
  tagList(
    tags$head(
      tags$script(src = "https://unpkg.com/@vue/[email protected]/dist/reactivity.global.js"),
    ),
    tags$p("we should see a number starting at 0 and increasing by one each second"),
    tags$div(id = "reporter"),
    tags$script(HTML(
"
let data = {x: 0};
let data_reactive = VueReactivity.reactive(data)  // could also use ref for primitive value
console.log(data, data_reactive)

VueReactivity.effect(() => {
  console.log(data_reactive.x)
  document.getElementById('reporter').innerText = data_reactive.x
})
setInterval(function() {data_reactive.x++}, 1000)

"
    ))
  )
)


# experiment with Shiny inputValues and vue-next
#   reference:
#     https://vuejs.org/v2/guide/reactivity.html
#     https://dev.to/jinjiang/understanding-reactivity-in-vue-3-0-1jni
ui <- tagList(
  tags$head(
    tags$script(src = "https://unpkg.com/@vue/[email protected]/dist/reactivity.global.js"),
  ),
  tags$div(
    tags$h3("Increment with JavaScript"),
    tags$span("Shiny: "),
    textOutput("reporterR", inline = TRUE),
    tags$span("JavaScript: "),
    tags$span(
      id = "reporterJS"
    )
  ),
  tags$div(
    tags$h3("Increment with R/Shiny"),
    tags$span("Shiny (used numeric input for convenience): "),
    numericInput(inputId = 'x2', label = "", value = 0),
    tags$span("JavaScript: "),
    tags$span(
      id = "reporterJS2"
    )
  ),
  tags$script(HTML(
"
$(document).on('shiny:connected', function() {

  // once Shiny connected replace Shiny inputValues with reactive Shiny inputValues
  Shiny.shinyapp.$inputValues = VueReactivity.reactive(Shiny.shinyapp.$inputValues)

  // do our counter using Shiny.setInputValue from JavaScript
  Shiny.setInputValue('x', 0) // initialize with 0
  VueReactivity.effect(() => {
    console.log('javascript', Shiny.shinyapp.$inputValues.x)
    document.getElementById('reporterJS').innerText = Shiny.shinyapp.$inputValues.x
  })
  setInterval(
    function() {
      Shiny.setInputValue('x', Shiny.shinyapp.$inputValues.x + 1) //increment by 1
    },
    1000
  )

  // react to counter implemented in Shiny
  VueReactivity.effect(() => {
    console.log('shiny', Shiny.shinyapp.$inputValues['x2:shiny.number'])
    document.getElementById('reporterJS2').innerText = Shiny.shinyapp.$inputValues['x2:shiny.number']
  })

})
"
  ))
)

server <- function(input, output, session) {
  x2 <- 0  # use this for state of Shiny counter
  output$reporterR <- renderText({input$x})

  observe({
    invalidateLater(1000, session = session)
    x2 <<- x2 + 1 # <<- or assign required to update parent
    updateNumericInput(inputId = "x2", value = x2, session = session)
  })
}

shinyApp(
  ui = ui,
  server = server,
  options = list(launch.browser = rstudioapi::viewer)
)

Error: path for html_dependency not provided

Thanks for a great package. Having fun trying some of your examples but when I try to render as a Rmd file to html I get this error. Eg:

mint <- htmlDependency(
  name = "mint-ui",
  version = "2.0.5",
  src = c(href="https://unpkg.com/mint-ui/lib"),
  script = "index.js",
  stylesheet = "style.css"
)
rmarkdown:::validate_html_dependency(mint)
#Error: path for html_dependency not provided

This has likely nothing to do with this package and is about rmarkdown/knitr, but if you could point me in the right direction to get the dependencies working for Rmd I'd appreciate it!

Michael

Shiny and vuex

I often wonder why does Shiny manage UI state from R instead of UI state (especially state that does not affect/require R) being managed in JavaScript. If we pursue this approach with vuex then JavaScript would manage all UI/frontend state and call R only when necessary allowing JS/HTML/CSS to do what it does best and more directly. This decision does not require vuex, but in a vue context I think vuex is a good established solution.

Code

# quick example of Vuex and Shiny
# reference: https://vuex.vuejs.org/guide/

library(htmltools)
library(shiny)
library(vueR)

vuex <- tags$script(src="https://unpkg.com/[email protected]/dist/vuex.js")

ui <- tagList(
  html_dependency_vue(offline = FALSE, minified = FALSE),
  tags$head(vuex),
  tags$h1("Shiny with Vue and Vuex"),
  tags$h3('Vue App'),
  tags$div(
    id = "app",
    tags$button(`@click`="increment", "+"),
    tags$button(`@click`="decrement", "-"),
    "{{ count }}"
  ),
  tags$h3("Shiny controls"),
  # add Shiny buttons to demonstrate Shiny to update Vuex state
  actionButton(inputId = "btnIncrement", label="+"),
  actionButton(inputId = "btnDecrement", label="-"),
  tags$script(HTML(
"
// first example in Vuex documentation
Vue.use(Vuex)

// make sure to call Vue.use(Vuex) if using a module system

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment: state => state.count++,
    decrement: state => state.count--
  }
})

const app = new Vue({
  el: '#app',
  store: store,
  computed: {
    count () {
      return this.$store.state.count
    }
  },
  methods: {
    increment () {
      this.$store.commit('increment')
    },
    decrement () {
    	this.$store.commit('decrement')
    }
  }
})

$(document).on('shiny:sessioninitialized', function() {
  // increment from Shiny custom message
  //   chose 'increment' but does not have to match the store mutation name
  Shiny.addCustomMessageHandler('increment', function(msg) {
    app.$store.commit('increment')
  });
  Shiny.addCustomMessageHandler('decrement', function(msg) {
    app.$store.commit('decrement')
  });
})
"
  ))
)

#browsable(ui)

server <- function(input, output, session) {
  observeEvent(input$btnIncrement, {
    session$sendCustomMessage("increment", list())
  })
  observeEvent(input$btnDecrement, {
    session$sendCustomMessage("decrement", list())
  })
}

shinyApp(
  ui = ui,
  server = server,
  options = list(launch.browser = rstudioapi::viewer)
)

vuer_7

vue Shiny input helper / template

In reactR we have a helper / template for creating Shiny-based react inputs. This article explains how to use the tools. I think providing similar tools in vueR would be helpful for R developers to allow easy integration of vue and abstract away some of the intimidating challenges of custom Shiny inputs.

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)

vue 3

Hi, sorry to bother @timelyportfolio but any attempt to configure vueR in order to make it working with the new vue 3 release ?

Best.
Fodil.

How to link vueR with leaflet?

Thanks for this great package. How can one link a reactive vueR variable to select topojson properties in leaflet? I tried the following example. The preselection incidents is working, but leaflet doesn't rerendering when I select an other property. Any Idea, how to fix that?

library(htmltools)
library(leaflet) # I installed leaflet 1.0 with devtools::install_github("timelyportfolio/[email protected]")
library(leaflet.extras)
library(vueR)

topojson <- readr::read_file('https://rawgit.com/TrantorM/leaflet-choropleth/gh-pages/examples/basic_topo/crimes_by_district.topojson')

map <- leaflet() %>% 
  setView(-75.14, 40, zoom = 11) %>% 
  addProviderTiles("CartoDB.Positron") %>% 
  addGeoJSONChoropleth(topojson,valueProperty = "{{selected}}")

ui <- tagList(tags$div(id="app",
                 tags$select("v-model" = "selected",
                             tags$option("disabled value"="","Select one"),
                             tags$option("incidents"),
                             tags$option("dist_num")),
                 tags$span("Selected: {{selected}}"),
                 tags$div(map)
        ),
        tags$script(
          "
          var app = new Vue({
          el: '#app',
          data: {
          selected: 'incidents'
          }
          });
          "
        ),
        html_dependency_vue())

browsable(ui)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.