Reputation: 45321
I cannot retrieve a newly added html object using its id while inside the Jupyter output cell. How can I do it?
EDIT: I have been able to replicate the same behavior in a notebook hosted on Azure:
https://notebooks.azure.com/rickteachey/projects/sandbox/html/js_repr_id_access.ipynb
NOTE: to run this notebook, click Clone
at the top right and run it in your own Azure project/notebook.
The javacript first adds a new html button using the Jupyter API (ie, element.html()
; in context, element
refers to the Jupyter output cell <div>
).
Then the code attempts to access the button using document.getElementById()
:
class C:
def _repr_javascript_(self):
return f'''
element.html(`<button id="clearBtn">Clear</button>`)
var x = document.getElementById("clearBtn")
alert(x)
'''
C()
EXPECTED BEHAVIOR: The alert should show a stringified version of the clearBtn
html button object.
ACTUAL BEHAVIOR: The alert shows a null object, which means the script fails to grab the clearBtn - even though I can see it in the DOM when I look at the source.
It's possible I'm using the API incorrectly. If so, how am I supposed to do this?
Another weird issue: when I look at the same notebook on nbviewer, the alert pops up the clearBtn html object as expected. It does NOT behave this way on my local machine(s), or on Azure. Should I report this as a bug?
https://nbviewer.jupyter.org/urls/dl.dropbox.com/s/dwfmnozfn42w0ck/access_by_id_SO_question.ipynb
Upvotes: 4
Views: 3113
Reputation: 437
I see two ways of doing it, by returning HTML or Javascript:
class C:
def _repr_javascript_(self):
alert = "alert('x');"
return f'''
element.html(`<button onclick="{alert}" id="clearBtn">Clear</button>`)
'''
C()
or
class C:
def _repr_html_(self):
return f'''
<button onclick="x()" id="clearBtn">Clear</button>
<script>
{{
var bt = document.getElementById("clearBtn");
bt.onclick = function(){{ alert('hi'); }};;
}}
</script>
'''
C()
Upvotes: 1
Reputation: 4689
Updated answer, original below:
I've tested this now and think I worked out why the below fixes it. Specifically, when the output cell is generated and inserted in the page, it happens in the following order:
<script>
element.element
in its local scope by some means I haven't yet seen. (This is different from how it happens in the nbviewer page, where the output is already rendered, and the script gets it by using something like var element = $('#ad74eb90-4105-4cc9-83e2-37fb7e953a9f');
, which can be seen in the source.)That means that at the time the script runs, the element
is inside a detached DOM node. This can also be checked with the following:
alert(element[0].parentNode.parentNode)
-> null
alert(element[0].parentNode.outerHTML)
->
<div class="output_area">
<div class="run_this_cell"></div>
<div class="prompt output_prompt">
<bdi>Out[28]:</bdi>
</div>
<div class="output_subarea output_javascript rendered_html">
<button id="clearBtn">Clear</button>
</div>
</div>
In other words, all manipulation or traversal of the rendered output needs to go through the element
variable (such as $("#clearBtn", element)
or element.find("#clearBtn")
or even element[0].querySelector("#clearBtn")
). It can't go through document
, because the element isn't yet part of the document when the script runs.
Original answer:
This is just a vague idea: Is it possible the global document
in this context is not actually the same document as the one element
is in? There might be some iframe stuff going on in the editor, which might explain why it works after being rendered to a single page by nbviewer but not before. (Elements inside iframes are not part of the parent document, even though the browser's DOM viewer nests them as if they were.)
I would suggest using the element
you already have to find the button you just inserted in it, instead of trying to find it from the document
. (I'm not sure what kind of object element
is, but there should be a way to get at the DOM node it's referencing and then use .querySelector("#clearBtn")
, right?)
Edit: If the element.html()
line is jQuery code, then element
is a jQuery object and element.find("#clearBtn")[0]
would find the contained button.
(This could also be done with element[0].querySelector("#clearBtn")
. Note that the return value of .find()
is itself a jQuery object, and that dereferencing [0]
on a jQuery object returns the (first) DOM element inside it.)
Upvotes: 2