keller-mark / anyhtmlwidget

Bringing core concepts from anywidget to R
https://keller-mark.github.io/anyhtmlwidget/
Other
10 stars 1 forks source link

Add `model` to `htmlwidget` #28

Open timelyportfolio opened 1 month ago

timelyportfolio commented 1 month ago

I think this is fairly harmless and will not cause negative side effects. Could we add model to the htmlwidget so that it is accessible through the HTMLWidgets.find() mechanism? See https://github.com/timelyportfolio/anyhtmlwidget/commit/79804a58c00efdb296c93a5ada14399f1474e28b. Looks like the attached model will need to be a Promise since render is async.

Here is a quick example.

library(htmlwidgets)
library(anyhtmlwidget)

esm <- "
function render({ el, model }) {
  el.style.border = '4px solid red';
  let count = () => model.get('count');
  let btn = document.createElement('button');
  btn.innerHTML = `count is ${count()}`;
  btn.addEventListener('click', () => {
    model.set('count', count() + 1);
    model.save_changes();
  });
  model.on('change:count', () => {
    btn.innerHTML = `count is ${count()}`;
  });
  el.appendChild(btn);
}
export default { render };
"

widget <- AnyHtmlWidget$new(
  .esm = esm,
  .mode = "static",
  .height='400px',
  count = 1
)

onRender

htmlwidgets::onRender(
  widget$render(),
  htmlwidgets::JS("
async function() {
  const model = await this.model;
  model.on('change:count', function(evt) {console.log(evt.detail)})
}
  ")
)

image

keller-mark commented 1 month ago

I am not sure I understand the use case. Can you elaborate on the use case / what cannot be achieved without this change? I am not familiar with HTMLWidgets.find() or htmlwidgets::onRender

timelyportfolio commented 1 month ago

@keller-mark very fair question. I am still trying to wrap my head around all the different contexts/environments in which anywidget/anyhtmlwidget will operate. For now, I am focused on the one that I know best which is traditional htmlwidgets. I believe one of the underlying principles is the ability to access/set/sync state in all contexts. Let's say for instance with the simple button example in an htmlwidget/static context that we would like something else (in this case a simple div) to know the state of the button widget, and we also do not want to impose any understanding of this one or multiple interested parties in the widget render method. I think this is consisent with the little I know about traitlets and also the other modes which allow access to state from outside the widget (even outside of JavaScript).

Also, I will caveat all of this discussion with I could very easily be missing something, and if so I am very sorry for my ignorance.

So we produce the button widget as in the example in static mode. The widget renders. Now when count changes we want the new count to update in the div outside of the widget. How would we accomplish this? I think we need access to the model, but currently I do not see any way to access the model from outside in an htmlwidgets static context.

library(htmlwidgets)
library(anyhtmlwidget)

esm <- "
function render({ el, model }) {
  el.style.border = '4px solid red';
  let count = () => model.get('count');
  let btn = document.createElement('button');
  btn.innerHTML = `count is ${count()}`;
  btn.addEventListener('click', () => {
    model.set('count', count() + 1);
    model.save_changes();
  });
  model.on('change:count', () => {
    btn.innerHTML = `count is ${count()}`;
  });
  el.appendChild(btn);
}
export default { render };
"

widget <- AnyHtmlWidget$new(
  .esm = esm,
  .mode = "static",
  .height='400px',
  count = 1
)

htmltools::browsable(
  htmltools::tagList(
    htmltools::tags$div(id = "tracker", "widget count??"),
    widget$render()
  )
)

Usually, htmlwidgets can be accessed/found through HTMLWidgets.find() which operates similarly to jQuery/$ or document.querySelector. HTMLWidgets.find() will return {renderValue, resize, ...} which is the return value from the factory function. In many cases htmlwidgets will provide accessors or helpful methods/functions in addition to renderValue and resize.

anyhtmlwidget is a little different in that it is one of the few (maybe only) htmlwidgets with an async renderValue. The only way we really will know that anyhtmlwidget has completed renderValue will be through htmlwidgets::onRender() which provides the return value from the factory function in this. If we know that an anyhtmlwidget has rendered, we could also access the model through HTMLWidgets.find(#el.id).model.

With the proposed change, we could do something like below.

htmltools::browsable(
  htmltools::tagList(
    htmltools::tags$div(id = "tracker", "waiting for widget to render..."),
    htmlwidgets::onRender(
      widget$render(),
      htmlwidgets::JS(
"
async function() {
  const model = await this.model;
  document.getElementById('tracker').innerText = model.get('count')
  model.on('change:count', function(evt) {
    document.getElementById('tracker').innerText = evt.detail
  })
}
"
      )
    )
  )
)

How else might we achieve a similar result? Thanks so much for your consideration.