ramnathv / htmlwidgets

HTML Widgets for R
http://htmlwidgets.org
Other
790 stars 207 forks source link

Allow access to object returned by renderValue #19

Closed ramnathv closed 8 years ago

ramnathv commented 10 years ago

The basic idea here (thanks @timelyportfolio) is to provide users a mechanism to access the return value of renderValue so that users can inject additional behavior using javascript. Since all instances of a widget have a unique id and this is accessible to renderValue, I believe we can use the id as key to identify the widget.

The easiest way to do this probably is to create a HTMLWidgets.obj object and push the output of renderValue with the widget id as key.

@jcheng5 @jjallaire Your thoughts on this? If the implementation is easy, I think it makes sense to do this as it will open up a wider range of use cases.

timelyportfolio commented 10 years ago

here was my naive unskilled attempt https://github.com/timelyportfolio/htmlwidgets/commit/8ee84fd01c3e19684f2732c6b4817934a767a879. I will be happy to submit a pull, but fear my incompetence will cause widespread damage :)

timelyportfolio commented 10 years ago

Consider these lines from a very basic widget. spin.js does not allow configuration ex-post like knob anywhere I can see, so I am forced to remove the nodes and rerender with each change. If I could access the already rendered spinner, then I could just change config.

jjallaire commented 10 years ago

You can return an arbitrary value from initialize which will then be passed to you as the last parameter of both resize and renderValue. This is basically a mechanism for per-widget instance data that you can access from rendering. See my jjallaire/networkD3 package for an example (there I return the D3 force layout as the instance data)

Does that help?

On Friday, August 8, 2014, timelyportfolio notifications@github.com wrote:

Consider these lines https://github.com/timelyportfolio/htmlwidgets_spin/blob/master/inst/htmlwidgets/spin.js#L4-L13 from a very basic widget. spin.js does not allow configuration ex-post like knob anywhere I can see, so I am forced to remove the nodes and rerender with each change. If I could access the already rendered spinner, then I could just change config.

— Reply to this email directly or view it on GitHub https://github.com/ramnathv/htmlwidgets/issues/19#issuecomment-51615093.

timelyportfolio commented 10 years ago

Not sure it helps in this case, but very good to know. Going to bang my head on keyboard, and I will report back.

ramnathv commented 10 years ago

@jjallaire I think @timelyportfolio is looking for a mechanism that will allow him to manipulate the widget directly using javascript, outside of renderValue. I have some use cases for this too. For example, the datamaps js library allows users to define their own plugins. But for the plugin to be activated, it needs to be added to a map instance that was created. Now without access to the map object returned by renderValue, I will not be able to do this.

Note that this feature is more for intermediate-to-advanced users who are going to write javascript to customize widget outputs further.

timelyportfolio commented 10 years ago

Yes, in a static context so outside of a shiny context, a way to access whatever we created. I will try to assemble a list of potential uses, but this spin.js to stop() is one and in terms of a port of rCharts dimple we will need this for users to configure after creation/render. As far as I now there is no way using spin.js or dimple to get these created objects.

timelyportfolio commented 10 years ago

Is there an issue, design flaw, or problem adding this to HTMLWidgets.widgets[...] especially in the static context as I have done in the example?

jcheng5 commented 10 years ago

I would just attach the object to the element itself using an expando property:

renderValue: function(el, data ) {
  var spin = new Spinner(data);
  // do stuff with spin
  el.spinner = spin; // Attach spinner to element for future use
}

Then later:

var spin = document.getElementById("whatever").spinner;
ramnathv commented 10 years ago

That is neat @jcheng5. But since widget id's are randomly generated, how would one know what element to access?

timelyportfolio commented 10 years ago

also, might be a good idea to use consistent property name such as returnWidget

jcheng5 commented 10 years ago

You can get the widget element however you want--by CSS class, dynamically in an event handler, whatever. It really depends on what the scenario is. For example, if you want to stop the spinner on click:

$(document).on("click", ".spinner", function(e) {
  var spin = e.target.spinner;
  spin.stop();
});
timelyportfolio commented 10 years ago

but say we have three ajax calls each represented by a spinner, how would we know which pairs with which? I guess we could just pass an identifier in config or data.

jcheng5 commented 10 years ago

Maybe I'm not understanding something. How would having access to the return value of renderValue help you with that problem either?

timelyportfolio commented 10 years ago

if in markdown I create a widget and then subsequently create another widget that I would like to bind to the other widget, how might I accomplish that? It seems I need a known identifier and the return value.

jcheng5 commented 10 years ago

I'm still not exactly sure we're on the same page, but: If you had two spinners:

var spinners = $(".spinner");
var spinner1 = spinners[0].returnValue;  // or .spinner or whatever
var spinner2 = spinners[1].returnValue;

I have to run to a lunch meeting but will be happy to continue this discussion later. Although at this point I wonder if a vchat on Monday might be more productive?

ramnathv commented 10 years ago

@jcheng5 I think a vchat would be good.

jcheng5 commented 10 years ago

My Monday is pretty open, anytime during business hours in the Pacific time zone.

jcheng5 commented 10 years ago

OK I think I understand what you guys are talking about now--not the widget author, but the user of the widget library wanting to customize the widget further? In that case, I think we certainly need to allow the user to provide the widget's ID, right? (with the default being a randomly generated one, of course)

ramnathv commented 10 years ago

@jcheng5 Exactly. Either to do some post rendering manipulation, or say to trigger a link between two widgets.

timelyportfolio commented 10 years ago

For reference, id is currently set here in htmlwidgets.

I consider charts a very limited subset of htmlwidgets, but as an example with rCharts, I was able to accomplish in one of 2 ways:

1 - Set an id as a parameter in the rCharts configuration; see lines where dom was set in a similar random strategy in initialize 2 - Use afterScript with mustache/whisker to do {{chartId}}. However this does not work as well with multiple widgets, but allows the user a last chance to manipulate, configure, set event handling, etc.

Then to possibly make this idea more tangible, I would love to remake this tags -built website ( code and site ) with all htmlwidgets.

timelyportfolio commented 10 years ago

A simple test would be

#now do an example with a knob talking to a spin
library(htmltools)
library(htmlwidgets)
#devtools::install_github("timelyportfolio/htmlwidgets_spin")
library(spin)
library(knob)

h <- tags$html(
  tags$div(id = "spin1", style = "width:25%;height:150px;display:inline-block"
    ,tags$h3("Spin To Adjust")
    ,spin(position = "relative", width = "100px", height = "100px")
  )
  ,tags$div(id = "spin2", style = "width:25%;height:150px;display:inline-block"
    ,tags$h3("Spin Not To Adjust")
    ,spin(position = "relative", width = "100px", height = "100px")
  )
  ,tags$div(
    tags$h3("Adjust # of Lines")
    ,knob('Knob Talks to Spin', 14, 0, 20, angleArc = 250, angleOffset = -125, 
          fgColor = "#66CC66",
    )
  )
  ,tags$script(
"
//override knob render value
//since no way currently to do #! js code !# or use JSONfn
HTMLWidgets.widgets[1].renderValue = function(el,data){
  /*var knobData = $('script[data-for = ' + $('.knob')[0].id + ']')
  var dataObj = JSON.parse(knobData.text());
  */
  data.change = function (v) {
    $('.spin')[0].spin.opts.lines = v;
     HTMLWidgets.widgets[0].renderValue(
       $('.spin')[0], $('.spin')[0].spin.opts
     )
  }
  //knobData.text(JSON.stringify(data));
  $('.knob').trigger('configure',data);
  $('.knob').val(data.value).trigger('change');
};

"
  )
)
html_print(h)
timelyportfolio commented 10 years ago

changed the code above to actually accomplish the objective but this has the unfortunate effect of overriding renderValue on all the knobs

timelyportfolio commented 10 years ago

After inspecting the code more closely, it appears we can addEventListener("DOMContentLoaded', function(){ do something after widget render}) as in

#now do an example with a knob talking to a spin
library(htmltools)
library(htmlwidgets)
library(spin)
library(knob)

h <- tags$html(
  tags$div(id = "spin1", style = "width:25%;height:150px;display:inline-block"
    ,tags$h3("Spin To Adjust")
    ,spin(position = "relative", width = "100px", height = "100px")
  )
  ,tags$div(id = "spin2", style = "width:25%;height:150px;display:inline-block"
    ,tags$h3("Spin Not To Adjust")
    ,spin(position = "relative", width = "100px", height = "100px")
  )
  ,tags$div(
    tags$h3("Adjust # of Lines")
    ,knob('Knob Talks to Spin', 14, 0, 20, angleArc = 250, angleOffset = -125, 
          fgColor = "#66CC66",
    )
  )
  ,tags$div(
    tags$h3("Do Nothing")
    ,knob('Knob Talks to Spin', 14, 0, 20, angleArc = 250, angleOffset = -125, 
          fgColor = "#66CC66",
    )
  )
  ,tags$script(
"
document.addEventListener('DOMContentLoaded',function (){
  $('.knob:first').trigger('configure', {
      'change': function (v) {
         $('.spin')[0].spin.opts.lines = v;
         HTMLWidgets.widgets[0].renderValue(
           $('.spin')[0], $('.spin')[0].spin.opts
         )
      }
  });
}
)

"
  )
)
html_print(h)
jcheng5 commented 10 years ago

@timelyportfolio That snippet works, but, ugh... it's so fragile. To support this properly it seems like we would want to:

  1. Allow the user to (optionally) specify the widget ID
  2. Provide an easy way for code to run after all the widgets have been loaded
  3. Make it easy to pass new values to be rendered from JS to individual widgets (like HTMLWidgets.renderValue(el, data) and have htmlwidgets figure out the right binding to use); we can do this by attaching the binding to the element at the time it is bound

Does that sound right to everyone?

ramnathv commented 10 years ago

I think the elementId argument and the ability to store instance values on the DOM element should close this issue. I will wait till @timelyportfolio can test this with the examples he has been playing with.

timelyportfolio commented 10 years ago

Not optimal, but I think it works for now. Unfortunately, I don't have any better ideas. I think this insight will come as I do this over and over with different widgets. I am satisfied for now that we can do some amazing things.

JT85 commented 9 years ago

@jcheng5 Wouldn't it be nice to have a function like knobInput for shiny which can be used as an input controller for a shiny app like for instance the ipod wheel: http://anthonyterrien.com/knob/? Do you think this is even feasible? This is probably not the right place for these kind of suggestions/questions. Let me know where I can do this in the future please.

ramnathv commented 9 years ago

@JT85 input widgets are something that we will be thinking about in the future. Currently, the focus is on output html widgets.

jcheng5 commented 9 years ago

@JT85 It's quite straightforward to do this today without help from htmlwidgets--see this article.

For htmlwidgets, input widgets are less of a win than output widgets, because the main advantage htmlwidgets gives you is to author a widget once and use it in static HTML pages, static R Markdown documents, RStudio Viewer, and Shiny. For input widgets, it doesn't make much sense to have an input widget in any of those contexts but Shiny (unless the widget user is going to write some javascript, in which case, it's not that clear why you'd need an R wrapper in the first place).

I'm not saying htmlwidgets wouldn't be helpful at all for input widgets--just that it'd be far, far less helpful than it is for output widgets.

jcheng5 commented 8 years ago

I think we can close this, right? Because we have onRender and onStaticRenderComplete?

timelyportfolio commented 8 years ago

Would agree can close this. Was a fun discussion.

timelyportfolio commented 8 years ago

didn't mean to close, I'll let y'all decide. Pushed the wrong button.

timelyportfolio commented 8 years ago

going to close since no other comments or objections