Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inject/execute JS code to IPython notebook and forbid its further execution on page reload

I'm writing the library which has to embed javascript code to IPython notebook and execute it. The HTML/JS code looks like:

<div id="unique_id"></div>
<script>
var div = document.getElementById("unique_id");
// Do the job and get "output"
div.textContent = output;  // display output after the cell
</script>

And the python code:

from IPython import display
display.display(display.HTML(code))

The side-effect is that the javascript code is stored in the output of the cell in notebook, and every time when the page is reloaded or the notebook is opened it will run again.

Are there any way of forbidding the code to be executed on reload? Or is it possible to run the javascript code without saving it within the output?

like image 931
Vladimir Avatar asked Jan 14 '18 11:01

Vladimir


2 Answers

After running into the same issue of Javascript executing on every notebook open, I adapted @Vladimir's solution to a more general form:

  • Use fresh unique IDs on every render (since old ID is saved with the HTML output of the notebook).
  • No polling to determine when HTML element is rendered.
  • Of course, when the notebook is closed, no HTML modifications done by JS are saved.

Key Insight: Replace Cell Output

from IPython.display import clear_output, display, HTML, Javascript
# JavaScript code here will execute once and will not be saved into the notebook.
display(Javascript('...'))
# `clear_output` replaces the need for `display_id` + `update`
clear_output()
# JavaScript code here *will* be saved into the notebook and executed on every open.
display(HTML('...'))

Making it Work

The challenge here is that the HTML and Javascript blocks can be rendered out of order, and the code which manipulates the HTML element needs to only execute once.

import random
from IPython.display import display, Javascript, HTML, clear_output

unique_id = str(random.randint(100000, 999999))

display(Javascript(
    '''
    var id = '%(unique_id)s';
    // Make a new global function with a unique name, to prevent collisions with past
    // executions of this cell (since JS state is reused).
    window['render_' + id] = function() {
        // Put data fetching function here.
        $('#' + id).text('Hello at ' + new Date());
    }
    // See if the `HTML` block executed first, and if so trigger the render.
    if ($('#' + id).length) {
        window['render_' + id]();
    }
    ''' % dict(unique_id=unique_id)
    # Use % instead of .format since the latter requires {{ and }} escaping.
))

clear_output()

display(HTML(
    '''
    <div id="%(unique_id)s"></div>
    <!-- When this script block executes, the <div> is ready for data. -->
    <script type="text/javascript">
        var id = '%(unique_id)s';
        // See if the `Javascript` block executed first, and if so trigger the render.
        if (window['render_' + id]) {
            window['render_' + id]();
        }
    </script>
    ''' % {'unique_id': unique_id}
))

To keep the notebook clean, I would put this plumbing code into a separate .py file and import it from Jupyter.

like image 150
mxxk Avatar answered Sep 28 '22 07:09

mxxk


I've figured out the hack.

The trick is to use update=True argument of the IPython.display.display() which will replace the output with a new one (see here for an example).

So what is needed to be done: first output javascript that does the job, and then waits until the div with a certain ID is created, to fill it with the output. Once this display() is called, we could call display a second time updating the first one with the actual HTML with the div. So the javascript code once finished will fill it with the results, but the code itself will not be saved.

Here's the test code:

First, define the callback function (it looks like, it is important here to display it as HTML("<script> ... </script>") rather than Javascript(...)):

from IPython.display import display, HTML, Javascript

js_getResults = """<script>
function getResults(data, div_id) {
    var checkExist = setInterval(function() {
       if ($('#' + div_id).length) {
          document.getElementById(div_id).textContent = data;

          clearInterval(checkExist);
       }
    }, 100);    
};
</script>"""

display(HTML(js_getResults))

And then execute the update trick in one cell:

js_request = '$.get("http://slow.server/", function(data){getResults(data, "unique_id");});'
html_div = '<div id="unique_id">Waiting for response...</div>'

display(Javascript(js_request), display_id='unique_disp_id')
display(HTML(html_div), display_id='unique_disp_id', update=True)

After the callback of get() is executed, the content Waiting for response... will be replaced with the output from the server.

like image 27
Vladimir Avatar answered Sep 28 '22 06:09

Vladimir