JSPM

  • Created
  • Published
  • Downloads 9502
  • Score
    100M100P100Q123312F
  • License MIT

1kb JavaScript library for building modern UI applications

Package Exports

  • hyperapp

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (hyperapp) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

hyperapp

cdnjs version travis codecov slack

HyperApp is a 1kb JavaScript library for building modern UI applications.

Install

npm i hyperapp

Usage

ES6

import { h, app } from "hyperapp"

CommonJS

const { h, app } = require("hyperapp")

For a complete introduction to HyperApp see the User Guide.

Examples

Hello World
app({
    model: "Hi.",
    view: model => <h1>{model}</h1>
})

View online

Counter
app({
    model: 0,
    update: {
        add: model => model + 1,
        sub: model => model - 1
    },
    view: (model, actions) =>
        <div>
            <button onclick={actions.add}>+</button>
            <h1>{model}</h1>
            <button onclick={actions.sub} disabled={model <= 0}>-</button>
        </div>
})

View online

Input
app({
    model: "",
    update: {
        text: (_, value) => value
    },
    view: (model, actions) =>
        <div>
            <h1>Hi{model ? " " + model : ""}.</h1>
            <input oninput={e => actions.text(e.target.value)} />
        </div>
})

View online

Drag & Drop
const model = {
    dragging: false,
    position: {
        x: 0, y: 0, offsetX: 0, offsetY: 0
    }
}

const view = (model, actions) =>
    <div
        onmousedown={e => actions.drag({
            position: {
                x: e.pageX, y: e.pageY, offsetX: e.offsetX, offsetY: e.offsetY
            }
        })}
        style={{
            userSelect: "none",
            cursor: "move",
            position: "absolute",
            padding: "10px",
            left: model.position.x - model.position.offsetX + "px",
            top: model.position.y - model.position.offsetY + "px",
            backgroundColor: model.dragging ? "gold" : "deepskyblue"
        }}
    >Drag Me!
    </div>

const update = {
    drop: model => ({ dragging: false }),
    drag: (model, { position }) => ({ dragging: true, position }),
    move: (model, { x, y }) => model.dragging
        ? ({ position: { ...model.position, x, y } })
        : model
}

const subscriptions = [
    (_, actions) => addEventListener("mouseup", actions.drop),
    (_, actions) => addEventListener("mousemove", e =>
        actions.move({ x: e.pageX, y: e.pageY }))
]

app({ model, view, update, subscriptions })

View online

Todo
const FilterInfo = { All: 0, Todo: 1, Done: 2 }

app({
    model: {
        todos: [],
        filter: FilterInfo.All,
        input: "",
        placeholder: "Add new todo!"
    },
    view: (model, actions) =>
        <div>
            <h1>Todo</h1>
            <p>
                Show: {Object.keys(FilterInfo)
                    .filter(key => FilterInfo[key] !== model.filter)
                    .map(key =>
                        <span><a data-no-routing href="#" onclick={_ => actions.filter({
                            value: FilterInfo[key]
                        })}>{key}</a> </span>
                    )}
            </p>

            <p><ul>
                {model.todos
                    .filter(t =>
                        model.filter === FilterInfo.Done
                            ? t.done :
                            model.filter === FilterInfo.Todo
                                ? !t.done :
                                model.filter === FilterInfo.All)
                    .map(t =>
                        <li style={{
                            color: t.done ? "gray" : "black",
                            textDecoration: t.done ? "line-through" : "none"
                        }}
                            onclick={e => actions.toggle({
                                value: t.done,
                                id: t.id
                            })}>{t.value}
                        </li>)}
            </ul></p>

            <p>
                <input
                    type="text"
                    onkeyup={e => e.keyCode === 13 ? actions.add() : ""}
                    oninput={e => actions.input({ value: e.target.value })}
                    value={model.input}
                    placeholder={model.placeholder}
                />{" "}
                <button onclick={actions.add}>add</button>
            </p>
        </div>,
    update: {
        add: model => ({
            input: "",
            todos: model.todos.concat({
                done: false,
                value: model.input,
                id: model.todos.length + 1
            })
        }),
        toggle: (model, { id, value }) => ({
            todos: model.todos.map(t =>
                id === t.id
                    ? Object.assign({}, t, { done: !value })
                    : t)
        }),
        input: (model, { value }) => ({ input: value }),
        filter: (model, { value }) => ({ filter: value })
    }
})

View online

See more examples

Documentation

h

h is a virtual DOM node factory function.

app({
    view: _ => h("a", { href: "#" }, "Hi")
})

View online

A virtual node has the following properties:

Property Type Description
tag String or Function The tag name, e.g. div. A function that returns a tree of virtual nodes is known as a child component.
data Object An object of DOM attributes, events, properties and lifecycle methods.
children ...Any? An array of children virtual nodes. If a node is a JavaScript primitive value, it will be rendered as a text node.

jsx

jsx enables you to mix HTML and JavaScript.

const link = <a href="#">Hi</a>

is equivalent to:

 const link = h("a", { href: "#" }, ["Hi"])

To use jsx with HyperApp follow the steps for your chosen module bundler.

Browserify

Create a .babelrc file:

{
    "presets": ["es2015"],
    "plugins": [
        [
            "transform-react-jsx",
            {
                "pragma": "h"
            }
        ]
    ]
}

Install development dependencies:

npm i -S \
    babel-plugin-transform-react-jsx \
    babel-preset-es2015 \
    babelify \
    browserify \
    bundle-collapser \
    uglifyify \
    uglifyjs

Bundle your application:

$(npm bin)/browserify \
    -t babelify \
    -g uglifyify \
    -p bundle-collapser/plugin index.js | uglifyjs > bundle.js

See boilerplate

Rollup

Create a rollup.config.js file:

import babel from "rollup-plugin-babel"
import resolve from "rollup-plugin-node-resolve"
import uglify from "rollup-plugin-uglify"

export default {
    plugins: [
        babel({
            babelrc: false,
            presets: ["es2015-rollup"],
            plugins: [
                ["transform-react-jsx", { pragma: "h" }]
            ]
        }),
        resolve({
            jsnext: true
        }),
        uglify()
    ]
}

Install development dependencies:

npm i -S \
    rollup \
    rollup-plugin-babel \
    rollup-plugin-node-resolve \
    rollup-plugin-uglify \
    babel-preset-es2015-rollup \
    babel-plugin-transform-react-jsx

Bundle your application:

$(npm bin)/rollup -cf iife -i index.js -o bundle.js

See boilerplate

Webpack

Create a .babelrc file:

{
    "presets": ["es2015"],
    "plugins": [
        [
            "transform-react-jsx",
            {
                "pragma": "h"
            }
        ]
    ]
}

Create a webpack.config.js file:

module.exports = {
    entry: "./index.js",
    output: {
        filename: "bundle.js",
    },
    module: {
        loaders: [{
            test: /\.js$/,
            exclude: /node_modules/,
            loader: "babel-loader"
        }]
    }
}

Install development dependencies:

npm i -S \
    webpack \
    babel-core \
    babel-loader \
    babel-preset-es2015 \
    babel-plugin-transform-react-jsx

Bundle your application:

$(npm bin)/webpack -p

See boilerplate

hyperx

hyperx is a template function factory, or ES6 alternative to jsx.

const { h, app } = require("hyperapp")
const hyperx = require("hyperx")
const html = hyperx(h)

app({
    model: "Hi.",
    view: model => html`<h1>${model}</h1>`
})

View online

Setup Instructions

Install hyperx:

npm i -D hyperx

Install development dependencies:

npm i -S \
    browserify \
    hyperxify \
    babelify \
    uglifyify \
    bundle-collapser
    uglify-js

Bundle your application:

$(npm bin)/browserify \
    -t hyperxify \
    -t babelify \
    -g uglifyify \
    -p bundle-collapser/plugin index.js | uglifyjs > bundle.js

See boilerplate

app

Use app to start your app.

app({
    model,
    update,
    view,
    effects,
    subscriptions,
    root,
    router
})

model

The model is a primitive type, array or object that represents the state of your application. HyperApp applications use a single model architecture.

update

Update is an object composed of functions called reducers. A reducer describes how to derive the next model from the current model.

const update = {
    increment: model => model + 1,
    decrement: model => model - 1
}

A reducer can return an entirely new model or part of a model. If it returns part of a model, that part will be merged with the current model.

A reducer can be triggered inside a view, effect or subscription.

A reducer has the signature (model, data, params):

  • model: the current model.
  • data: the data sent to the reducer.

If the router is used, reducers receives an additional argument:

  • params: an object with the matched route parameters.

view

A function that returns a virtual node tree using jsx, hyperx or h.

A view has the signature (model, actions):

  • model: the current model.
  • actions: an object used to trigger reducers and effects.

To send actions:

actions.action(data)
  • data: any data you want to send to action.
  • action: the name of the reducer or effect.
Example
app({
    model: true,
    view: (model, actions) => <button onclick={actions.toggle}>{model+""}</button>,
    update: {
        toggle: model => !model
    }
})

View online

Lifecycle Methods

The lifecycle methods are functions that can be attached to virtual nodes in order to access their real DOM elements.

  • oncreate(e : HTMLElement)
  • onupdate(e : HTMLElement)
  • onremove(e : HTMLElement)
app({
    view: _ => <div oncreate={e => console.log(e)}>Hi.</div>
})
Example
const repaint = (canvas, model) => {
    const context = canvas.getContext("2d")
    context.fillStyle = "white"
    context.fillRect(0, 0, canvas.width, canvas.height)
    context.beginPath()
    context.arc(model.x, model.y, 50, 0, 2 * Math.PI)
    context.stroke()
}

app({
    model: { x: 0, y: 0 },
    view: model => <canvas
        width="600"
        height="300"
        onupdate={e => repaint(e, model)} />,
    update: {
        move: (model) => ({ x: model.x + 1, y: model.y + 1 })
    },
    subscriptions: [
      (_, actions) => setInterval(_ => actions.move(), 10)
    ]
})

View online

effects

Effects are actions that cause side effects and can be asynchronous, like writing to a database, or sending requests to servers.

Effects have the following signature: (model, actions, data, error).

  • model: the current model.
  • actions: an object used to trigger reducers and effects.
  • data: the data sent to the effect.
  • error: a function you may call to throw an error.
Example
const wait = time => new Promise(resolve => setTimeout(_ => resolve(), time))

const model = {
    counter: 0,
    waiting: false
}

const view = (model, actions) =>
    <button
        onclick={actions.waitThenAdd}
        disabled={model.waiting}>{model.counter}
    </button>


const update = {
    add: model => ({ counter: model.counter + 1 }),
    toggle: model => ({ waiting: !model.waiting})
}

const effects = {
    waitThenAdd: (model, actions) => {
        actions.toggle()
        wait(1000).then(actions.add).then(actions.toggle)
    }
}

app({ model, view, update, effects })

View online

subscriptions

Subscriptions are functions scheduled to run only once when the DOM is ready. Use a subscription to register global events, open a socket connection, attached mouse or keyboard event listeners, etc.

A subscription has the signature (model, actions, error).

Example
app({
    model: { x: 0, y: 0 },
    update: {
        move: (_, { x, y }) => ({ x, y })
    },
    view: model => <h1>{model.x}, {model.y}</h1>,
    subscriptions: [
        (_, actions) => addEventListener("mousemove", e => actions.move({
            x: e.clientX,
            y: e.clientY
        }))
    ]
})

View online

hooks

Hooks are function handlers that can be used to inspect your application, implement middleware, loggers, etc. There are three: onUpdate, onAction, and onError.

onUpdate

Called when the model changes. Signature: (lastModel, newModel, data).

onAction

Called when an action (reducer or effect) is triggered. Signature: (name, data).

onError

Called when you use the error function inside a subscription or effect. If you don't use this hook, the default behavior is to throw. Signature: (err).

Example
app({
    model: true,
    view: (model, actions) =>
        <div>
            <button onclick={actions.doSomething}>Log</button>
            <button onclick={actions.boom}>Error</button>
        </div>,
    update: {
        doSomething: model => !model,
    },
    effects: {
        boom: (model, actions, data, err) => setTimeout(_ => err(Error("BOOM")), 1000)
    },
    hooks: {
        onError: e =>
            console.log("[Error] %c%s", "color: red", e),
        onAction: name =>
            console.log("[Action] %c%s", "color: blue", name),
        onUpdate: (last, model) =>
            console.log("[Update] %c%s -> %c%s", "color: gray", last, "color: blue", model)
    }
})

View online

root

The root is the HTML element container of your application. If none is given, a div element is appended to document.body and used as the container.

router

The router is a function with the signature (render, options):

  • render: a function that can render a view.
  • options: the same options object passed to app

HyperApp provides a router out of the box.

import { h, app, router } from "hyperapp"

To use the router, pass it to app.

app({
    router,
    view: {
        "/": (model, actions) => {},
        "/about": (model, actions) => {},
        "/:key": (model, actions, params) => {}
    }
})

In this case, the view property is used as a dictionary to look up views by route.

The key is a route and the value is the view function.

Example
const Anchor = ({ href }) => <h1><a href={"/" + href}>{href}</a></h1>

app({
    view: {
        "/": _ => <Anchor href={Math.floor(Math.random() * 999)}></Anchor>,
        "/:key": (model, actions, { key }) =>
            <div>
                <h1>{key}</h1>
                <a href="/">Back</a>
            </div>
    },
    router
})

View online

  • /: match the index route or use as a wildcard to select the view when no route matches.

  • /:key: match a route using the regular expression [A-Za-z0-9]+. The matched key is passed to the view function via params.

actions.setLocation

Call actions.setLocation(path) to update the location.pathname. If the path matches an existing route, the corresponding view will be rendered. Requires the default Router.

Example
const Page = ({ title, target, onclick }) =>
    <div>
        <h1>{title}</h1>
        <button onclick={onclick}>{target}</button>
    </div>

app({
    view: {
        "/": (model, actions) =>
            <Page
                title="Home"
                target="About"
                onclick={_ => actions.setLocation("/about")}>
            </Page>
        ,
        "/about": (model, actions) =>
            <Page
                title="About"
                target="Home"
                onclick={_ => actions.setLocation("/")}>
            </Page>
    },
    router
})

View online

href

HyperApp intercepts all <a href="/path">...</a> clicks and calls action.setLocation("/path"). External links and links that begin with a # character are not intercepted.

Example
app({
    view: {
        "/": (model, actions) =>
            <div>
                <h1>Home</h1>
                <a href="/about">About</a>
            </div>
        ,
        "/about": (model, actions) =>
            <div>
                <h1>About</h1>
                <a href="/">Home</a>
            </div>
    },
    router
})

View online

Add a custom data-no-routing attribute to anchor elements that should be handled differently.

<a data-no-routing>Not a route</a>