Rick
Rick

Reputation: 45321

Access an element by id inside Jupyter notebook _repr_javascript_ method

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

Answers (2)

ChrCury78
ChrCury78

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

Christoph Burschka
Christoph Burschka

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:

  1. Page receives data from the server.
  2. parses the HTML, including the <script> element.
  3. executes the script, seemingly putting the 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.)
  4. only then inserts the HTML content in the document.

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

Related Questions