GitHub - tunnckoCore/hyperapp: 1kb JavaScript library for building modern UI applications · GitHub
Skip to content

tunnckoCore/hyperapp

 
 

Folders and files

Version TravisCI Codecov Slack

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

Install

npm i hyperapp

Usage

CommonJS

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

ES6

import { h, app } from "hyperapp"

Bundle

With Browserify.

browserify -t hyperxify -g uglifyify index.js | uglifyjs > bundle.js

Or Webpack/Rollup.

CDN

HyperApp is also distributed as a minified file, hosted on a CDN.

<script src="https://unpkg.com/hyperapp/dist/hyperapp.js"></script>

For a more thorough introduction and advanced usage see the HyperApp 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, msg) =>
        <div>
            <h1>Todo</h1>
            <p>
                Show: {Object.keys(FilterInfo)
                    .filter(key => FilterInfo[key] !== model.filter)
                    .map(key =>
                        <span><a data-no-routing href="#" onclick={_ => msg.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 => msg.toggle({
                                value: t.done,
                                id: t.id
                            })}>{t.value}
                        </li>)}
            </ul></p>

            <p>
                <input
                    type="text"
                    onkeyup={e => e.keyCode === 13 ? msg.add() : ""}
                    oninput={e => msg.input({ value: e.target.value })}
                    value={model.input}
                    placeholder={model.placeholder}
                />{" "}
                <button onclick={msg.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

jsx

Import the h function and include the jsx pragma, in any order.

import { h, app } from "hyperapp"
/** @jsx h */

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

View online

Or, add it to your .babelrc configuration.

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

html

To use HyperApp without jsx, import the html function instead.

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

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

View online

html is a Hyperx-based template function.

app

Use app to start the app.

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

model

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

update

An object composed of functions often 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
}

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

Reducers can be triggered inside a view, effect or subscription.

Reducers have the signature (model, data, params):

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

When using the Router, the view receives additionally

  • params an object with the matched route parameters.

view

A function that returns an HTML element using jsx or the html function.

A view has the signature (model, actions):

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

To use actions:

actions.action(data)
  • data is any data you want to send to action.
  • action is 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

Functions that can be attached to your virtual HTML nodes 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

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 is the current model.
  • actions is an object used to trigger reducers and effects.
  • data is the data sent to the effect.
  • error is 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

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 HTML element container of your application. If none is given, a div element is appended to document.body and used as the container.

router

HyperApp provides a router out of the box.

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

app({ view, router })

When using the router, the view must be an object that consists of routes, each with a corresponding view function.

app({
    view: {
        "/": (model, actions) => {},
        "/about": (model, actions) => {},
        "/:key": (model, actions, params) => {}
    }
})
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

  • / matches the index route or when no other route matches.

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

The router path syntax is loosely based in the same syntax used in Express.

actions.setLocation

A special action available when using the Router. Use setLocation(path) to update the location.pathname. If the path matches an existing route, the corresponding view will be rendered.

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

app({
    router,
    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>
    }
})

View online

href

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

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

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>

About

1kb JavaScript library for building modern UI applications

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

Contributors

Languages

  • JavaScript 100.0%