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
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
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>
})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>
})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 })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 })
}
})Documentation
h
h is a virtual DOM node factory function.
app({
view: _ => h("a", { href: "#" }, "Hi")
})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
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
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
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>`
})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
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):
To send actions:
actions.action(data)data: any data you want to send toaction.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
}
})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)
]
})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 })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
}))
]
})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)
}
})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):
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
})/: 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 viaparams.
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
})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
})Add a custom data-no-routing attribute to anchor elements that should be handled differently.
<a data-no-routing>Not a route</a>