Hyperapp with Pug templates
John Kazer
Posted on June 28, 2019
I recently completed an excellent course by James Moore on the basics of functional JavaScript for web apps. The app framework he created is nice but uses hyperscript to define HTML as functions. I find hyperscript to be an interesting approach and it does allow for composable UI components. But I don't really get on with it...
As I was browsing DEV, I noticed a post from @aspittel which mentioned Hyperapp. Turns out this is built along very similar lines with an option to use JSX as well as hyperscript. I imagine the new v2 has moved on quite a bit from when Ali checked it out in early 2018.
Now, the Hyperapp framework is wonderful and has a number of features I like:
- Clear functional approach to business logic
- State-driven UI
- Centralised state and no stateful components (easy 'undo' option and perfect for quick and reliable development of small to medium scale apps)
- Events dispatched to update the state which updates the UI using virtualDOM diff
- Fast, small and simple but sufficient
However, previously I've used Pug to define my UI templates. I like the retained ability to see the page structure and the clearer separation of logic and UI. Combining HTML with the business logic using hyperscript h functions doesn't sit right with me (yet?) and I find it hard to reason about a page when the layout is so abstract.
Maybe I'll come round eventually, but maybe I don't need to...
Fortunately for me, the project pug-vdom (obviously) brings a virtualDOM to Pug. So what follows is a brief intro to the very simple app I built to demo how Hyperapp can use Pug templates. See the Hyperapp pages to get a better understanding of the full range of what it does, or try their new Udemy/Packt course.
As project setup, here's the package.json. Key items to note being the start script and pug/pug-vdom dependencies (and you need Node.js version 6.4 or above).
{
"name": "hyperapp-pug",
"version": "1.0.1",
"description": "An instance of hyperapp which uses pug and pug-vdom rather than the default h functions",
"main": "main.js",
"scripts": {
"start": "node ./compilePug.js && parcel ./src/index.html"
},
"author": "John Kazer",
"license": "ISC",
"devDependencies": {
"parcel": "1.12.4"
},
"dependencies": {
"hyperapp": "2.0.12",
"pug": "2.0.4",
"pug-vdom": "1.1.2"
}
}
And here is the basic project file structure
\dist (parcel output dir)
\src
app.pug.js (compiled pug template)
index.html
main.js
pug-to-view.js
pug-vdom.js (added as a local copy to handle Hyperapp approach to textNodes)
\views
app.pug
compilePug.js (see the start script)
package.json
And that's it.
Note: There's a PR for pug-vdom to include this tweak, so perhaps in future the local copy can go away.
As you can imagine, after running "npm install", using the "npm run start" script runs a compilation process which converts the Pug view into a .js file. The compilation is defined by compilePug.js. The compiled file is included require('./app.pug.js')
by main.js and provides the virtual DOM nodes that Hyperapp needs to display the content. Then parcel runs, updates the other files in \src and starts a browser instance of \dist\index.html.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hyperapp demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id='app'></div>
<script src="./main.js"></script>
</body>
</html>
The compile process is pretty simple - a list of the templates and destinations (in this case just the one):
// compilePug.js
const vDom = require('pug-vdom')
vDom.generateFile('./views/app.pug', './src/app.pug.js', './views')
It compiles my simple Pug template:
// Need a root div to grab as the start of the view
div
// receives the variables and functions from the supplied state object
- var greeting = "Hello " + greet
p(style={color: "red"}) #{greeting}
input(size="60" placeholder=placeholder onchange=handler.updateMe)
button(id='clickMe' onclick=handler.clickMe) Click Me
p #{userText}
Now let's have a quick look at the main.js which defines the app:
// main.js
import { app, h, text } from 'hyperapp'
import { pugToView } from "./pug-to-view"
const view = pugToView(h, text)
// event handlers
const clickMe = (state, event) => ({
...state,
userText: state.value
})
const updateMe = (state, event) => ({
...state,
value: event.target.value
})
const initialState = {
greet: 'friends',
placeholder: 'Write something here first, hit \<enter\> then click the button',
value: '',
userText: '',
handler: {
clickMe,
updateMe
}
}
const node = document.getElementById('app')
app({
init: initialState,
view: view,
node: node
})
Where the helper function pugToView comes from here
import 'pug-vdom/runtime' // runtime library is required and puts 'pugVDOMRuntime' into the global scope
const render = require('./app.pug.js')
export const pugToView = (h, text) => state =>
render(
state,
(name, props, children) => h(name, props.attributes, children),
text
)[0] // grabs the root 'div' element whilst adjusting the way pug-vdom deals with props compared to hyperapp
The main.js file uses the three API functions provided by Hyperapp (yes, just three!), h
, text
and app
. These functions provide state management, virtual DOM, diffing and DOM patching when state changes.
This is a static page, so there is only a simple initial state; some initial values and the event handlers to associate with the button and input elements.
We are going to inject the app at the selected node
by providing the view
as defined by the compiled Pug template using the content
function.
The app
function pulls all this together and Hyperapp takes care of the rest. Simple declarative, functional, logic coupled to a familiar templating system!
Find the repo here.
Posted on June 28, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.