Hello Transcrypt

This step is gonna get freaky.

Perhaps you noticed that we wrote the counter in JS. Perhaps you noticed that there will therefore be a duplication in implementation logic: Python for the first render and JavaScript for once it is hydrated.

Let’s write the counter once, in Python, and have Transcrypt generate the JS for Hyperapp.

Counter, In Python

Make a file called counter.py:

from hyperappHtml import button, div, h3, p

state = dict(count=10)

actions = dict(
    down=lambda value: lambda s: dict(count=s.count - value),
    up=lambda value: lambda s: dict(count=s.count + value),
)


def view(st, ac):
    return div({}, [
        h3({}, 'Welcome Counter'),
        p({}, 'Current Counter: ' + str(st.count)),
        button(dict(onclick=lambda: ac.down(1)), "-"),
        button(dict(onclick=lambda: ac.up(1)), "+")
    ])


try:
    window.hyperapp.app(
        state,
        actions,
        view,
        document.getElementById("counter")
    )
except NameError:
    pass

This is a Python file that will be run through Transcrypt to generate the moral equivalent of counter.js which we just saw. If you use a smart editor, you’ll see red squigglies complaining that window and document are undefined.

And that’s true. Those are symbols which don’t exist in Python. They spring into existence when run in JavaScript.

Note

Need to find a way to teach the editor that those symbols can exist and give them a structure.

In the first line, you see a Python import of hyperappHtml. What’s that? The hyperapp/html “package” is really a single, simple file. This tutorial just converted it to Python, to allow us to work towards a single “template” which works both on initial render (in Python) and browser-side (in JS).

And here’s that file.

HTML Helper

Create hyperappHtml.py with the following:

try:
    h = window.hyperapp.h
except NameError:
    def h(name, attributes, children):
        # TODO Implement h in Python
        pass


def vnode(name):
    def f(attributes, children):
        return h(name, attributes, children)

    return f


def h3(attributes, children):
    return vnode("h1")(attributes, children)


def h4(attributes, children):
    return vnode("h1")(attributes, children)


def button(attributes, children):
    return vnode("button")(attributes, children)


def div(attributes, children):
    return vnode("div")(attributes, children)


def p(attributes, children):
    return vnode("div")(attributes, children)

As you can see, this is just a proof-of-concept:

  • It doesn’t include all the tags
  • It can’t be used 100% in Python, because it still needs the h function from Hyperapp (only 60 or so lines, but dense in concepts)

Still, it allows us to use Python tooling (e.g. type hinting) when writing our views, rather than rely on some alien symbol representing a JavaScript function.

Loading the Generated JS

Transcrypt is going to read our Python and generate JS. We need to load that generated JS. Transcrypt generated ESM-friendly JS, so we can do an import. Change index.tpl to (a) remove loading hyperHtml.js, (b) remove loading counter.js, and (c) add the new <script:

<html>
<head>
    <title>Transcrypt Hyperapp</title>
    <link rel="icon" type="image/x-icon"
          href="https://www.python.org/static/favicon.ico">
    <link rel="stylesheet" href="index.css"/>
    <script src="hyperapp.js"></script>
    <script src="hyperappHtml.js"></script>
</head>
<body>
<h1>Transcrypt + Hyperapp Demo</h1>
<p>This sample application has a counter. On first load, the counter
    renders, server-side, the HTML. After that, client-side events
    re-render the counter.</p>
<div id="counter">
    <div>
        <h3>Counter Demo</h3>
        <p>Current Count: {{ state['count'] }}</p>
        <button>-</button>
        <button>+</button>
    </div>
</div>
<script src="counter.js"></script>
</body>
</html>

Run Transcrypt

Now it’s time to get Transcrypt to do its magic. Let’s have run_server.py handle it, so we can run it any time counter.py changes. Even better, the browser will reload with our changes.

import bottle
from bottle import route, default_app, static_file
from bottle import template
from livereload import Server, shell

from counter import state

bottle.debug(True)

# TODO Instead of forking and running Python, extract
#   stuff from transcrypt.__main__.main
transpile = 'env/bin/transcrypt -b -m -n counter.py'


@route('/')
def index():
    return template('index', state=state)


@route('/<filepath:path>')
def server_static(filepath):
    return static_file(filepath, root='.')


if __name__ == '__main__':
    app = default_app()
    server = Server(app)
    server.watch('counter.py', shell(transpile))
    server.watch('hydrate/__target__/*', delay=2)
    server.watch('index.css')
    server.watch('counter.js')
    server.watch('index.tpl', ignore=False)
    server.serve()

In the previous step, the initial-rendering (in Python) used a different state than the hydrated count, which was in JS. Now that the counter is in Python, we can import the initial value and hand it to the template.

Future: One Template

This is all a little freaky. It’s hard to remember some times which side of the fence is governing which of the executions (initial in Python, later in JS.)

Of course, it can get a lot freakier. For example, we still have two templates:

  • The index.tpl Bottle template
  • The hyperapp/html (converted to Python) in the Hyperapp view

Wouldn’t it be great if there was just one template? That way the initial render could use the same template that was used later.

A full-port of hyperapp/html to Python is half the equation. The other is harder: porting the “h” function is also needed. It’s short but complicated. Some of the complexity could go, as a diff isn’t needed: this is the first render.