Search code examples
javascriptpythontornado

Embedding multiple pieces of JavaScript via a Tornado UIModule


I'm working on a Python package that uses Tornado to send data to the browser for visualization. In order to do this, I want the users to be able to write multiple arbitrary modules for the server to render together on a single page -- including each module's own JavaScript.

However, by default, the Tornado's UIModule class's embedded_javascript() method only appends JavaScript to <script>...</script> once per module class. I'm hoping there is a simple way to embed multiple pieces of JS, one for every UIModule (or another way to get the same effect).

Here's a minimal example of what I'm talking about:

import tornado.ioloop
import tornado.web
import tornado.template

class Element(tornado.web.UIModule):
    '''
    Module to add some custom JavaScript to the page.
    '''
    def render(self, element):
        self.js_code = element.js_code
        return ""

    def embedded_javascript(self):
        return self.js_code

class InterfaceElement(object):
    '''
    Object to store some custom JavaScript code.
    '''
    def __init__(self, js_code):
        '''
        Args:
            js_code: Some JavaScript code in string form to add to the page.
        '''
        self.js_code = js_code


class MainPageHandler(tornado.web.RequestHandler):
    def get(self):
        elements = self.application.modules
        self.render("uitest_template.html", app_name="Testing", elements=elements)


class ThisApp(tornado.web.Application):
    def __init__(self, modules):
        self.modules = modules
        main_handler = (r'/', MainPageHandler)
        #settings = {"ui_modules": {"Element": Element}}
        settings = {"ui_modules": {"Element": Element},
                    "template_path": "ui_templates"}
        super().__init__([main_handler], **settings)

# Create two objects with some custom JavaScript to render
module_1 = InterfaceElement("var a = 1;")
module_2 = InterfaceElement("var b = 2;")

app = ThisApp([module_1, module_2])
app.listen(8888)
tornado.ioloop.IOLoop.instance().start()

And the template for uitest_template.html is just

<!DOCTYPE html>
<head>
    <title> Hello World </title>
</head>
<body>
    {% for element in elements %}
        {%module Element(element) %}
    {% end %}
</body>

The rendered page then includes a <script> tag in body that is:

<script type="text/javascript">
//<![CDATA[
var b = 2;
//]]>
</script>

And what I want is:

<script type="text/javascript">
//<![CDATA[
var a = 1;
var b = 2;
//]]>
</script>

Or something like it. Any ideas?

Added - my solution

Based on the answer below, here's how I ended up handling it:

import tornado.ioloop
import tornado.web
import tornado.template


class InterfaceElement(object):
    include_js = [] # List of .js files to include
    js_code = '' # JavaScript string to include

    def __init__(self, include_js=[], js_code=''):
        self.include_js = include_js
        self.js_code = js_code

class MainPageHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("modular_template.html", 
                    includes=self.application.include_js, 
                    scripts=self.application.js_code)


class ThisApp(tornado.web.Application):
    def __init__(self, modules):
        # Extract the relevant info from modules:
        self.modules = modules
        self.include_js = set()
        self.js_code = []
        for module in self.modules:
            for include_file in module.include_js:
                self.include_js.add(include_file)
            if module.js_code != '':
                self.js_code.append(module.js_code)


        main_handler = (r'/', MainPageHandler)
        settings = {"template_path": "ui_templates",
                    "static_path": "ui_templates"}
        super().__init__([main_handler], **settings)

module_1 = InterfaceElement(js_code="var a = 1;")
module_2 = InterfaceElement(include_js=["test.js"], js_code="var b = 1;")

app = ThisApp([module_1, module_2])
app.listen(8888)
tornado.ioloop.IOLoop.instance().start()

Which goes with the following template:

<!DOCTYPE html>
<head>
    <title> Hello world </title>
</head>
<body>
    <!-- Script includes go here -->
    {% for file_name in includes %}
        <script src="/static/{{ file_name }}" type="text/javascript"></script>
    {% end %}
    <script type="text/javascript">
        // Actual script snippets go here.
        {% for script in scripts %}
            {% raw script %}
        {% end %}
    </script> 

</body>

Solution

  • embedded_javascript and related methods are (effectively) class-level methods; they must return the same value for any instance of the class. (They're intended to be a kind of dependency-management system, so you can load a piece of javascript only on pages that include a module that needs it)

    The only thing that is allowed to vary per instance is the output of render(), so to embed multiple pieces of javascript you should include the script tag in the result of your render() method.