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.