HyperApp is a 1kb
functional JavaScript library for building modern UI applications.
Install
npm i hyperapp
Usage
CDN
<script src="https://cdn.rawgit.com/hyperapp/hyperapp/0.0.9/dist/app.min.js"></script>
<script src="https://cdn.rawgit.com/hyperapp/hyperapp/0.0.9/dist/html.min.js"></script>
Browserify
browserify -g uglifyify index.js | uglifyjs > bundle.js
Examples
Hello world
app({
model: "Hi.",
view: model => html`<h1>${model}</h1>`
})
View online
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>`
})
View online
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>`
})
View online
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 subs = [
(_, msg) => addEventListener("mouseup", msg.drop),
(_, msg) => addEventListener("mousemove", e =>
msg.move({ x: e.pageX, y: e.pageY }))
]
app({ model, view, update, subs })
View online
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 })
View online
See more examples
Documentation
html
Use html
to compose HTML elements.
const hello = html`<h1>Hello World!</h1>`
html
is a tagged template string. If you are familiar with React, this is like JSX, but without breaking JavaScript.
app
Use app
to bootstrap your app.
app({
model, update, view, subs, 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
model
is the current model, anddata
is 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
model
is the current model,msg
is an object you use to send actions (call reducers or cause effects) andparams
are the route parameters.
Use msg
to send actions.
msg.action(data)
where data
is any data you want to pass to the reducer / effect.
Example
app({
model: true,
view: (model, msg) => html`<button onclick=${msg.toggle}>${model+""}</button>`,
update: {
toggle: model => !model
}
})
View online
The view object may accommodate multiple views too. See routing.
Example
app({
view: {
"/": _ => html`<h1>Home</h1>`,
"/about": _ => html`<h1>About</h1>`
}
})
View online
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, error)
, where
model
is the current model,msg
is an object you use to call reducers / cause effects (see view), anderror
is a function you may call with an error if something goes wrong.
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 })
View online
subs
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>`,
subs: [
(_, msg) => addEventListener("mousemove", e => msg.move({ x: e.clientX, y: e.clientY }))
]
})
View online
hooks
Hooks are functions called for certain events during the lifetime of the app. You can use hooks to implement middleware, loggers, etc.
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)
}
})
View online
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)
.
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/:c
matches 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>`
}
})
View online
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>`
}
})
View online
href
As a bonus, we intercept all <a href="/path">...</a>
clicks and call msg.setLocation("/path")
for you. If you want to opt out of this, add the custom attribute data-no-routing
to any anchor element 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>`
}
})
View online