loading...

Hyperapp with Pug templates

johnkazer profile image John Kazer Updated on ・4 min read

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:

  1. Clear functional approach to business logic
  2. State-driven UI
  3. Centralised state and no stateful components (easy 'undo' option and perfect for quick and reliable development of small to medium scale apps)
  4. Events dispatched to update the state which updates the UI using virtualDOM diff
  5. 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.0",
  "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.3"
  },
  "dependencies": {
    "hyperapp": "2.0.0-beta.22",
    "pug": "^2.0.4",
    "pug-vdom": "^1.0.12"
  }
}

And here is the basic project file structure

\dist (parcel output dir)
\src
  app.pug.js (compiled pug template)
  index.html
  main.js
\views
  app.pug
compilePug.js (see the start script)
package.json

And that's it.

As you can imagine, after running "npm install", the "npm 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}
    button(id='clickMe' onclick=handler.clickMe) Click Me
    input(placeholder=placeholder onchange=[handler.updateMe,handler.targetValue])
    p #{text}

Now let's have a quick look at the main.js which defines the app:

// main.js
import { app, h } from 'hyperapp'
import { pugToView } from "./pug-to-view"

const view = pugToView(h)
const targetValue = e => e.target.value

// event handlers
const clickMe = (state, event) => ({
    ...state,
    text: state.value
})
const updateMe = (state, event) => ({
    ...state,
    value: event.target.value
})

const initialState = {
    greet: 'friends',
    placeholder: 'Write something here first',
    value: '',
    text: '',
    handler: {
        clickMe,
        updateMe,
        targetValue
    }
}

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) => state =>
  render(state, (name, props, children) =>
    h(name, props.attributes, children)
  )[0] // grabs the root 'div' element whilst adjusting the way pug-vdom deals with props compared to hyperapp

The main.js file uses the two API functions provided by Hyperapp (yes, just two!), h 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 by:

johnkazer profile

John Kazer

@johnkazer

Interested in making life easier and smarter. Amazed by the power of the modern web. solarfuture.org.uk

Discussion

markdown guide