Open timelyportfolio opened 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
@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.
I think this is fairly harmless and will not cause negative side effects. Could we add
model
to thehtmlwidget
so that it is accessible through theHTMLWidgets.find()
mechanism? See https://github.com/timelyportfolio/anyhtmlwidget/commit/79804a58c00efdb296c93a5ada14399f1474e28b. Looks like the attachedmodel
will need to be aPromise
since render is async.Here is a quick example.
onRender