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
With npm or yarn.
npm i hyperapp
Usage
CommonJS
const { app, html } = require("hyperapp")ES6
import { app, html } from "hyperapp"Bundle
With Browserify.
browserify -t hyperxify -g uglifyify index.js | uglifyjs > bundle.js
CDN
HyperApp is also distributed as a minified file, hosted on a CDN.
For JSX.
<script src="https://unpkg.com/hyperapp@0.0.12/dist/hyperapp.js"></script>For Hyperx.
<script src="https://unpkg.com/hyperapp@0.0.12/dist/hyperapp.hx.js"></script>For a more thorough introduction and advanced usage see the HyperApp User Guide.
Examples
Counter
app({
model: 0,
update: {
add: model => model + 1,
sub: model => model - 1
},
view: (model, msg) => html`
<div>
<button onclick=${msg.add}>+</button>
<h1>${model}</h1>
<button onclick=${msg.sub} disabled=${model <= 0}>-</button>
</div>`
})Input
app({
model: "",
update: {
text: (_, value) => value
},
view: (model, msg) => html`
<div>
<h1>Hi${model ? " " + model : ""}.</h1>
<input oninput=${e => msg.text(e.target.value)} />
</div>`
})Drag & Drop
const model = {
dragging: false,
position: {
x: 0, y: 0, offsetX: 0, offsetY: 0
}
}
const view = (model, msg) => html`
<div
onmousedown=${e => msg.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 = [
(_, msg) => addEventListener("mouseup", msg.drop),
(_, msg) => addEventListener("mousemove", e =>
msg.move({ x: e.pageX, y: e.pageY }))
]
app({ model, view, update, subscriptions })Todo
const FilterInfo = { All: 0, Todo: 1, Done: 2 }
const model = {
todos: [],
filter: FilterInfo.All,
input: "",
placeholder: "Add new todo!"
}
const view = (model, msg) => {
return html`
<div>
<h1>Todo</h1>
<p>
Show: ${
Object.keys(FilterInfo)
.filter(key => FilterInfo[key] !== model.filter)
.map(key => html`
<span><a 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 => html`
<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>`
}
const 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 })
}
app({ model, view, update })Documentation
html
Use to compose HTML elements.
const hello = html`<h1>Hello.</h1>`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>
})Or, add it to your .babelrc configuration.
{
"plugins": [
["transform-react-jsx", { "pragma": "h" }]
]
}app
Use app to bootstrap your app.
app({
model, update, view, subscriptions, effects, hooks, root
})All properties are optional.
model
A value or object that represents the entire state of your app.
To update the model, you send actions describing how the model should change. See view.
update
An object composed of functions known as reducers. These are a kind of action you send to update the model.
A reducer describes how the model should change by returning a new model or part of a model.
const update = {
increment: model => model + 1,
decrement: model => model - 1
}If a reducer returns part of a model, that part will be merged with the current model.
You call reducers inside a view, effect or subscription.
Reducers have a signature (model, data), where
modelis the current model, anddatais the data sent along with the action.
view
The view is a function that returns HTML using the html function.
The view has a signature (model, msg, params), where
modelis the current model,msgis an object you use to send actions (call reducers or cause effects) andparamsare the route parameters.
Use msg to send actions.
msg.action(data)datais the data that's passed to theaction, andactionthe name of the reducer / effect.
Example
app({
model: true,
view: (model, msg) => html`<button onclick=${msg.toggle}>${model+""}</button>`,
update: {
toggle: model => !model
}
})The view object may accommodate multiple views too. See routing.
Example
app({
view: {
"/": _ => html`<h1>Home</h1>`,
"/about": _ => html`<h1>About</h1>`
}
})Lifecycle Methods
Function handlers you can attach to virtual nodes to access DOM elements. The available methods are:
- oncreate(e :
HTMLElement) - onupdate(e :
HTMLElement) - onremove(e :
HTMLElement)
app({
view: _ => html`<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 => html`<canvas
width="600"
height="300"
onupdate=${e => repaint(e, model)} />`,
update: {
move: (model) => ({ x: model.x + 1, y: model.y + 1 })
},
subscriptions: [
(_, msg) => setInterval(_ => msg.move(), 10)
]
})effects
Effects cause side effects and are often asynchronous, like writing to a database, or sending requests to servers. They can dispatch other actions too.
Effects have a signature (model, msg, data, error), where
modelis the current model,msgis an object you use to call reducers / cause effects (see view),datais the data passed into the effect, anderroris a function you can optionally call to throw an error
Example
const wait = time => new Promise(resolve => setTimeout(_ => resolve(), time))
const model = {
counter: 0,
waiting: false
}
const view = (model, msg) =>
html`
<button
onclick=${msg.waitThenAdd}
disabled=${model.waiting}>${model.counter}
</button>`
const update = {
add: model => ({ counter: model.counter + 1 }),
toggle: model => ({ waiting: !model.waiting})
}
const effects = {
waitThenAdd: (model, msg) => {
msg.toggle()
wait(1000).then(msg.add).then(msg.toggle)
}
}
app({ model, view, update, effects })subscriptions
Subscriptions are functions that run once when the DOM is ready. Use a subscription to register global events, like mouse or keyboard listeners.
While reducers and effects are actions you cause, you can't call subscriptions directly.
A subscription has a signature (model, msg, error).
Example
app({
model: { x: 0, y: 0 },
update: {
move: (_, { x, y }) => ({ x, y })
},
view: model => html`<h1>${model.x}, ${model.y}</h1>`,
subscriptions: [
(_, msg) => addEventListener("mousemove", e => msg.move({ x: e.clientX, y: e.clientY }))
]
})hooks
Function handlers that can be used to inspect your application, implement middleware, loggers, etc.
onUpdate
Called when the model changes. Signature (lastModel, newModel, data).
onAction
Called when an action (reducer or effect) is dispatched. 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, msg) => html`
<div>
<button onclick=${msg.doSomething}>Log</button>
<button onclick=${msg.boom}>Error</button>
</div>`,
update: {
doSomething: model => !model,
},
effects: {
boom: (model, msg, 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 that will serve as a container for your app. If none is given, a div element is appended to the document.body.
Routing
Instead of a view as a single function, declare an object with multiple views and use the route path as the key.
app({
view: {
"*": (model, msg) => {},
"/": (model, msg) => {},
"/:slug": (model, msg, params) => {}
}
})/index route, also used when no other route matches/:a/:b/:cmatches a route with three components using the regular expression[A-Za-z0-9]+and stores each captured group in the params object, which is passed into the view function.
The route path syntax is based in the same syntax found in Express.
Example
const { app, html } = require("hyperapp")
const anchor = n => html`<h1><a href=${"/" + n}>${n}</a></h1>`
app({
view: {
"/": _ => anchor(Math.floor(Math.random() * 999)),
"/:key": (model, msg, { key }) => html`
<div>
<h1>${key}</h1>
<a href="/">Back</a>
</div>`
}
})setLocation
To update the address bar relative location and render a different view, use msg.setLocation(path).
Example
app({
view: {
"/": (model, msg) => html`
<div>
<h1>Home</h1>
<button onclick=${_ => msg.setLocation("/about")}>About</button>
</div>`,
"/about": (model, msg) => html`
<div>
<h1>About</h1>
<button onclick=${_ => msg.setLocation("/")}>Home</button>
</div>`
}
})href
HyperApp intercepts all <a href="/path">...</a> clicks and calls msg.setLocation("/path") for you. If you want to opt out of this, add a custom data-no-routing attribute to anchor elements that should be handled differently.
<a data-no-routing>...</a>Example
app({
view: {
"/": (model, msg) => html`
<div>
<h1>Home</h1>
<a href="/about">About</a>
</div>`,
"/about": (model, msg) => html`
<div>
<h1>About</h1>
<a href="/">Home</a>
</div>`
}
})