UNPKG

20.4 kBMarkdownView Raw
1# <img height=24 src=https://cdn.rawgit.com/jorgebucaran/f53d2c00bafcf36e84ffd862f0dc2950/raw/882f20c970ff7d61aa04d44b92fc3530fa758bc0/Hyperapp.svg> Hyperapp
2
3[![Travis CI](https://img.shields.io/travis/hyperapp/hyperapp/master.svg)](https://travis-ci.org/hyperapp/hyperapp)
4[![Codecov](https://img.shields.io/codecov/c/github/hyperapp/hyperapp/master.svg)](https://codecov.io/gh/hyperapp/hyperapp)
5[![npm](https://img.shields.io/npm/v/hyperapp.svg)](https://www.npmjs.org/package/hyperapp)
6[![Slack](https://hyperappjs.herokuapp.com/badge.svg)](https://hyperappjs.herokuapp.com "Join us")
7
8Hyperapp is a JavaScript micro-framework for building web applications.
9
10* **Minimal** — We have aggressively minimized the concepts you need to understand to be productive while remaining on par with what other frameworks can do.
11* **Pragmatic** — Hyperapp holds firm on the functional programming front when managing your state, but takes a pragmatic approach to allowing for side effects, asynchronous actions, and DOM manipulations.
12* **Standalone** — Do more with less. Hyperapp combines state management with a virtual DOM engine that supports keyed updates & lifecycle events — all with no dependencies.
13
14## Getting Started
15
16Our first example is a counter that can be incremented or decremented. Go ahead and [try it online](https://codepen.io/hyperapp/pen/zNxZLP/left/?editors=0010).
17
18```jsx
19import { h, app } from "hyperapp"
20
21const state = {
22 count: 0
23}
24
25const actions = {
26 down: value => state => ({ count: state.count - value }),
27 up: value => state => ({ count: state.count + value })
28}
29
30const view = (state, actions) => (
31 <div>
32 <h1>{state.count}</h1>
33 <button onclick={() => actions.down(1)}>-</button>
34 <button onclick={() => actions.up(1)}>+</button>
35 </div>
36)
37
38app(state, actions, view, document.body)
39```
40
41Hyperapp consists of a two-function API. <samp>hyperapp.h</samp> returns a new [virtual DOM](#view) node tree and <samp>hyperapp.app</samp> [mounts](#mounting) a new application in the specified DOM element. Without an element, it's possible to use Hyperapp "headless", which can be useful when unit testing your program.
42
43This example assumes you are using a JavaScript compiler like [Babel](https://babeljs.io) or [TypeScript](https://www.typescriptlang.org) and a module bundler like [Parcel](https://parceljs.org), [Webpack](https://webpack.js.org), etc. If you are using JSX, all you need to do is install the JSX [transform plugin](https://babeljs.io/docs/plugins/transform-react-jsx) and add the pragma option to your <samp>.babelrc</samp> file.
44
45```json
46{
47 "plugins": [["transform-react-jsx", { "pragma": "h" }]]
48}
49```
50
51JSX is a language syntax extension that lets you write HTML tags interspersed with JavaScript. Because browsers don't understand JSX, we use a compiler to transform it into <samp>hyperapp.h</samp> function calls under the hood.
52
53```jsx
54const view = (state, actions) =>
55 h("div", {}, [
56 h("h1", {}, state.count),
57 h("button", { onclick: () => actions.down(1) }, "-"),
58 h("button", { onclick: () => actions.up(1) }, "+")
59 ])
60```
61
62Note that JSX is not required for building applications with Hyperapp. You can use <samp>hyperapp.h</samp> directly and without a compilation step as shown above. Other alternatives to JSX include [@hyperapp/html](https://github.com/hyperapp/html), [hyperx](https://github.com/substack/hyperx), [t7](https://github.com/trueadm/t7) and [ijk](https://github.com/lukejacksonn/ijk).
63
64## Installation
65
66Install with npm or Yarn.
67
68<pre>
69npm i <a href=https://www.npmjs.com/package/hyperapp>hyperapp</a>
70</pre>
71
72Then with a module bundler like [Rollup](https://rollupjs.org) or [Webpack](https://webpack.js.org), use as you would anything else.
73
74```js
75import { h, app } from "hyperapp"
76```
77
78If you don't want to set up a build environment, you can download Hyperapp from a CDN like [unpkg.com](https://unpkg.com/hyperapp) and it will be globally available through the <samp>window.hyperapp</samp> object. We support all ES5-compliant browsers, including Internet Explorer 10 and above.
79
80```html
81<script src="https://unpkg.com/hyperapp"></script>
82```
83
84## Overview
85
86Hyperapp applications consist of three interconnected parts: the [state](#state), [view](#view), and [actions](#actions).
87
88Once initialized, your application executes in a continuous loop, taking in actions from users or from external events, updating the state, and representing changes in the view through a virtual DOM model. Think of an action as a signal that notifies Hyperapp to update the state and schedule the next view redraw. After processing an action, the new state is presented back to the user.
89
90### State
91
92The state is a plain JavaScript object that describes your entire program. It consists of all the dynamic data that is moved around in the application during its execution. The state cannot be mutated once it is created. We must use actions to update it.
93
94```js
95const state = {
96 count: 0
97}
98```
99
100Like any JavaScript object, the state can be a nested tree of objects. We refer to nested objects in the state as partial state. A single state tree does not conflict with modularity — see [Nested Actions](#nested-actions) to find out how to update deeply nested objects and split your state and actions.
101
102```js
103const state = {
104 top: {
105 count: 0
106 },
107 bottom: {
108 count: 0
109 }
110}
111```
112
113Because Hyperapp performs a shallow merge when updating your state, the top-level state must be a plain JavaScript object, other than this, you can use any type, including arrays, ES6 [Maps](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), [Sets](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set), [Immutable.js](https://facebook.github.io/immutable-js/) structures, etc.
114
115### Actions
116
117The only way to change the state is via actions. An action is a unary function (accepts a single argument) expecting a payload. The payload can be anything you want to pass into the action.
118
119To update the state, an action must return a partial state object. The new state will be the result of a shallow merge between this object and the current state. Under the hood, Hyperapp wires every function from your actions to schedule a view redraw whenever the state changes.
120
121```js
122const actions = {
123 setValue: value => ({ value })
124}
125```
126
127Instead of returning a partial state object directly, an action can return a function that takes the current state and actions as arguments and returns a partial state object.
128
129```js
130const actions = {
131 down: value => state => ({ count: state.count - value }),
132 up: value => state => ({ count: state.count + value })
133}
134```
135
136State updates are always immutable. Do not mutate the state object argument within an action and return it — the results are not what you expect (e.g., the view will not be redrawn).
137
138Immutability enables time-travel debugging, helps prevent introducing hard-to-track-down bugs by making state changes more predictable, and allows cheap memoization of components using shallow equality <samp>===</samp> checks.
139
140#### Asynchronous Actions
141
142Actions used for side effects (writing to databases, sending a request to a server, etc.) don't need to have a return value. You may call an action from within another action or callback function. Actions which return a Promise, <samp>undefined</samp> or <samp>null</samp> will not trigger redraws or update the state.
143
144```js
145const actions = {
146 upLater: value => (state, actions) => {
147 setTimeout(actions.up, 1000, value)
148 },
149 up: value => state => ({ count: state.count + value })
150}
151```
152
153An action can be an <samp>[async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)</samp> function. Because <samp>async</samp> functions return a Promise, and not a partial state object, you need to call another action in order to update the state.
154
155```js
156const actions = {
157 upLater: () => async (state, actions) => {
158 await new Promise(done => setTimeout(done, 1000))
159 actions.up(10)
160 },
161 up: value => state => ({ count: state.count + value })
162}
163```
164
165#### Nested Actions
166
167Actions can be nested inside namespaces. Updating deeply nested state is as easy as declaring actions inside an object in the same path as the part of the state you want to update.
168
169```jsx
170const state = {
171 counter: {
172 count: 0
173 }
174}
175
176const actions = {
177 counter: {
178 down: value => state => ({ count: state.count - value }),
179 up: value => state => ({ count: state.count + value })
180 }
181}
182```
183
184#### Interoperability
185
186The <samp>app</samp> function returns a copy of your actions where every function is wired to changes in the state. Exposing this object to the outside world can be useful to operate your application from another program or framework, subscribe to global events, listen to mouse and keyboard input, etc.
187
188To see this in action, modify the example from [Getting Started](#getting-started) to save the wired actions to a variable and try using them. You should see the counter update accordingly.
189
190```jsx
191const main = app(state, actions, view, document.body)
192
193setInterval(main.up, 250, 1)
194setInterval(main.down, 500, 1)
195```
196
197Including an action returning the state argument can be useful. Because state updates are always immutable, returning a reference to the current state will not schedule a view redraw.
198
199```jsx
200const actions = {
201 getState: () => state => state
202}
203```
204
205### View
206
207Every time your application state changes, the view function is called so that you can specify how you want the DOM to look based on the new state. The view returns your specification in the form of a plain JavaScript object known as a virtual DOM and Hyperapp takes care of updating the actual DOM to match it.
208
209```js
210import { h } from "hyperapp"
211
212export const view = (state, actions) =>
213 h("div", {}, [
214 h("h1", {}, state.count),
215 h("button", { onclick: () => actions.down(1) }, "-"),
216 h("button", { onclick: () => actions.up(1) }, "+")
217 ])
218```
219
220A virtual DOM is a description of what a DOM should look like using a tree of nested JavaScript objects known as virtual nodes. Think of it as a lightweight representation of the DOM. In the example, the view function returns an object like this.
221
222```jsx
223{
224 nodeName: "div",
225 attributes: {},
226 children: [
227 {
228 nodeName: "h1",
229 attributes: {},
230 children: [0]
231 },
232 {
233 nodeName: "button",
234 attributes: { ... },
235 children: ["-"]
236 },
237 {
238 nodeName: "button",
239 attributes: { ... },
240 children: ["+"]
241 }
242 ]
243}
244```
245
246The virtual DOM model allows us to write code as if the entire document is thrown away and rebuilt on each change, while we only update what actually changed. We try to do this in the least number of steps possible, by comparing the new virtual DOM against the previous one. This leads to high efficiency, since typically only a small percentage of nodes need to change, and changing real DOM nodes is costly compared to recalculating the virtual DOM.
247
248It may seem wasteful to throw away the old virtual DOM and re-create it entirely on every update — not to mention the fact that at any one time, Hyperapp is keeping two virtual DOM trees in memory, but as it turns out, browsers can create hundreds of thousands of objects very quickly. On the other hand, modifying the DOM is several orders of magnitude more expensive.
249
250### Mounting
251
252To mount your application in a page, we need a DOM element. This element is referred to as the application container. Applications built with Hyperapp always have a single container.
253
254```jsx
255app(state, actions, view, container)
256```
257
258Hyperapp will also attempt to reuse existing elements inside the container enabling SEO optimization and improving your sites time-to-interactive. The process consists of serving a fully rendered page together with your application. Then instead of throwing away the existing content, we'll turn your DOM nodes into an interactive application out of the box.
259
260This is how we can recycle server-rendered content out the counter example from before. See [Getting Started](#getting-started) for the application code.
261
262```html
263<!doctype html>
264<html>
265<head>
266 <meta charset="utf-8">
267 <script defer src="bundle.js"></script>
268</head>
269
270<body>
271 <div>
272 <h1>0</h1>
273 <button>-</button>
274 <button>+</button>
275 </div>
276</body>
277</html>
278```
279
280### Components
281
282A component is a pure function that returns a virtual node. Unlike the view function, components are not wired to your application state or actions. Components are dumb, reusable blocks of code that encapsulate markup, styles and behaviors that belong together.
283
284Here's a taste of how to use components in your application. The application is a typical To-Do manager. Go ahead and [try it online here](https://codepen.io/hyperapp/pen/zNxRLy).
285
286```jsx
287import { h } from "hyperapp"
288
289const TodoItem = ({ id, value, done, toggle }) => (
290 <li
291 class={done && "done"}
292 onclick={() =>
293 toggle({
294 value: done,
295 id: id
296 })
297 }
298 >
299 {value}
300 </li>
301)
302
303export const view = (state, actions) => (
304 <div>
305 <h1>Todo</h1>
306 <ul>
307 {state.todos.map(({ id, value, done }) => (
308 <TodoItem id={id} value={value} done={done} toggle={actions.toggle} />
309 ))}
310 </ul>
311 </div>
312)
313```
314
315If you don't know all the attributes that you want to place in a component ahead of time, you can use the [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator). Note that Hyperapp components can return an array of elements as in the following example. This technique lets you group a list of children without adding extra nodes to the DOM.
316
317```jsx
318const TodoList = ({ todos, toggle }) =>
319 todos.map(todo => <TodoItem {...todo} toggle={toggle} />)
320```
321
322#### Lazy Components
323
324Components can only receive attributes and children from their parent component. Similarly to the top-level view function, lazy components are passed your application global state and actions. To create a lazy component, return a view function from a regular component.
325
326```jsx
327import { h } from "hyperapp"
328
329export const Up = ({ by }) => (state, actions) => (
330 <button onclick={() => actions.up(by)}>+ {by}</button>
331)
332
333export const Down = ({ by }) => (state, actions) => (
334 <button onclick={() => actions.down(by)}>- {by}</button>
335)
336
337export const Double = () => (state, actions) => (
338 <button onclick={() => actions.up(state.count)}>+ {state.count}</button>
339)
340
341export const view = (state, actions) => (
342 <main>
343 <h1>{state.count}</h1>
344 <Up by={2} />
345 <Down by={1} />
346 <Double />
347 </main>
348)
349```
350
351#### Children Composition
352
353Components receive their children elements via the second argument, allowing you and other components to pass arbitrary children down to them.
354
355```jsx
356const Box = ({ color }, children) => (
357 <div class={`box box-${color}`}>{children}</div>
358)
359
360const HelloBox = ({ name }) => (
361 <Box color="green">
362 <h1 class="title">Hello, {name}!</h1>
363 </Box>
364)
365```
366
367## Supported Attributes
368
369Supported attributes include [HTML attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes), [SVG attributes](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute), [DOM events](https://developer.mozilla.org/en-US/docs/Web/Events), [Lifecycle Events](#lifecycle-events), and [Keys](#keys). Note that non-standard HTML attribute names are not supported, <samp>onclick</samp> and <samp>class</samp> are valid, but <samp>onClick</samp> or <samp>className</samp> are not.
370
371### Styles
372
373The <samp>style</samp> attribute expects a plain object rather than a string as in HTML.
374Each declaration consists of a style name property written in <samp>camelCase</samp> and a value. CSS variables are supported too.
375
376Individual style properties will be diffed and mapped against <samp>[HTMLElement.style](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style)</samp> property members of the DOM element - you should therefore use the JavaScript style object [property names](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Properties_Reference), e.g. <samp>backgroundColor</samp> rather than <samp>background-color</samp>.
377
378```jsx
379import { h } from "hyperapp"
380
381export const Jumbotron = ({ text }) => (
382 <div
383 style={{
384 color: "white",
385 fontSize: "32px",
386 textAlign: center,
387 backgroundImage: `url(${imgUrl})`
388 }}
389 >
390 {text}
391 </div>
392)
393```
394
395### Lifecycle Events
396
397You can be notified when elements managed by the virtual DOM are created, updated or removed via lifecycle events. Use them for animation, data fetching, wrapping third party libraries, cleaning up resources, etc.
398
399Note that lifecycle events are attached to virtual DOM nodes not to components. Consider adding a key to ensure that the event is attached to a specific DOM element, rather than a recycled one.
400
401#### oncreate
402
403This event is fired after the element is created and attached to the DOM. Use it to manipulate the DOM node directly, make a network request, create a slide/fade in animation, etc.
404
405```jsx
406import { h } from "hyperapp"
407
408export const Textbox = ({ placeholder }) => (
409 <input
410 type="text"
411 placeholder={placeholder}
412 oncreate={element => element.focus()}
413 />
414)
415```
416
417#### onupdate
418
419This event is fired every time we update the element attributes. Use <samp>oldAttributes</samp> inside the event handler to check if any attributes changed or not.
420
421```jsx
422import { h } from "hyperapp"
423
424export const Textbox = ({ placeholder }) => (
425 <input
426 type="text"
427 placeholder={placeholder}
428 onupdate={(element, oldAttributes) => {
429 if (oldAttributes.placeholder !== placeholder) {
430 // Handle changes here!
431 }
432 }}
433 />
434)
435```
436
437#### onremove
438
439This event is fired before the element is removed from the DOM. Use it to create slide/fade out animations. Call <samp>done</samp> inside the function to remove the element. This event is not called in its child elements.
440
441```jsx
442import { h } from "hyperapp"
443
444export const MessageWithFadeout = ({ title }) => (
445 <div onremove={(element, done) => fadeout(element).then(done)}>
446 <h1>{title}</h1>
447 </div>
448)
449```
450
451#### ondestroy
452
453This event is fired after the element has been removed from the DOM, either directly or as a result of a parent being removed. Use it for invalidating timers, canceling a network request, removing global events listeners, etc.
454
455```jsx
456import { h } from "hyperapp"
457
458export const Camera = ({ onerror }) => (
459 <video
460 poster="loading.png"
461 oncreate={element => {
462 navigator.mediaDevices
463 .getUserMedia({ video: true })
464 .then(stream => (element.srcObject = stream))
465 .catch(onerror)
466 }}
467 ondestroy={element => element.srcObject.getTracks()[0].stop()}
468 />
469)
470```
471
472### Keys
473
474Keys helps identify nodes every time we update the DOM. By setting the <samp>key</samp> property on a virtual node, you declare that the node should correspond to a particular DOM element. This allow us to re-order the element into its new position, if the position changed, rather than risk destroying it.
475
476```jsx
477import { h } from "hyperapp"
478
479export const ImageGallery = ({ images }) =>
480 images.map(({ hash, url, description }) => (
481 <li key={hash}>
482 <img src={url} alt={description} />
483 </li>
484 ))
485```
486
487Keys must be unique among sibling-nodes. Don't use an array index as key, if the index also specifies the order of siblings. If the position and number of items in a list is fixed, it will make no difference, but if the list is dynamic, the key will change every time the tree is rebuilt.
488
489```jsx
490import { h } from "hyperapp"
491
492export const PlayerList = ({ players }) =>
493 players
494 .slice()
495 .sort((player, nextPlayer) => nextPlayer.score - player.score)
496 .map(player => (
497 <li key={player.username} class={player.isAlive ? "alive" : "dead"}>
498 <PlayerProfile {...player} />
499 </li>
500 ))
501```
502
503#### Top-Level Nodes
504
505Keys are not registered on the top-level node of your view. If you are toggling the top-level view, and you must use keys, wrap them in an unchanging node.
506
507## Links
508
509* [Slack](https://hyperappjs.herokuapp.com)
510* [Twitter](https://twitter.com/hyperappJS)
511* [Examples](https://codepen.io/search/pens/?q=hyperapp)
512* [/r/hyperapp](https://www.reddit.com/r/hyperapp)
513
514## License
515
516Hyperapp is MIT licensed. See [LICENSE](LICENSE.md).