Hyped about Hyperapp

#boscc

Gleb Bahmutov, PhD

Boston Code Camp April 2018

🔊  Dr Gleb Bahmutov PhD

C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional

Cypress.io open source E2E test runner

Goal: write an web application

Hard to pick

View + Data store

  • React + Redux

  • Angular + ng-rx

  • Vue + Vuex

Which JavaScript framework should I use next?

  • Features
  • Learning curve
  • Size and speed
  • Testability

1 kB JavaScript framework for building web applications.

"Introducing Hyperapp 1.0 🎉"

  • Minimal
  • Pragmatic
  • Standalone

Minimal

We have aggressively minimized the concepts you need to understand to be productive while remaining on par with what other frameworks can do.

Pragmatic

Hyperapp holds firm on the functional programming front when managing your state, but takes a pragmatic approach to allowing for side effects, asynchronous actions, and DOM manipulations.

Standalone

Do more with less. Hyperapp combines state management with a virtual DOM engine that supports keyed updates & lifecycle events — all with no dependencies.

Entire Hyperapp script

This is the most heartwarming picture of the Elm Architecture I've ever seen. 😍

Richard Feldman

actions

View

<head>
  <script src="https://unpkg.com/hyperapp"></script>
</head>
<body>
  <script>
    const {h, app} = window.hyperapp
    const view = () =>
      h('div', {class: 'hello'}, 'Hello World!')
    app(null, null, view, document.body)
  </script>
</body>

"Hello World!" Hyperapp

<head>
  <script src="https://unpkg.com/hyperapp"></script>
</head>
<body>
  <script>
    const {h, app} = window.hyperapp
    const view = () =>
      h('div', {class: 'hello'}, 'Hello World!')
    app(null, null, view, document.body)
  </script>
</body>

"Hello World!" Hyperapp

Single script

<head>
  <script src="https://unpkg.com/hyperapp"></script>
</head>
<body>
  <script>
    const {h, app} = window.hyperapp
    const view = () =>
      h('div', {class: 'hello'}, 'Hello World!')
    app(null, null, view, document.body)
  </script>
</body>

"Hello World!" Hyperapp

Virtual DOM helper

<head>
  <script src="https://unpkg.com/hyperapp"></script>
</head>
<body>
  <script>
    const {h, app} = window.hyperapp
    const view = () =>
      h('div', {class: 'hello'}, 'Hello World!')
    app(null, null, view, document.body)
  </script>
</body>

"Hello World!" Hyperapp

Application helper

<head>
  <script src="https://unpkg.com/hyperapp"></script>
</head>
<body>
  <script>
    const {h, app} = window.hyperapp
    const view = () =>
      h('div', {class: 'hello'}, 'Hello World!')
    app(null, null, view, document.body)
  </script>
</body>

"Hello World!" Hyperapp

View function

<head>
  <script src="https://unpkg.com/hyperapp"></script>
</head>
<body>
  <script>
    const {h, app} = window.hyperapp
    const view = () =>
      h('div', {class: 'hello'}, 'Hello World!')
    app(null, null, view, document.body)
  </script>
</body>

"Hello World!" Hyperapp

Element name "div"

<head>
  <script src="https://unpkg.com/hyperapp"></script>
</head>
<body>
  <script>
    const {h, app} = window.hyperapp
    const view = () =>
      h('div', {class: 'hello'}, 'Hello World!')
    app(null, null, view, document.body)
  </script>
</body>

"Hello World!" Hyperapp

Element attributes

<head>
  <script src="https://unpkg.com/hyperapp"></script>
</head>
<body>
  <script>
    const {h, app} = window.hyperapp
    const view = () =>
      h('div', {class: 'hello'}, 'Hello World!')
    app(null, null, view, document.body)
  </script>
</body>

"Hello World!" Hyperapp

Element contents

View function returns virtual node

<head>
  <script src="https://unpkg.com/hyperapp"></script>
</head>
<body>
  <script>
    const {h, app} = window.hyperapp
    const view = () =>
      h('div', {class: 'hello'}, 'Hello World!')
    app(null, null, view, document.body)
  </script>
</body>

"Hello World!" Hyperapp

Start application

<head>
  <script src="https://unpkg.com/hyperapp"></script>
</head>
<body>
  <script>
    const {h, app} = window.hyperapp
    const view = () =>
      h('div', {class: 'hello'}, 'Hello World!')
    app(null, null, view, document.body)
  </script>
</body>

"Hello World!" Hyperapp

Start application

View function

<head>
  <script src="https://unpkg.com/hyperapp"></script>
</head>
<body>
  <script>
    const {h, app} = window.hyperapp
    const view = () =>
      h('div', {class: 'hello'}, 'Hello World!')
    app(null, null, view, document.body)
  </script>
</body>

"Hello World!" Hyperapp

Start application

Target DOM element

State

const state = {
  message: 'Boston!'
}
const view = (state) =>
  h('div',
    {class: 'hello'},
    `Hello ${state.message}`
  )
app(state, null, view, document.body)

"Hello <message>!" Hyperapp

const state = {
  message: 'Boston!'
}
const view = (state) =>
  h('div',
    {class: 'hello'},
    `Hello ${state.message}`
  )
app(state, null, view, document.body)

"Hello <message>!" Hyperapp

Initial state

const state = {
  message: 'Boston!'
}
const view = (state) =>
  h('div',
    {class: 'hello'},
    `Hello ${state.message}`
  )
app(state, null, view, document.body)

"Hello <message>!" Hyperapp

Pass initial state

const state = {
  message: 'Boston!'
}
const view = (state) =>
  h('div',
    {class: 'hello'},
    `Hello ${state.message}`
  )
app(state, null, view, document.body)

"Hello <message>!" Hyperapp

Current state

Actions

const {h, app} = window.hyperapp
const state = {
  message: ''
}
const actions = {
  onchange: e => state =>
    ({message: e.target.value})
}

"Hello <input>!" Hyperapp

const {h, app} = window.hyperapp
const state = {
  message: ''
}
const actions = {
  onchange: e => state =>
    ({message: e.target.value})
}

"Hello <input>!" Hyperapp

Object of actions

const {h, app} = window.hyperapp
const state = {
  message: ''
}
const actions = {
  onchange: e => state =>
    ({message: e.target.value})
}

"Hello <input>!" Hyperapp

Action takes arguments

const {h, app} = window.hyperapp
const state = {
  message: ''
}
const actions = {
  onchange: e => state =>
    ({message: e.target.value})
}

"Hello <input>!" Hyperapp

Then current state

const {h, app} = window.hyperapp
const state = {
  message: ''
}
const actions = {
  onchange: e => state =>
    ({message: e.target.value})
}

"Hello <input>!" Hyperapp

and returns new state

const view = (state, actions) =>
  h('div',
    {class: 'hello'},
    [`Hello ${state.message}`,
    h('input', {
      type: 'text',
      placeholder: 'enter your name',
      onchange: actions.onchange
    })]
  )
app(state, actions, view, document.body)

"Hello <input>!" Hyperapp

Pass actions to view

const view = (state, actions) =>
  h('div',
    {class: 'hello'},
    [`Hello ${state.message}`,
    h('input', {
      type: 'text',
      placeholder: 'enter your name',
      onchange: actions.onchange
    })]
  )
app(state, actions, view, document.body)

"Hello <input>!" Hyperapp

Call action on event

const view = (state, actions) =>
  h('div',
    {class: 'hello'},
    [`Hello ${state.message}`,
    h('input', {
      type: 'text',
      placeholder: 'enter your name',
      onchange: actions.onchange
    })]
  )
app(state, actions, view, document.body)

"Hello <input>!" Hyperapp

Hyper app

Why I ❤️ Hyperapp

View

State

Actions

Uses pure functions

const view = (state, actions) =>
  h('div',
    {class: 'hello'},
    [`Hello ${state.message}`,
    h('input', {
      type: 'text',
      placeholder: 'enter your name',
      onchange: actions.onchange
    })]
  )

View is a pure function, state and actions are arguments

const actions = {
  onchange: e => (state, actions) =>
    ({message: e.target.value})
}

Actions are pure functions with arguments, state, and other actions

  • Understand

  • Test

  • Extend

Pure functions are easy to:

Why I ❤️ Hyperapp

Handles view + state

Ok, let's write "serious" app

🎁 organize code

✅ testing

import {state} from './state'
import {actions} from './actions'
import {view} from './view'
import {app} from 'hyperapp'
app(state, actions, view, document.body)

app.js

<body>
  <script src="app.js"></script>
</body>

index.html

{
  "scripts": {
    "start": "parcel serve ./index.html"
  },
  "dependencies": { "hyperapp": "1.2.5" },
  "devDependencies": {
    "parcel-bundler": "1.7.0"
  }
}

package.json

Parcel 🎁 is awesomesauce

Parcel 🎁 is awesomesauce

Parcel 🎁 is awesomesauce

import {h} from 'hyperapp'

export const view = (state, actions) => (
  <div className="hello">
    Hello {state.message}
    <input type="text"
      placeholder="enter your name"
      onchange={actions.onchange} />
  </div>)

view.js

$ npm start

Use JSX

Parcel 🎁 is awesomesauce

export type State = {
  message: string
}
export const state:State = {
  message: ''
}

state.ts

Use TypeScript

Hyperapp includes TypeScript definitions

Parcel handles TS pretty well with zero config

Pro tip: slice state and actions

const state = {
  foo: { message: '' },
  bar: { age: 21 }
}
const actions = {
  foo: {
    // slice = current state.foo
    onchange: e => slice =>
      ({message: e.target.value})
  },
  bar: {},
  print: () => state => console.log(state)
}

Pro tip: call actions yourself

const actions = {
  ...
  print: () => state => console.log(state)
}
window.__app = app(state, actions, view, document.body)

Pro tip: split view functions

// pure arguments => virtual node function
const HelloX = ({greeting, name}) =>
  h('p', {}, `${greeting} ${name}`)
// call HelloX with arguments
const view = (state) =>
  h('div',
    {class: 'hello'},
    [HelloX({
      greeting: 'Hello',
      name: state.message
    })]
  )

Pro tip: actions can call other actions

const actions = {
  set: todos => state => ({todos}),
  click: () => (state, actions) => {
    fetch()
      .then(r => r.json())
      .then(actions.set)
  }
}

Pragmatic approach to side effects

✅ Testing

End to end testing with Cypress.io

Unit testing using Cypress.io

E2E

unit

it('logs user in', () => {
  cy.visit('page.com')
  cy.get('#login').click()
})
/// <reference types="cypress" />
beforeEach(() => {
  cy.visit('http://localhost:1234')
})
it('greets', () => {
  cy.get('input').type('tester{enter}')
  cy.contains('.hello', 'Hello tester')
    .should('be.visible')
})
$ npm i -D cypress start-server-and-test parcel-bundler
{
  "scripts": {
    "start": "parcel serve ./index.html",
    "build": "parcel build ./index.html",
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "e2e": "server-test 1234 cy:open",
    "ci": "server-test 1234 cy:run"
  }
}

package.json

cypress/integration/e2e.js

Cypress.io running end to end test

E2E

unit

import {view} from '../src/view'
import {mount} from 
  'cypress-hyperapp-unit-test'
it('renders', () => {
  mount(view)
  cy.click(...)
})

Unit testing using Cypress via an adaptor

Testing demo

Hyperapp is 💯

Easy to try

Easy to learn: state, actions, view

Easy to organize and test

Bonus

Parcel bundler + Hyperapp = ⚡️

Hyped about Hyperapp