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 JavaScript library for building frontend applications.
- Declarative: HyperApp's design is based on the Elm Architecture. Create scalable browser-based applications using a functional paradigm. The twist is you don't have to learn a new language.
- Stateless components: Build complex user interfaces from micro-components. Stateless components are framework agnostic, reusable, predictable and easier to debug.
- Batteries-included: Out of the box, HyperApp has Elm-like state management, a virtual DOM engine and a router; it still weighs
1kband has no dependencies. We're not opinionated about your stack either; use Browserify with Hyperx; Webpack or Rollup with Babel/JSX, etc.
Installation
npm i -S hyperapp
Usage
In Node.js.
import { h, app } from "hyperapp"In the browser via the CDN.
const { h, app } = hyperappExamples
Counter
app({
model: 0,
reducers: {
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: "",
reducers: {
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={{
position: "absolute",
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 reducers = {
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, reducers, 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>,
reducers: {
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(tag, data, children)
Creates a virtual DOM node.
tagis a tag name, e.g.divor a function that returns a tree of virtual nodes.datais an object with attributes, styles, events, properties, lifecycle methods, etc.childrenis an array of children virtual nodes. (Optional)
See the HyperApp User Guide for JSX/Hyperx setup instructions.
app(options)
Starts the application.
app({
model,
reducers,
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.
const model = {
count: 0
}reducers
Reducers are actions that describe how to derive a new model from the current model.
const reducers = {
add: model => model + 1,
sub: model => model - 1
}A reducer can return a 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 following signature: (model, data, params).
modelis the current model.dataos the data sent to the reducer.
When using the router, reducers receives an additional argument:
paramsis an object with the matched route parameters.
view
A view is a function that returns a virtual element tree. See h.
A view has the following signature: (model, actions).
To send actions:
actions.action(data)Example
app({
model: true,
view: (model, actions) => <button onClick={actions.toggle}>{model+""}</button>,
reducers: {
toggle: model => !model
}
})Lifecycle Methods
Lifecycle methods are functions that can be attached to virtual nodes in order to access actual DOM elements when they are created, updated or before they are removed.
- onCreate(e :
HTMLElement) - onUpdate(e :
HTMLElement) - onRemove(e :
HTMLElement)
app({
view: _ => <div onCreate={e => console.log(e)}></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)}
/>,
reducers: {
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 are often asynchronous, like writing to a database, or sending requests to servers.
Effects have the following signature: (model, actions, data, error).
modelis the current model.actionsis an object used to trigger reducers and effects.datais the data sent to the effect.erroris a function you can 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 reducers = {
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, reducers, 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, attach mouse or keyboard event listeners, etc.
A subscription has the following signature: (model, actions, error).
Example
app({
model: { x: 0, y: 0 },
reducers: {
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: (oldModel, 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>,
reducers: {
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 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 any function with the following signature: (render, options).
You can define your own router or use the one provided with HyperApp.
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) => {}
}
})The view property is used as a dictionary of routes/views.
The key is the 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./:keymatch 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>