Building Web Apps with Hyperapp v1

Gleb Bahmutov, PhD

Saturday, March 9, 2019ย  4:15PM

these slides

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

18 people. Atlanta, Philly, Boston, Chicago, NYC

Fast, easy and reliable testing for anything that runs in a browser

One fine morning ...

<X> Bahmutovย 

Dad, make me a game

Clone of 2048

Vanilla JS

Game of GitHub

Vanilla JS

Vanilla JS is not going to scale

Goal: write an web application

Hard to pick

View + Data store

  • React + Redux

  • Angular + ng-rx

  • Vue + Vuex

  • X + ...x

Which JavaScript framework should I use next?

  • Features
  • Learning curve
  • Size and speed
  • Testability

Hyperapp

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

Entire Hyperapp script

This is the most heartwarming picture of the Elm Architecture I've ever seen. ๐Ÿ˜

Richard Feldman (Elm)

actions

state

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

easy to handles view + state

<X> Bahmutovย 

Dad, make me a maze runner game

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

if VSCode removes unused "h" see #49794ย 

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)
}

Text

Text

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",
    "test": "cypress run",
    "ci": "server-test 1234"
  }
}

package.json

cypress/integration/e2e.js

$ npm run ci

Can he do it? On 1 slide? No way!

Entire E2E test setup on 1 slide

Cypress.io running end to end test

Cypress.io running count-app tests

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

Cypress โค๏ธ Hyperapp

๐Ÿ“ผ video of end to end testing

https://github.com/LearnHyperapp/hyperapp-cypress-demo

โญ๏ธ https://www.cypress.io/

Hyperapp V2

  • Declarative actions #749
  • Effects #750
  • Subscriptions #752
  • and a few other things

now (March 2019)?

Magic state (v1)

const state = {
  message: '',
  count: 0
}
const actions = {
  onchange: e => state =>
    ({message: e.target.value}),
  increment: e => state =>
    ({count: state.count + 1})
}

just return a change

Explicit state (v2)

const state = {
  message: '',
  count: 0
}
const actions = {
  onchange: (state, data, e) =>
    ({...state, message: e.target.value}),
  increment: (state, data, e) =>
    ({...state, count: state.count + 1})
}

return full state

Call actions (v1)

<button onclick={() => actions.increment(1)}>+</button>

call action function

<button onClick={[IncrementBy, { number: 1 }]}+</button>

list function and arguments

Declare actions (v2)

Side effects (v2)

import * as Http from "@hyperapp/http"

const SendHttp = state => [
  { ...state, error: null, fetching: true },
  Http.fetch({
    url: state.url,
    action: SuccessResponse,
    error: ErrorResponse
  })
]

action returns [new state, effect]

Hyperapp V2

=> Cycle.js

If you ๐Ÿ‘ HyperApp V1 check out Overmind.js

Control state like Hyperapp but use any UI layer (React / Angular / Vue)

Hyperapp is ๐Ÿ’ฏ

Easy to try

Easy to learn: state, actions, view

Easy to organize and test

Bonus

Parcel bundler + Hyperapp = โšก๏ธ

Building Web Apps with Hyperapp v1

Let's Party ๐ŸŽ‰

Building Web Apps with Hyperapp - NERD Summit 2019

By Gleb Bahmutov

Building Web Apps with Hyperapp - NERD Summit 2019

What is the smallest and purest front-end framework out there? Did someone say Elm? I said smallest! The answer is Hyperapp - a really tiny, Elm-inspired pure functional web framework that combines the best features of today's hottest frameworks. It has built-in state management, pure functions, virtual DOM and still is easy to learn in a few minutes. Come to this session to learn how Hyperapp allows one to build a complete web application from small pure parts that are easy to code, simple to understand and convenient to test. NERD Summit 2019 video https://www.youtube.com/watch?v=8pcx0XitUCc

  • 851
Loading comments...

More from Gleb Bahmutov