UNPKG

22.3 kBMarkdownView Raw
1# Hyperapp
2
3[![Travis CI](https://img.shields.io/travis/jorgebucaran/hyperapp/master.svg)](https://travis-ci.org/jorgebucaran/hyperapp)
4[![Codecov](https://img.shields.io/codecov/c/github/jorgebucaran/hyperapp/master.svg)](https://codecov.io/gh/jorgebucaran/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> :wave: [**Hyperapp 2.0**](https://github.com/jorgebucaran/hyperapp/pull/726) is coming out soon! Try it from the [V2](https://github.com/jorgebucaran/hyperapp/tree/V2) branch right now and be sure to follow [@HyperappJS](https://twitter.com/hyperappjs) for all the Hyperapp news & updates.
11
12- **Minimal** — We have aggressively minimized the concepts you need to understand to be productive while remaining on par with what other frameworks can do.
13- **Pragmatic** — Hyperapp holds firm on the functional programming front when managing your state, but takes a pragmatic approach to allow for side effects, asynchronous actions, and DOM manipulations.
14- **Standalone** — Do more with less. Hyperapp combines state management with a virtual DOM engine that supports keyed updates & lifecycle events — all with no dependencies.
15
16## Getting Started
17
18Our 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).
19
20```jsx
21import { h, app } from "hyperapp"
22
23const state = {
24 count: 0
25}
26
27const actions = {
28 down: value => state => ({ count: state.count - value }),
29 up: value => state => ({ count: state.count + value })
30}
31
32const view = (state, actions) => (
33 <div>
34 <h1>{state.count}</h1>
35 <button onclick={() => actions.down(1)}>-</button>
36 <button onclick={() => actions.up(1)}>+</button>
37 </div>
38)
39
40app(state, actions, view, document.body)
41```
42
43Hyperapp 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.
44
45This 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/en/babel-plugin-transform-react-jsx) and add the pragma option to your <samp>.babelrc</samp> file.
46
47```json
48{
49 "plugins": [["@babel/plugin-transform-react-jsx", { "pragma": "h" }]]
50}
51```
52
53JSX 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.
54
55```jsx
56const view = (state, actions) =>
57 h("div", {}, [
58 h("h1", {}, state.count),
59 h("button", { onclick: () => actions.down(1) }, "-"),
60 h("button", { onclick: () => actions.up(1) }, "+")
61 ])
62```
63
64Note 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).
65
66## Installation
67
68Install with npm or Yarn.
69
70<pre>
71npm i <a href=https://www.npmjs.com/package/hyperapp>hyperapp</a>
72</pre>
73
74Then with a module bundler like [Rollup](https://rollupjs.org) or [Webpack](https://webpack.js.org), use as you would anything else.
75
76```js
77import { h, app } from "hyperapp"
78```
79
80If 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.
81
82```html
83<script src="https://unpkg.com/hyperapp"></script>
84```
85
86## Overview
87
88Hyperapp applications consist of three interconnected parts: the [state](#state), [view](#view), and [actions](#actions).
89
90Once 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 to redraw. After processing an action, the new state is presented back to the user.
91
92### State
93
94The 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.
95
96```js
97const state = {
98 count: 0
99}
100```
101
102Like 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.
103
104```js
105const state = {
106 top: {
107 count: 0
108 },
109 bottom: {
110 count: 0
111 }
112}
113```
114
115Because 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.
116
117#### Local State
118
119Hyperapp does not have the concept of local state. Instead, components are pure functions that return a virtual DOM representation of the global state.
120
121### Actions
122
123The 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.
124
125To 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 in your actions to automatically redraw the view on state changes.
126
127```js
128const actions = {
129 setValue: value => ({ value })
130}
131```
132
133Instead 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.
134
135```js
136const actions = {
137 down: value => state => ({ count: state.count - value }),
138 up: value => state => ({ count: state.count + value })
139}
140```
141
142State 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).
143
144Immutability 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.
145
146#### Asynchronous Actions
147
148Actions 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.
149
150```js
151const actions = {
152 upLater: value => (state, actions) => {
153 setTimeout(actions.up, 1000, value)
154 },
155 up: value => state => ({ count: state.count + value })
156}
157```
158
159An 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.
160
161```js
162const actions = {
163 upLater: () => async (state, actions) => {
164 await new Promise(done => setTimeout(done, 1000))
165 actions.up(10)
166 },
167 up: value => state => ({ count: state.count + value })
168}
169```
170
171#### Nested Actions
172
173Actions 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.
174
175```jsx
176const state = {
177 counter: {
178 count: 0
179 }
180}
181
182const actions = {
183 counter: {
184 down: value => state => ({ count: state.count - value }),
185 up: value => state => ({ count: state.count + value })
186 }
187}
188```
189
190#### Interoperability
191
192The <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.
193
194To 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.
195
196```jsx
197const main = app(state, actions, view, document.body)
198
199setInterval(main.up, 250, 1)
200setInterval(main.down, 500, 1)
201```
202
203Because state updates are always immutable, returning a reference to the current state will not schedule a view redraw.
204
205```jsx
206const actions = {
207 getState: () => state => state
208}
209```
210
211### View
212
213Every 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.
214
215```js
216import { h } from "hyperapp"
217
218export const view = (state, actions) =>
219 h("div", {}, [
220 h("h1", {}, state.count),
221 h("button", { onclick: () => actions.down(1) }, "-"),
222 h("button", { onclick: () => actions.up(1) }, "+")
223 ])
224```
225
226A 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.
227
228```jsx
229{
230 nodeName: "div",
231 attributes: {},
232 children: [
233 {
234 nodeName: "h1",
235 attributes: {},
236 children: [0]
237 },
238 {
239 nodeName: "button",
240 attributes: { ... },
241 children: ["-"]
242 },
243 {
244 nodeName: "button",
245 attributes: { ... },
246 children: ["+"]
247 }
248 ]
249}
250```
251
252The 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.
253
254It 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.
255
256### Mounting
257
258To mount your application on 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.
259
260```jsx
261app(state, actions, view, container)
262```
263
264Hyperapp 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.
265
266This is how we can recycle server-rendered content out the counter example from before. See [Getting Started](#getting-started) for the application code.
267
268```html
269<!doctype html>
270<html>
271<head>
272 <meta charset="utf-8">
273 <script defer src="bundle.js"></script>
274</head>
275
276<body>
277 <div>
278 <h1>0</h1>
279 <button>-</button>
280 <button>+</button>
281 </div>
282</body>
283</html>
284```
285
286### Components
287
288A 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.
289
290Here'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).
291
292```jsx
293import { h } from "hyperapp"
294
295const TodoItem = ({ id, value, done, toggle }) => (
296 <li
297 class={done && "done"}
298 onclick={() =>
299 toggle({
300 value: done,
301 id: id
302 })
303 }
304 >
305 {value}
306 </li>
307)
308
309export const view = (state, actions) => (
310 <div>
311 <h1>Todo</h1>
312 <ul>
313 {state.todos.map(({ id, value, done }) => (
314 <TodoItem id={id} value={value} done={done} toggle={actions.toggle} />
315 ))}
316 </ul>
317 </div>
318)
319```
320
321If 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.
322
323```jsx
324const TodoList = ({ todos, toggle }) =>
325 todos.map(todo => <TodoItem {...todo} toggle={toggle} />)
326```
327
328#### Lazy Components
329
330Components 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.
331
332```jsx
333import { h } from "hyperapp"
334
335export const Up = ({ by }) => (state, actions) => (
336 <button onclick={() => actions.up(by)}>+ {by}</button>
337)
338
339export const Down = ({ by }) => (state, actions) => (
340 <button onclick={() => actions.down(by)}>- {by}</button>
341)
342
343export const Double = () => (state, actions) => (
344 <button onclick={() => actions.up(state.count)}>+ {state.count}</button>
345)
346
347export const view = (state, actions) => (
348 <main>
349 <h1>{state.count}</h1>
350 <Up by={2} />
351 <Down by={1} />
352 <Double />
353 </main>
354)
355```
356
357#### Handling Component State
358
359Suppose you have a list of questions with answers that are collapsed initially. A flag `answerIsOpen` is used to determine if a question's answer is open.
360
361Since there is no concept of local state in Hyperapp, the global state is always updated rather than an individual component's state.
362
363To update a single question's state, the entire `questions` array will be mapped to a new array where the `answerIsOpen` property will be toggled if the question matches the one belonging to the component.
364
365[View the example online](https://codepen.io/anon/pen/ZogRYP).
366
367#### Children Composition
368
369Components receive their children elements via the second argument, allowing you and other components to pass arbitrary children down to them.
370
371```jsx
372const Box = ({ color }, children) => (
373 <div class={`box box-${color}`}>{children}</div>
374)
375
376const HelloBox = ({ name }) => (
377 <Box color="green">
378 <h1 class="title">Hello, {name}!</h1>
379 </Box>
380)
381```
382
383## Supported Attributes
384
385Supported 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.
386
387### Styles
388
389The <samp>style</samp> attribute expects a plain object rather than a string as in HTML.
390Each declaration consists of a style name property written in <samp>camelCase</samp> and a value. CSS variables are supported too.
391
392Individual 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>.
393
394```jsx
395import { h } from "hyperapp"
396
397export const Jumbotron = ({ text }) => (
398 <div
399 style={{
400 color: "white",
401 fontSize: "32px",
402 textAlign: center,
403 backgroundImage: `url(${imgUrl})`
404 }}
405 >
406 {text}
407 </div>
408)
409```
410
411### Lifecycle Events
412
413You 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.
414
415Note 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.
416
417#### oncreate
418
419This 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.
420
421```jsx
422import { h } from "hyperapp"
423
424export const Textbox = ({ placeholder }) => (
425 <input
426 type="text"
427 placeholder={placeholder}
428 oncreate={element => element.focus()}
429 />
430)
431```
432
433#### onupdate
434
435This 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.
436
437```jsx
438import { h } from "hyperapp"
439
440export const Textbox = ({ placeholder }) => (
441 <input
442 type="text"
443 placeholder={placeholder}
444 onupdate={(element, oldAttributes) => {
445 if (oldAttributes.placeholder !== placeholder) {
446 // Handle changes here!
447 }
448 }}
449 />
450)
451```
452
453#### onremove
454
455This 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.
456
457```jsx
458import { h } from "hyperapp"
459
460export const MessageWithFadeout = ({ title }) => (
461 <div onremove={(element, done) => fadeout(element).then(done)}>
462 <h1>{title}</h1>
463 </div>
464)
465```
466
467#### ondestroy
468
469This 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.
470
471```jsx
472import { h } from "hyperapp"
473
474export const Camera = ({ onerror }) => (
475 <video
476 poster="loading.png"
477 oncreate={element => {
478 navigator.mediaDevices
479 .getUserMedia({ video: true })
480 .then(stream => (element.srcObject = stream))
481 .catch(onerror)
482 }}
483 ondestroy={element => element.srcObject.getTracks()[0].stop()}
484 />
485)
486```
487
488### Keys
489
490Keys help 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 allows us to re-order the element into its new position, if the position changed, rather than risk destroying it.
491
492```jsx
493import { h } from "hyperapp"
494
495export const ImageGallery = ({ images }) =>
496 images.map(({ hash, url, description }) => (
497 <li key={hash}>
498 <img src={url} alt={description} />
499 </li>
500 ))
501```
502
503Keys must be unique among sibling nodes. Don't use an array index as the key, if the index also specifies the order of siblings. If the position and number of items in a list are fixed, it will make no difference, but if the list is dynamic, the key will change every time the tree is rebuilt.
504
505```jsx
506import { h } from "hyperapp"
507
508export const PlayerList = ({ players }) =>
509 players
510 .slice()
511 .sort((player, nextPlayer) => nextPlayer.score - player.score)
512 .map(player => (
513 <li key={player.username} class={player.isAlive ? "alive" : "dead"}>
514 <PlayerProfile {...player} />
515 </li>
516 ))
517```
518
519#### Top-Level Nodes
520
521Keys 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.
522
523## Packages
524
525The Hyperapp community has provided several packages for additional functionality to use with Hyperapp.
526
527* [**@hyperapp/router**](https://github.com/hyperapp/router) - Declarative routing for Hyperapp using the History API.
528* [**@hyperapp/transitions**](https://github.com/hyperapp/transitions) - Animate elements as they are appear, disappear and move around on the page.
529* [**@hyperapp/render**](https://github.com/hyperapp/render) - Render Hyperapp views to an HTML string.
530
531## Starter Kits
532
533If you are looking to get started with a production app using a build setup quickly and easily, there are community starter kits to help.
534
535* [**Hyperapp Kit**](https://github.com/atomiks/hyperapp-kit) - A starter kit using [Parcel](https://parceljs.org/) that supports prerendering for improved inital page load performance.
536* [**Hyperapp One**](https://github.com/selfup/hyperapp-one) - A starter kit using [Webpack](https://webpack.js.org) for quickstarting an application with AirBnB's JavaScript Styleguide.
537
538## Links
539
540- [Slack](https://hyperappjs.herokuapp.com)
541- [Twitter](https://twitter.com/hyperappJS)
542- [Examples](https://codepen.io/search/pens/?q=hyperapp)
543- [/r/hyperapp](https://www.reddit.com/r/hyperapp)
544
545## License
546
547Hyperapp is MIT licensed. See [LICENSE](LICENSE.md).