├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── docs ├── api │ ├── app.md │ ├── h.md │ ├── memo.md │ └── text.md ├── architecture │ ├── actions.md │ ├── dispatch.md │ ├── effects.md │ ├── flowchart.md │ ├── state.md │ ├── subscriptions.md │ └── views.md ├── reference.md └── tutorial.md ├── index.d.ts ├── index.js ├── package.json ├── packages ├── dom │ ├── README.md │ ├── index.js │ └── package.json ├── events │ ├── README.md │ ├── index.js │ └── package.json ├── html │ ├── README.md │ ├── index.js │ └── package.json ├── svg │ ├── README.md │ ├── index.js │ └── package.json └── time │ ├── README.md │ ├── index.js │ └── package.json └── tests └── index.test.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | test: 5 | env: 6 | NODE_ENV: development 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [14.x] 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v1 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: Test 19 | run: | 20 | npm install -g codecov 21 | npm install 22 | npm test 23 | codecov 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.map 3 | *.br 4 | *.gz 5 | 6 | package-lock.json 7 | node_modules 8 | coverage 9 | private 10 | 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © Jorge Bucaran <> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperapp 2 | 3 | > The tiny framework for building hypertext applications. 4 | 5 | - **Do more with less**—We have minimized the concepts you need to learn to get stuff done. Views, actions, effects, and subscriptions are all pretty easy to get to grips with and work together seamlessly. 6 | - **Write what, not how**—With a declarative API that's easy to read and fun to write, Hyperapp is the best way to build purely functional, feature-rich, browser-based apps using idiomatic JavaScript. 7 | - **Smaller than a favicon**—1 kB, give or take. Hyperapp is an ultra-lightweight Virtual DOM, [highly-optimized diff algorithm](https://javascript.plainenglish.io/javascript-frameworks-performance-comparison-2020-cd881ac21fce), and state management library obsessed with minimalism. 8 | 9 | Here's the first example to get you started. [Try it here](https://codepen.io/jorgebucaran/pen/zNxZLP?editors=1000)—no build step required! 10 | 11 | 12 | ```html 13 | 41 | 42 |
43 | ``` 44 | 45 | [Check out more examples](https://codepen.io/collection/nLLvrz?grid_type=grid) 46 | 47 | The app starts by setting the initial state and rendering the view on the page. User input flows into actions, whose function is to update the state, causing Hyperapp to re-render the view. 48 | 49 | When describing how a page looks in Hyperapp, we don't write markup. Instead, we use `h()` and `text()` to create a lightweight representation of the DOM (or virtual DOM for short), and Hyperapp takes care of updating the real DOM efficiently. 50 | 51 | ## Installation 52 | 53 | ```console 54 | npm install hyperapp 55 | ``` 56 | 57 | ## Documentation 58 | 59 | Learn the basics in the [Tutorial](docs/tutorial.md), check out the [Examples](https://codepen.io/collection/nLLvrz?grid_type=grid), or visit the [Reference](docs/reference.md). 60 | 61 | ## Packages 62 | 63 | Official packages provide access to [Web Platform](https://platform.html5.org) APIs in a way that makes sense for Hyperapp. For third-party packages and real-world examples, browse the [Hyperawesome](https://github.com/jorgebucaran/hyperawesome) collection. 64 | 65 | | Package | Status | About | 66 | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | 67 | | [`@hyperapp/dom`](/packages/dom) | [![npm](https://img.shields.io/npm/v/@hyperapp/dom.svg?style=for-the-badge&color=0366d6&label=)](https://www.npmjs.com/package/@hyperapp/dom) | Inspect the DOM, focus and blur. | 68 | | [`@hyperapp/svg`](/packages/svg) | [![npm](https://img.shields.io/npm/v/@hyperapp/svg.svg?style=for-the-badge&color=0366d6&label=)](https://www.npmjs.com/package/@hyperapp/svg) | Draw SVG with plain functions. | 69 | | [`@hyperapp/html`](/packages/html) | [![npm](https://img.shields.io/npm/v/@hyperapp/html.svg?style=for-the-badge&color=0366d6&label=)](https://www.npmjs.com/package/@hyperapp/html) | Write HTML with plain functions. | 70 | | [`@hyperapp/time`](/packages/time) | [![npm](https://img.shields.io/npm/v/@hyperapp/time.svg?style=for-the-badge&color=0366d6&label=)](https://www.npmjs.com/package/@hyperapp/time) | Subscribe to intervals, get the time now. | 71 | | [`@hyperapp/events`](/packages/events) | [![npm](https://img.shields.io/npm/v/@hyperapp/events.svg?style=for-the-badge&color=0366d6&label=)](https://www.npmjs.com/package/@hyperapp/events) | Subscribe to mouse, keyboard, window, and frame events. | 72 | | [`@hyperapp/http`](/packages/http) | [![npm](https://img.shields.io/badge/-planned-6a737d?style=for-the-badge&label=)](https://www.npmjs.com/package/@hyperapp/http) | Talk to servers, make HTTP requests ([#1027](https://github.com/jorgebucaran/hyperapp/discussions/1027)). | 73 | | [`@hyperapp/random`](/packages/random) | [![npm](https://img.shields.io/badge/-planned-6a737d?style=for-the-badge&label=)](https://www.npmjs.com/package/@hyperapp/random) | Declarative random numbers and values. | 74 | | [`@hyperapp/navigation`](/packages/navigation) | [![npm](https://img.shields.io/badge/-planned-6a737d?style=for-the-badge&label=)](https://www.npmjs.com/package/@hyperapp/navigation) | Subscribe and manage the browser URL history. | 75 | 76 | > Need to create your own effects and subscriptions? [You can do that too](docs/reference.md). 77 | 78 | ## Help, I'm stuck! 79 | 80 | If you've hit a stumbling block, hop on our [Discord](https://discord.gg/eFvZXzXF9U) server to get help, and if you remain stuck, [please file an issue](https://github.com/jorgebucaran/hyperapp/issues/new), and we'll help you figure it out. 81 | 82 | ## Contributing 83 | 84 | Hyperapp is free and open-source software. If you want to support Hyperapp, becoming a contributor or [sponsoring](https://github.com/sponsors/jorgebucaran) is the best way to give back. Thank you to everyone who already contributed to Hyperapp! <3 85 | 86 | [![](https://opencollective.com/hyperapp/contributors.svg?width=1024&button=false)](https://github.com/jorgebucaran/hyperapp/graphs/contributors) 87 | 88 | ## License 89 | 90 | [MIT](LICENSE.md) 91 | -------------------------------------------------------------------------------- /docs/api/app.md: -------------------------------------------------------------------------------- 1 | # `app()` 2 | 3 | Initializes and mounts a Hyperapp application. 4 | 5 | ```elm 6 | app : ({ Init, View, Node, Subscriptions?, Dispatch? }) -> DispatchFn 7 | ``` 8 | 9 | | Prop | Type | Required? | 10 | | -------------------------------- | --------------------------------------------------------------------------- | -------------------------------- | 11 | | [init:](#init) | | No | 12 | | [view:](#view) | [View](../architecture/views.md) | No | 13 | | [node:](#node) | DOM element | **Yes when `view:` is present.** | 14 | | [subscriptions:](#subscriptions) | Function | No | 15 | | [dispatch:](#dispatch) | [Dispatch Initializer](../architecture/dispatch.md#dispatch-initializer) | No | 16 | 17 | | Return Value | Type | 18 | | --------------------------------------- | -------- | 19 | | [dispatch](../architecture/dispatch.md) | Function | 20 | 21 | ```js 22 | import { app, h, text } from "hyperapp" 23 | 24 | app({ 25 | init: { message: "Hello World!" }, 26 | view: (state) => h("p", {}, text(state.message)), 27 | node: document.getElementById("app"), 28 | }) 29 | ``` 30 | 31 | ## `init:` 32 | 33 | _(default value: `{}`)_ 34 | 35 | Initializes the app by either setting the initial value of the [state](../architecture/state.md) or taking an [action](../architecture/actions.md). It takes place before the first view render and subscriptions registration. 36 | 37 | ### Forms of `init:` 38 | 39 | - `init: state` 40 | 41 | Sets the initial state directly. 42 | 43 | ```js 44 | app({ 45 | init: { counter: 0 }, 46 | // ... 47 | }) 48 | ``` 49 | 50 | - `init: [state, ...effects]` 51 | 52 | Sets the initial state and then runs the given list of [effects](../architecture/effects.md). 53 | 54 | ```js 55 | app({ 56 | init: [ 57 | { loading: true }, 58 | log("Loading..."), 59 | load("myUrl?init", DoneAction), 60 | ], 61 | // ... 62 | }) 63 | ``` 64 | 65 | - `init: Action` 66 | 67 | Runs the given [Action](../architecture/actions.md). 68 | 69 | This form is useful when the action can be reused later. The state passed to the action in this case is `undefined`. 70 | 71 | ```js 72 | const Reset = (_state) => ({ counter: 0 }) 73 | 74 | app({ 75 | init: Reset, 76 | // ... 77 | }) 78 | ``` 79 | 80 | - `init: [Action, payload]` 81 | 82 | Runs the given [Action](../architecture/actions.md) with a payload. 83 | 84 | ```js 85 | const SetCounter = (_state, n) => ({ counter: n }) 86 | 87 | app({ 88 | init: [SetCounter, 10], 89 | // ... 90 | }) 91 | ``` 92 | 93 | ## `view:` 94 | 95 | The [top-level view](../architecture/views.md#top-level-view) that represents the app as a whole. There can only be one top-level view in your app. Hyperapp uses this to map your state to your UI for rendering the app. Every time the [state](../architecture/state.md) of the application changes, this function will be called to render the UI based on the new state, using the logic you've defined inside of it. 96 | 97 | ```js 98 | app({ 99 | // ... 100 | view: (state) => h("main", {}, [ 101 | outworld(state), 102 | netherrealm(state), 103 | ]), 104 | }) 105 | ``` 106 | 107 | 108 | 109 | ## `node:` 110 | 111 | The DOM element to render the virtual DOM over (the **mount node**). The given element is replaced by a Hyperapp application. This process is called **mounting**. It's common to define an intentionally empty element in your HTML which has an ID that your app can use for mounting. If the mount node had content within it then Hyperapp will attempt to [recycle](../architecture/views.md#recycling) that content. 112 | 113 | ```html 114 |
115 | ``` 116 | 117 | ```js 118 | app({ 119 | // ... 120 | node: document.getElementById("app"), 121 | }) 122 | ``` 123 | 124 | ## `subscriptions:` 125 | 126 | A function that returns an array of [subscriptions](../architecture/subscriptions.md) for a given state. Every time the [state](../architecture/state.md) of the application changes, this function will be called to determine the current subscriptions. 127 | 128 | If a subscription entry is falsy then the subscription that was at that spot, if any, will be considered unsubscribed from and will be cleaned up. 129 | 130 | If `subscriptions:` is omitted the app has no subscriptions. It behaves the same as if you were using: `subscriptions: (state) => []` 131 | 132 | ```js 133 | import { onKey } from "./subs" 134 | 135 | app({ 136 | // ... 137 | subscriptions: (state) => [ 138 | onKey("w", MoveForward), 139 | onKey("a", MoveBackward), 140 | onKey("s", StrafeLeft), 141 | onKey("d", StrafeRight), 142 | state.playingDOOM1993 || onKey(" ", Jump), 143 | ], 144 | }) 145 | ``` 146 | 147 | 148 | 149 | ## `dispatch:` 150 | 151 | A [dispatch initializer](../architecture/dispatch.md#dispatch-initializer) that can create a [custom dispatch function](../architecture/dispatch.md#custom-dispatching) to use instead of the default dispatch. Allows tapping into dispatches for debugging, testing, telemetry etc. 152 | 153 | ## Return Value 154 | 155 | `app()` returns the [dispatch](../architecture/dispatch.md) function your app uses. This can be handy if you want to control your app externally, ie where only a subsection of your app is implemented with Hyperapp. 156 | 157 | Calling the dispatch function with no arguments frees the app's resources and runs every active subscription's cleanup function. 158 | 159 | ## Other Considerations 160 | 161 | - You can embed your Hyperapp application within another already existing Hyperapp application or an app that was built with some other framework. 162 | 163 | - Multiple Hyperapp applications can coexist on the page simultaneously. They each have their own state and behave independently relative to each other. They can communicate with each other using subscriptions and effects (i.e. using events). 164 | -------------------------------------------------------------------------------- /docs/api/h.md: -------------------------------------------------------------------------------- 1 |

h()

2 | 3 | **_Definition:_** 4 | 5 | > A function that creates [virtual DOM nodes (VNodes)](../architecture/views.md#virtual-dom) which are used for defining [views](../architecture/views.md). 6 | 7 | **_Import & Usage:_** 8 | 9 | ```js 10 | import { h } from "hyperapp" 11 | 12 | // ... 13 | 14 | h(tag, props, children) 15 | ``` 16 | 17 | **_Signature & Parameters:_** 18 | 19 | ```elm 20 | h : (String, Object, VNode? | [...VNodes]?) -> VNode 21 | ``` 22 | 23 | | Parameters | Type | Required? | 24 | | --------------------- | ------------------------ | --------- | 25 | | [tag](#tag) | String | yes :100: | 26 | | [props](#props) | Object | yes :100: | 27 | | [children](#children) | VNode or array of VNodes | no | 28 | 29 | | Return Value | Type | 30 | | ---------------------------------------------------- | ----- | 31 | | [virtual node](../architecture/views.md#virtual-dom) | VNode | 32 | 33 | `h()` effectively represents the page elements used in your app. Because it's just JavaScript we can easily render whichever elements we see fit in a dynamical manner. 34 | 35 | ```js 36 | const hobbit = (wearingElvenCloak) => 37 | h("div", {}, [ 38 | !wearingElvenCloak && h("p", {}, text("Frodo")), 39 | ]) 40 | ``` 41 | 42 | 43 | 44 | --- 45 | 46 | ## Parameters 47 | 48 | ### `tag` 49 | 50 | Name of the node. For example, `div`, `h1`, `button`, etc. Essentially any [HTML element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element) or [SVG element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element) or [custom element](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements). 51 | 52 | ### `props` 53 | 54 | HTML or SVG properties ("props") for the DOM element are defined using an object where the keys are the property names and the values are the corresponding property values. 55 | 56 | ```js 57 | h("input", { 58 | type: "checkbox", 59 | id: "picard", 60 | checked: state.engaging, 61 | }) 62 | ``` 63 | 64 | 65 | 66 | Hyphenated props will need to be quoted in order to use them. The quotes are necessary to abide by JavaScript syntax restrictions. 67 | 68 | ```js 69 | h("q", { "data-zoq-fot-pik": "Frungy" }, text("The Sport of Kings!")) 70 | ``` 71 | 72 | 73 | 74 | Certain properties are treated in a special way by Hyperapp. 75 | 76 | #### `class:` 77 | 78 | The [classes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class) to use with the VNode. The `class` prop can be given in various formats: 79 | 80 | - As a string representing a class name. Because of the way Hyperapp internally processes class strings they're allowed to have a space-separated list of different class names. 81 | 82 | ```js 83 | h("div", { class: "muggle-studies" }) 84 | ``` 85 | 86 | 87 | 88 | - As an object where the keys are the names of the classes while the values are booleans for toggling the classes. 89 | 90 | ```js 91 | h("div", { class: { arithmancy: true, "study-of-ancient-runes": true } }) 92 | ``` 93 | 94 | 95 | 96 | - As an array that contains any combination of the various formats including this one. 97 | 98 | ```js 99 | h("div", { class: ["magical theory", "xylomancy"] }) 100 | ``` 101 | 102 | 103 | 104 | This means the array format is recursive. 105 | 106 | ```js 107 | h("input", { 108 | type: "range", 109 | class: [ 110 | { dragonzord: state.green && !state.white }, 111 | "mastodon", 112 | state.pink && "pterodactyl", 113 | [ 114 | { triceratops: state.blue }, 115 | "sabretooth-tiger", 116 | state.red && "tyrannosaurus", 117 | ], 118 | ], 119 | }) 120 | ``` 121 | 122 | 131 | 132 | #### `style:` 133 | 134 | The [inline CSS styles](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/style) to use with the VNode. The `style` prop can be an object of [CSS properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference) where the keys are the CSS property names and the values are the corresponding CSS property values. Hyphenated CSS property names can either be in camelCase or quoted to abide by JavaScript syntax restrictions. 135 | 136 | ```js 137 | h( 138 | "span", 139 | { 140 | style: { 141 | backgroundColor: "white", 142 | color: "blue", 143 | display: "inline-block", 144 | "font-weight": "bold", 145 | }, 146 | }, 147 | text("+\\") 148 | ) 149 | ``` 150 | 151 | 152 | 153 | #### `key:` 154 | 155 | A unique string per VNode that helps Hyperapp track if VNodes are changed, added, or removed in situations where it's unable to do so, such as in arrays. 156 | 157 | ```js 158 | const pokedex = (pokemon) => 159 | h( 160 | "ul", 161 | {}, 162 | pokemon.map((p) => h("li", { key: p.id }, text(p.name))) 163 | ) 164 | ``` 165 | 166 | 167 | 168 | #### Event Listeners 169 | 170 | Props that represent event listeners, such as [`onclick`](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event), [`onchange`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event), [`oninput`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event), etc. are where you would assign [actions](../architecture/actions.md) to VNodes. 171 | 172 | Synthetic events can be added in the same way as long as their name starts with "on", so an event created with 173 | ```js 174 | const buildEvent = new Event("build") 175 | ``` 176 | can be used like this: 177 | ```js 178 | h("button", { onbuild: BuildAction }, text("Click Me")) 179 | ``` 180 | 181 | ### `children` 182 | 183 | The children of the VNode are other VNodes which are directly nested within it. 184 | 185 | `children` can either be given as a single child VNode: 186 | 187 | ```js 188 | h("q", {}, text("There is no spoon.")) 189 | ``` 190 | 191 | 192 | 193 | or as an array of child VNodes: 194 | 195 | ```js 196 | h("q", {}, [ 197 | text("I know Kung Fu."), 198 | h("em", {}, text("Show me.")), 199 | ]) 200 | ``` 201 | 202 | 203 | 204 | --- 205 | 206 | ## Other Considerations 207 | 208 | ### JSX Support 209 | 210 | Hyperapp doesn't support [JSX](https://reactjs.org/docs/introducing-jsx.html) out-of-the-box. That said you can use this custom JSX function to be able to use it. 211 | 212 | ```js 213 | import { h, text } from "hyperapp" 214 | 215 | const jsxify = (h) => (type, props, ...children) => 216 | typeof type === "function" 217 | ? type(props, children) 218 | : h( 219 | type, 220 | props || {}, 221 | [].concat(...children).map((x) => 222 | typeof x === "string" || typeof x === "number" ? text(x) : x 223 | ) 224 | ) 225 | 226 | const jsx = jsxify(h) /** @jsx jsx */ 227 | ``` 228 | -------------------------------------------------------------------------------- /docs/api/memo.md: -------------------------------------------------------------------------------- 1 | # `memo()` 2 | 3 | **_Definition:_** 4 | 5 | > A wrapper function to cache your [views](../architecture/views.md) based on properties you pass into them. 6 | 7 | **_Import & Usage:_** 8 | 9 | ```js 10 | import { memo } from "hyperapp" 11 | 12 | // ... 13 | 14 | memo(view, props) 15 | ``` 16 | 17 | **_Signature & Parameters:_** 18 | 19 | ```elm 20 | memo : (View, IndexableData) -> VNode 21 | ``` 22 | 23 | | Parameters | Type | Required? | 24 | | ------------- | ----------------------------------------------- | --------- | 25 | | [view](#view) | [View](../architecture/views.md) | yes :100: | 26 | | [data](#data) | anything indexable (i.e. Array, Object, String) | no | 27 | 28 | | Return Value | Type | 29 | | ---------------------------------------------------- | ----- | 30 | | [virtual node](../architecture/views.md#virtual-dom) | VNode | 31 | 32 | `memo()` lets you take advantage of a performance optimization technique known as [memoization](../architecture/views.md#memoization). 33 | 34 | --- 35 | 36 | ## Parameters 37 | 38 | ### `view` 39 | 40 | A [view](../architecture/views.md) you want [memoized](../architecture/views.md#memoization). 41 | 42 | ### `data` 43 | 44 | The data to pass along to the wrapped view function instead of the [state](../architecture/state.md). The wrapped view is recomputed when the data for it changes. 45 | 46 | --- 47 | 48 | ## Example 49 | 50 | Here we have a list of numbers displayed in a regular view as well as a memoized version of the same view. One button changes the list which affects both views. Another button updates a counter which affects the counter's view and also the regular view of the list but not the memoized view of the list. 51 | 52 | ```js 53 | import { h, text, app, memo } from "hyperapp" 54 | 55 | const randomHex = () => "0123456789ABCDEF"[Math.floor(Math.random() * 16)] 56 | const randomColor = () => "#" + Array.from({ length: 6 }, randomHex).join("") 57 | 58 | const listView = (list) => 59 | h("p", { 60 | style: { 61 | backgroundColor: randomColor(), 62 | color: randomColor(), 63 | }, 64 | }, text(list)) 65 | 66 | const MoreItems = (state) => ({ ...state, list: [...state.list, randomHex()] }) 67 | const Increment = (state) => ({ ...state, counter: state.counter + 1 }) 68 | 69 | app({ 70 | init: { 71 | list: ["a", "b", "c"], 72 | counter: 0, 73 | }, 74 | view: (state) => 75 | h("main", {}, [ 76 | h("button", { onclick: MoreItems }, text("Grow list")), 77 | h("button", { onclick: Increment }, text("+1 to counter")), 78 | h("p", {}, text(`Counter: ${state.counter}`)), 79 | h("p", {}, text("Regular view showing list:")), 80 | listView(state.list), 81 | h("p", {}, text("Memoized view showing list:")), 82 | memo(listView, state.list), 83 | ]), 84 | node: document.querySelector("main"), 85 | }) 86 | ``` 87 | 88 | --- 89 | 90 | ## Other Considerations 91 | 92 | ### Performance 93 | 94 | Using `memo()` too often will lead to [degraded performance](../architecture/views.md#performance). Only use `memo()` when you know it will improve rendering. When in doubt, benchmark! 95 | 96 | ### Memo Data Gotcha 97 | 98 | When Hyperapp checks memo data for changes it will do index-for-index comparisons between what the data currently is with how it was in the previous state. So, any indexable type like strings and arrays can be compared with one another and in certain edge cases be considered "equal" when it comes to determining if a re-render should happen. 99 | 100 | We can modify parts of the example from earlier to illustrate this: 101 | 102 | ```js 103 | // ... 104 | 105 | const MoreItems = (state) => ({ 106 | ...state, 107 | list: Array.isArray(state.list) 108 | ? [...state.list, randomHex()] 109 | : state.list + "" + randomHex(), 110 | }) 111 | 112 | const Increment = (state) => ({ 113 | ...state, 114 | counter: state.counter + 1, 115 | 116 | // The following should cause the memoized view to rerender but it doesn't. 117 | list: Array.isArray(state.list) 118 | ? state.list.join("") 119 | : state.list.split(""), 120 | }) 121 | 122 | // ... 123 | ``` 124 | -------------------------------------------------------------------------------- /docs/api/text.md: -------------------------------------------------------------------------------- 1 | # `text()` 2 | 3 | **_Definition:_** 4 | 5 | > A function that creates a [virtual DOM node (VNode)](../architecture/views.md#virtual-dom) out of a given value. 6 | 7 | **_Import & Usage:_** 8 | 9 | You'll normally use it with [`h()`](./h.md). 10 | 11 | ```js 12 | import { text } from "hyperapp" 13 | 14 | // ... 15 | 16 | h("p", {}, text(content)) 17 | ``` 18 | 19 | **_Signature & Parameters:_** 20 | 21 | ```elm 22 | text : (String | Number) -> VNode 23 | ``` 24 | 25 | | Parameter | Type | Required? | Notes | 26 | | ------------------- | ----------------------------------------------------- | -------------- | --------------------------------------- | 27 | | [content](#content) | any (sort of), but meaningfully only String or Number | yes :100: | | 28 | | node | DOM element | prohibited :x: | This is for internal Hyperapp use only! | 29 | 30 | | Return Value | Type | 31 | | --------------------------------------------------------- | ----- | 32 | | [virtual text node](../architecture/views.md#virtual-dom) | VNode | 33 | 34 | You would use `text()` to insert regular text content into your views. 35 | 36 | ```js 37 | h("p", {}, text("You must construct additional pylons.")) 38 | ``` 39 | 40 | 41 | 42 | Of course, this may include anything relevant from the [current state](../architecture/state.md). 43 | 44 | ```js 45 | h("p", {}, text(state.message)) 46 | ``` 47 | 48 | `text()` exists as the way of defining text nodes such that Hyperapp's implementation is kept simpler than it otherwise would have been. 49 | 50 | --- 51 | 52 | ## Parameters 53 | 54 | ### `content` 55 | 56 | While `content` can technically be anything, what will actually be used for the content of the VDOM element will be the stringified version of `content`. So, using actual strings and numbers makes a lot of sense but using arrays will probably be formatted in a way you don't want and objects won't work well at all. 57 | -------------------------------------------------------------------------------- /docs/architecture/actions.md: -------------------------------------------------------------------------------- 1 | # Actions 2 | 3 | **_Definition:_** 4 | 5 | > An **action** is a message used within your app that signals the valid way to change [state](state.md). 6 | 7 | An action is implemented by a deterministic function that produces no side-effects which describes a transition between the current state and the next state and in so doing may optionally list out [effects](effects.md) to be run as well. 8 | 9 | Actions are dispatched by either DOM events in your app, [effecters](effects.md#effecters), or [subscribers](subscriptions.md#subscribers). When dispatched, actions always implicitly receive the current state as their first argument. 10 | 11 | **_Signature:_** 12 | 13 | ```elm 14 | Action : (State, Payload?) -> NextState 15 | | [NextState, ...Effects] 16 | | OtherAction 17 | | [OtherAction, Payload?] 18 | ``` 19 | 20 | **_Naming Recommendation:_** 21 | 22 | Actions are recommended to be named in `PascalCase` to signal to the developer that they should be thought of as messages intended for use by Hyperapp itself. It is also recommended to use a verb (for instance `Add`) or a verb-noun phrase (`AddArticle`) for the name. The verb can be either in its imperative form, like `IncrementBy`, `ToggleVisibility`, `GetPizzas` or `SaveAddress`, or in the past tense form, for instance `GotData`, `StoppedCounting` – especially when the action is used for a "final" state transition at the end of an action-effect-chain. 23 | 24 | --- 25 | 26 | ## Simple State Transitions 27 | 28 | The simplest possible action merely returns the current state: 29 | 30 | ```js 31 | // Action : (State) -> SameState 32 | const Identity = (state) => state 33 | ``` 34 | 35 | It seems useless at first but can be helpful as a placeholder for other actions while prototyping a new app or [component](views.md#components). 36 | 37 | Probably the most common way to use an action is to assign it as an event handler for one of the nodes in your view. 38 | 39 | ```js 40 | h("button", { onclick: Identity }, text("Do Nothing")) 41 | ``` 42 | 43 | The next simplest type of action merely sets the state. 44 | 45 | ```js 46 | // Action : () -> ForcedState 47 | const FeedFace = () => 0xfeedface 48 | ``` 49 | 50 | 51 | 52 | 53 | But you'll most likely want to do actual state transitions. 54 | 55 | ```js 56 | // Action : (State) -> NewState 57 | const Increment = (state) => ({ ...state, value: state.value + 1 }) 58 | 59 | // ... 60 | 61 | h("button", { onclick: Increment }, text("+")) 62 | ``` 63 | 64 | --- 65 | 66 | ## Payloads 67 | 68 | Actions can also accept an optional **payload** along with the current state. 69 | 70 | ```js 71 | // Action : (State, Payload?) -> NewState 72 | const AddBy = (state, amount) => ({ ...state, value: state.value + amount }) 73 | ``` 74 | 75 | To give a payload to an action we'll want to use an **action descriptor**. 76 | 77 | ```js 78 | h("button", { onclick: [AddBy, 5] }, text("+5")) 79 | ``` 80 | 81 | ### Event Payloads 82 | 83 | Actions used as event handlers receive the event object as the default payload. 84 | 85 | If we were to use our `AddBy` action without specifying its payload: 86 | 87 | ```js 88 | h("button", { onclick: AddBy }, text("+5")) 89 | ``` 90 | 91 | then it will receive the event object when the user clicks it and will attempt to directly "add" that to our state which would obviously be a bug. 92 | 93 | However, if we wanted to make proper use of the event object we have a couple options: 94 | 95 | - Rewrite `AddBy` to account for the possibility of receiving an event payload. 96 | - Or preprocess the event object to make it work with `AddBy` as it is. 97 | 98 | The latter option is preferred because it lets our action remain unconcerned with how its payload is sourced thereby maintaining its reusability. 99 | 100 | Which brings us to... 101 | 102 | --- 103 | 104 | ## Wrapped Actions 105 | 106 | Actions can return other actions. The simplest form of these basically acts like an alias. 107 | 108 | ```js 109 | // Action : () -> OtherAction 110 | const PlusOne = () => Increment 111 | ``` 112 | 113 | A more useful form preprocesses payloads to use with other actions. We can make an event adaptor so our primary action can use event data without coupling to the event source. 114 | 115 | ```js 116 | // Action : (State, EventPayload) -> [OtherAction, Payload] 117 | const AddByValue = (state, event) => [AddBy, +event.target.value] 118 | ``` 119 | 120 | We'll make use of `AddByValue` with an `input` node instead of the `button` from earlier because we want the event that gets preprocessed to have a `value` property we can extract: 121 | 122 | ```js 123 | h("input", { value: state, oninput: AddByValue }) 124 | ``` 125 | 126 | You can keep wrapping actions for as long as your sanity permits. The benefit is the ability to chain together payload adjustments. 127 | 128 | ```js 129 | const AddBy = (state, amount) => ({ ...state, value: state.value + amount }) 130 | const AddByMore = (_, amount) => [AddBy, amount + 5] 131 | const AddByEvenMore = (_, amount) => [AddByMore, amount + 10] 132 | 133 | // ... 134 | 135 | h( 136 | "button", 137 | { onclick: [AddByEvenMore, 1] }, 138 | text("+16") 139 | ) 140 | ``` 141 | 142 | --- 143 | 144 | ## Transforms 145 | 146 | You may consider refactoring very large and/or complicated actions it into simpler, more manageable functions. If so, remember that actions are just messages and, conceptually speaking, are not composable like the functions that implement them. That being said, it can at times be advantageous to delegate some state processing to other functions. Each of these constituent functions is a **transform** and is intended for use by actions or other transforms. 147 | 148 | ```js 149 | const Liokaiser = (state) => ({ 150 | ...state, 151 | combined: true, 152 | leftArm: hellbat(state), 153 | rightArm: guyhawk(state), 154 | upperTorso: leozack(state), 155 | lowerTorso: jallguar(state), 156 | leftLeg: drillhorn(state), 157 | rightLeg: killbison(state), 158 | }) 159 | ``` 160 | 161 | 162 | 163 | --- 164 | 165 | ## Stopping Your App 166 | 167 | You can cease all Hyperapp processes by transitioning to an `undefined` state. This can be useful if you need to do specific cleanup work for your app. 168 | 169 | ```js 170 | // Action : () -> undefined 171 | const Stop = () => undefined 172 | ``` 173 | 174 | Once your app stops, several things happen: 175 | 176 | - All of the app's subscriptions stop. 177 | - The DOM is no longer touched. 178 | - Event handlers stop working. 179 | 180 | A stopped app cannot be restarted. 181 | 182 | If you encounter a scenario where your app doesn't respond when you click stuff within it, then your app might have been stopped by mistake. 183 | 184 | --- 185 | 186 | ## Other Considerations 187 | 188 | ### Transitioning Array State 189 | 190 | An array returned from an action carries [special meaning as already mentioned earlier](effects.md#using-effects). For this reason an actual [array state](state.md#array-state) needs special consideration. 191 | 192 | There are a couple of options available: 193 | 194 | - Wrap the return state within an [effectful state array](state.md#state-with-effects). Mention also that init: option of app() function must also be wrapped. 195 | 196 | ```js 197 | const ArrayAction = (state) => [[...state, "one"]] 198 | ``` 199 | 200 | - Or you can choose a different format for the state by setting it up as an object that contains the the array so actions can work with it like they would with any other object state. 201 | 202 | ```js 203 | const ObjectAction = (state) => ({ ...state, list: [...state.list, "one"] }) 204 | ``` 205 | 206 | ### Nonstandard Usage 207 | 208 | - Using an anonymous function for an action has the disadvantage that it has no name for debugging tools to make use of. That's significant because it's recommended that actions have names. 209 | 210 | - If you wanted to use curried functions to implement actions then you can use named function expressions. 211 | 212 | ```js 213 | const Meet = (name) => 214 | function AndGreet(state) { 215 | return `${state.salutation}, my name is ${name}.` 216 | } 217 | ``` 218 | 219 | - If you have some special requirements you can customize how actions are [dispatched](dispatch.md). 220 | 221 | - Because of the way Hyperapp works internally, anywhere actions can be used literal values can be used instead to directly set state and possibly run effects. 222 | 223 | ```js 224 | h("button", { onclick: 55 }, text("55")) 225 | h("button", { onclick: [55, log] }, text("55 and log")) 226 | ``` 227 | 228 | However, this conflicts with the notion that state transitions happen through the usage of actions. The valid way to achieve the same thing would be: 229 | 230 | ```js 231 | const FiftyFive = () => 55 232 | const FiftyFiveAndLog = () => [55, log] 233 | ``` 234 | 235 | ```js 236 | h("button", { onclick: FiftyFive }, text("55")) 237 | h("button", { onclick: FiftyFiveAndLog }, text("55 and log")) 238 | ``` 239 | 240 | The [`init`](../api/app.md#init) property of [`app()`](../api/app.md) is the only place where it's valid to either directly set the state or use an action to do it. 241 | 242 | That said, this type of usage is fascinating... 243 | 244 | ```js 245 | h("button", { onclick: state.startingOver ? "Begin" : MyCoolAction }, text("cool")) 246 | ``` 247 | -------------------------------------------------------------------------------- /docs/architecture/dispatch.md: -------------------------------------------------------------------------------- 1 | # Dispatch 2 | 3 | **_Definition:_** 4 | 5 | > The **dispatch** function controls Hyperapp's core dispatching process which executes [actions](actions.md), applies state transitions, runs [effects](effects.md), and starts/stops [subscriptions](subscriptions.md) that need it. 6 | 7 | You can augment the dispatcher to tap into the dispatching process for debugging/instrumentation purposes. Such augmentation is loosely comparable to middleware used in other frameworks. 8 | 9 | **_Signature:_** 10 | 11 | ```elm 12 | DispatchFn : (Action, Payload?) -> void 13 | ``` 14 | 15 | --- 16 | 17 | ## Dispatch Initializer 18 | 19 | The dispatch initializer accepts the default dispatch as its sole argument and must give back a dispatch in return. Hyperapp's default dispatch initializer is equivalent to: 20 | 21 | ```js 22 | const boring = (dispatch) => dispatch 23 | ``` 24 | 25 | In your own initializer you'll likely want to return a variant of the regular dispatch. 26 | 27 | --- 28 | 29 | ## Augmented Dispatching 30 | 31 | A dispatch function accepts as its first argument an [action](actions.md) or anything an action can return, and its second argument is the default [payload](actions.md#payloads) if there is one. The payload will be used if the first argument is an action function. 32 | 33 | The action will then be carried out and its resulting state transition will be applied and then any effects it requested to be run will be run. 34 | 35 | ```js 36 | // DispatchFn : (Action, Payload?) -> void 37 | const dispatch = (action, payload) => { 38 | // Do your custom work here. 39 | // ... 40 | 41 | // Hand dispatch over to built-in dispatch. 42 | dispatch(action, payload) 43 | } 44 | ``` 45 | 46 | ## Dispatch recursion 47 | 48 | Dispatch is implemented in a recursive fashion, such that if the action dispatched does not represent the next state (or next state with effects), it will use the dispatched action and payload to resolve the next thing to dispatch. 49 | 50 | A call to `dispatch([ActionFn, payload])` will recurse `dispatch(ActionFn, payload)`, which will recurse to `dispatch(ActionFn(currentState, payload))`. 51 | 52 | --- 53 | 54 | ## Example 1 - Log actions 55 | 56 | Let's say you need to debug the order in which actions are dispatched. An augmented dispatch that logs each action could help with that, rather than having to add `console.log` to every action. 57 | 58 | ```js 59 | const logActionsMiddleware = dispatch => (action, payload) => { 60 | 61 | if (typeof action === 'function') { 62 | console.log('DISPATCH: ', action.name || action) 63 | } 64 | 65 | //pass on to original dispatch 66 | dispatch(action, payload) 67 | } 68 | ``` 69 | 70 | --- 71 | 72 | ## Example 2 - Log state 73 | 74 | To log each state transformation, we first create a general state middleware and then use it to create an augmented dispatch for state logging: 75 | 76 | ```js 77 | const stateMiddleware = fn => dispatch => (action, payload) => { 78 | if (Array.isArray(action) && typeof action[0] !== 'function') { 79 | action = [fn(action[0]), ...action.slice(1)] 80 | } else if (!Array.isArray(action) && typeof action !== 'function') { 81 | action = fn(action) 82 | } 83 | dispatch(action, payload) 84 | } 85 | 86 | const logStateMiddleware = stateMiddleware(state => { 87 | console.log('STATE:', state) 88 | return state 89 | }) 90 | ``` 91 | 92 | --- 93 | 94 | ## Example 3 - Immutable state 95 | 96 | When learning Hyperapp and during developemt it can sometimes be useful to guarantee states are not mutated by mistake, let's use `stateMiddleware` above to create an augmented dispatch for state immutability: 97 | 98 | ```js 99 | // a proxy prohibiting mutation 100 | const immutableProxy = o => { 101 | if (o===null || typeof o !== 'object') return o 102 | return new Proxy(o, { 103 | get(obj, prop) { 104 | return immutableProxy(obj[prop]) 105 | }, 106 | set(obj, prop) { 107 | throw new Error(`Can not set prop ${prop} on immutable object`) 108 | } 109 | }) 110 | } 111 | 112 | export const immutableMiddleware = stateMiddleware(state => immutableProxy(state)) 113 | ``` 114 | 115 | 116 | ## Usage 117 | 118 | The [`app()`](../api/app.md) function will check to see if you have a dispatch initializer assigned to the [`dispatch:`](../api/app.md#dispatch) property while instantiating your application. If so, your app will use it instead of the default one. 119 | 120 | The only time the dispatch initializer gets used is once during the instantiation of your app. 121 | 122 | Only one dispatch initializer can be defined per app. Consequently, only one dispatch can be defined per app. 123 | 124 | Extending the example from above, the dispatch initializer would be used like this: 125 | 126 | ```js 127 | import { mwLogState } from "./middleware.js" 128 | 129 | app({ 130 | // ... 131 | dispatch: logActionsMiddleware 132 | }) 133 | ``` 134 | 135 | And if you wanted to use all custom dispatches together, you can chain them like this: 136 | 137 | ```js 138 | import { logActionsMiddleware, logStateMiddleware, immutableMiddleware } from "./middleware.js" 139 | 140 | app({ 141 | // ... 142 | dispatch: dispatch => logStateMiddleware(logActionsMiddleware(immutableMiddleware(dispatch))) 143 | }) 144 | ``` 145 | 146 | 147 | --- 148 | 149 | ## Other Considerations 150 | 151 | - [`app()`](../api/app.md) returns the dispatch function to allow [dispatching externally](../api/app.md#instrumentation). 152 | 153 | - If you're feeling truly adventurous and/or know what you're doing you can choose to have your dispatch initializer return a completely custom dispatch from the ground up. For what purpose? You tell me! However, a completely custom dispatch won't have access to some important internal framework functions, so it's unlikely to be something useful without building off of the original dispatch. 154 | -------------------------------------------------------------------------------- /docs/architecture/effects.md: -------------------------------------------------------------------------------- 1 | # Effects 2 | 3 | _**Definition:**_ 4 | 5 | > An **effect** is a representation used by actions to interact with some external process. 6 | 7 | As with [subscriptions](subscriptions.md), effects are used to deal with impure asynchronous interactions with the outside world in a safe, pure, and immutable way. Creating an HTTP request, giving focus to a DOM element, saving data to local storage, sending data over a WebSocket, and so on, are all examples of effects at a conceptual level. 8 | 9 | **_Signature:_** 10 | 11 | ```elm 12 | Effect : EffecterFn | [EffecterFn, Payload] 13 | ``` 14 | 15 | **_Naming Recommendation:_** 16 | 17 | Effects are recommended to be named in `camelCase` using a verb (for instance `log`) or verb-noun phrase (like `saveAsPDF`) in its imperative form for the name. 18 | 19 | ## Using Effects 20 | 21 | An action can associate its state transition with a list of one or more [effects](#effects) to run alongside the transition. It does this by returning an array containing the [state with effects](state.md#state-with-effects) where the first entry is the next state while the remaining entries are the effects to run. 22 | 23 | ```js 24 | import { log } from "./fx" 25 | 26 | // Action : (State) -> [NextState, ...Effects] 27 | const SayHi = (state) => [ 28 | { ...state, value: state.value + 1 }, 29 | log("hi"), 30 | log("there"), 31 | ] 32 | 33 | // ... 34 | 35 | h("button", { onclick: SayHi }, text("Say Hi")) 36 | ``` 37 | 38 | Actions can of course receive payloads and use effects simultaneously. 39 | 40 | ```js 41 | // Action : (State, Payload) -> [NextState, ...Effects] 42 | const SayBye = (state, amount) => [ 43 | { ...state, value: state.value + amount }, 44 | log("bye"), 45 | ] 46 | 47 | // ... 48 | 49 | h("button", { onclick: [SayBye, 1] }, text("Bye")) 50 | ``` 51 | 52 | ## Excluding Effects 53 | 54 | If you don't include any effects in the return array then only the state transition happens. 55 | 56 | Here, `OnlyIncrement` both behaves and is used similarly to `Increment` [shown here](actions.md#actual-state-transition): 57 | 58 | ```js 59 | // Action : (State) -> [NextState] 60 | const OnlyIncrement = (state) => [{ ...state, value: state.value + 1 }] 61 | 62 | // ... 63 | 64 | h("button", { onclick: OnlyIncrement }, text("+")) 65 | ``` 66 | 67 | Such a single-element array may seem redundant at first but it can come into play if you have an action that conditionally runs effects. 68 | 69 | For example, compare this: 70 | 71 | ```js 72 | const DoIt = (state) => { 73 | let transition = { ...state, value: "MacGuffin" } 74 | if (state.eating) { 75 | transition = [transition, log("eating")] 76 | } 77 | if (state.drinking) { 78 | transition = Array.isArray(transition) 79 | ? [...transition, log("drinking")] 80 | : [transition, log("drinking")] 81 | } 82 | return transition 83 | } 84 | ``` 85 | 86 | 87 | 88 | with this: 89 | 90 | ```js 91 | const DoItBetter = (state) => { 92 | let transition = [{ ...state, value: "MacGuffin" }] 93 | if (state.eating) { 94 | transition = [...transition, log("eating")] 95 | } 96 | if (state.drinking) { 97 | transition = [...transition, log("drinking")] 98 | } 99 | return transition 100 | } 101 | ``` 102 | 103 | Admittedly, these examples are a bit contrived but the latter is less complex. 104 | 105 | However, for these examples in particular we can do even better by taking advantage of the fact that any "effects" that are actually falsy values are ignored. 106 | 107 | ```js 108 | const DoItBest = (state) => [ 109 | { ...state, value: "MacGuffin" }, 110 | state.eating && log("eating"), 111 | state.drinking && log("drinking"), 112 | ] 113 | ``` 114 | 115 | ## Defining Effects 116 | 117 | Syntactically speaking, an effect takes the form of a tuple containing its [effecter](#effecters) and any associated data. 118 | 119 | Technically, an effect can be used directly but using a function that creates the effect is recommended because it offers flexibility with how the tuple is created while looking a little cleaner overall. 120 | 121 | ```js 122 | const massFx = (data) => [runNormandy, data] 123 | ``` 124 | 125 | 126 | 127 | ## Effecters 128 | 129 | **_Definition:_** 130 | 131 | > An **effecter** is the function that actually carries out an effect. 132 | 133 | **_Signature:_** 134 | 135 | ```elm 136 | EffecterFn : (DispatchFn, Payload?) -> void 137 | ``` 138 | 139 | As with [subscribers](subscriptions.md#subscribers), effecters are allowed to use side-effects and can also manually [`dispatch`](dispatch.md) actions in order to inform your app of any pertinent results from their execution. 140 | 141 | It's important to know that effecters are more than just a way to wrap any arbitrary impure code. Their purpose is to be a generalized bridge between your app's business logic and the impure code that needs to exist. By keeping the effecters as generic as we can, we form a clean, manageable separation between what is requested to be done from how that request is done. 142 | 143 | To demonstrate this approach take this ill-formed effecter for example: 144 | 145 | ```js 146 | // This effecter is ill-formed. 147 | const runHarvest = (dispatch, _payload) => { 148 | const tiberium = document.getElementById("tiberium") 149 | dispatch((state) => ({ ...state, tiberium })) 150 | } 151 | ``` 152 | 153 | 154 | 155 | Sure it runs, but it's also coupled to our app's state and the element ID being referenced is also hard-coded. 156 | 157 | Let's address this by first decoupling our callback action from the effecter by leveraging our ability to give the effecter a payload: 158 | 159 | ```js 160 | const runHarvest = (dispatch, payload) => 161 | dispatch(payload.action, document.getElementById("tiberium")) 162 | ``` 163 | 164 | Let's further utilize our payload by using it to pass in data our effecter needs to work: 165 | 166 | ```js 167 | const runHarvest = (dispatch, payload) => 168 | dispatch(payload.action, document.getElementById(payload.id)) 169 | ``` 170 | 171 | Finally, we should rename the effecter to reflect its generic nature: 172 | 173 | ```js 174 | const runGetElement = (dispatch, payload) => 175 | dispatch(payload.action, document.getElementById(payload.id)) 176 | ``` 177 | 178 | A well-formed effecter is as generic as it can be. 179 | 180 | ### Synchronization 181 | 182 | Effecters which run some asynchronous operation and wish to report the results of it back to your app will need to ensure that the timing of their communication dispatch happens in alignment with Hyperapp's repaint cycle. This is important to ensure the state is set correctly. 183 | 184 | Hyperapp's repaint cycle stays synchronized with the browser's natural repaint cycle, so asynchronous effecters must do the same. The preferred way to do this is with [`requestAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame). If for some reason that method is unavailable, the fallback is [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout). 185 | 186 | Let's see an example of an ill-formed asynchronous effecter: 187 | 188 | ```js 189 | // This effecter is ill-formed. 190 | const runBrotherhood = async (dispatch, payload) => { 191 | const response = await fetch(payload.lookForKaneHere) 192 | const kaneLives = response.json() 193 | requestAnimationFrame(() => { 194 | dispatch((state) => ({ 195 | ...state, 196 | message: kaneLives ? "One vision! One purpose!" : "", 197 | })) 198 | }) 199 | } 200 | ``` 201 | 202 | 203 | 204 | Now let's see a more well-formed asynchronous effecter: 205 | 206 | ```js 207 | const runSimpleFetch = async (dispatch, payload) => { 208 | const response = await fetch(payload.url) 209 | requestAnimationFrame(() => dispatch(payload.action, response.json())) 210 | } 211 | ``` 212 | 213 | ### Custom Events 214 | 215 | The ideal scenario to use custom effects is when your Hyperapp application needs to communicate with a legacy app via custom events. 216 | 217 | We can have our Hyperapp application use a custom effect for triggering custom events. 218 | 219 | ```js 220 | // ./fx.js 221 | 222 | const runEmit = (_dispatch, payload) => 223 | dispatchEvent(new CustomEvent(payload.type, { detail: payload.detail })) 224 | 225 | export const emit = (type, detail) => [runEmit, { type, detail }] 226 | ``` 227 | 228 | ```js 229 | import { h, text, app } from "hyperapp" 230 | import { emit } from "./fx" 231 | 232 | app({ 233 | view: () => 234 | h("main", {}, [ 235 | h( 236 | "button", 237 | { 238 | onclick: (state) => [ 239 | state, 240 | emit("outgoing", { message: "hello" }) 241 | ], 242 | }, 243 | text("Send greetings") 244 | ), 245 | ]), 246 | node: document.querySelector("main"), 247 | }) 248 | ``` 249 | -------------------------------------------------------------------------------- /docs/architecture/flowchart.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | graph TD 3 | init("     init     ") 4 | init --- |"init:{state}
init:[state, effect(s)]
"|j0[ ] --- j1[ ] -->nextState 5 | init --> |"init:Action
init:[Action, payload?]"|Action 6 | 7 | domevent("DOM/synthetic 
events
 (click/myevent) ") --> viewEvent 8 | viewEvent(("    view    
event")) --> Action 9 | externalEvents("global/external
processes
 (window resize) ") --> subscription 10 | subscription(("subscription")) -->Action 11 | 12 | Action[" Action 
(state change)"] -->|"OtherAction
 [OtherAction, payload?]
"|Action 13 | Action --- |"NextState
[NextState, ...Effects]"|j2[ ] ---> nextState 14 | 15 | nextState(("  next  
state")) --- j3[ ] 16 | j3 --> |"view(state)"|newDom("   (re)render  
    DOM   ") 17 | j3 --> |"subscriptions(state)"|recalcSubs(recalc
subscriptions) 18 | j3 --> |"(dispatch, Payload?) -> void"|effect 19 | effect("Effects
(impure code)") -.-> |"dispatch"|dispatchAction("Action") 20 | 21 | style j0 height:1px; 22 | style j0 width:1px; 23 | style j1 height:1px; 24 | style j1 width:1px; 25 | style j2 height:1px; 26 | style j2 width:1px; 27 | style j3 height:1px; 28 | style j3 width:1px; 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/architecture/state.md: -------------------------------------------------------------------------------- 1 | # State 2 | 3 | **_Definition:_** 4 | 5 | > The **state** of your Hyperapp application is the unified set of data that your [views](views.md), [actions](actions.md), and [subscriptions](subscriptions.md) all have access to. 6 | 7 | Hyperapp by design offers no strong opinion about how your state should be structured aside from it being unified. This means [components](views.md#components) don't technically possess their own local state but that also means they have direct access to any part of the entire state they need. The user is entrusted with shaping things beyond that. 8 | 9 | --- 10 | 11 | ## Assignment 12 | 13 | When you initially create your app instance with [`app()`](../api/app.md) you get to setup your state with the [`init:`](../api/app.md#init) property. Since it's possible to have several different app instances [active simultaneously](../api/app.md#multiple-apps) it's important to know that each app retains its own separate state. 14 | 15 | ### State Transitions 16 | 17 | Aside from the aforementioned [`init:`](../api/app.md#init) property the only way to affect state is by changing it through the use of [actions](actions.md). 18 | 19 | ## State With Effects 20 | 21 | If you use an array to set the state Hyperapp will interpret this as a special array where the first entry is the state and if there are more entries they will be [effects](effects.md) that need to be run. 22 | 23 | So, Hyperapp will apply the state first and then will run the effects in the order they appear. 24 | 25 | ```js 26 | [state, log(state), log("MOAR")] 27 | ``` 28 | 29 | ### Array State 30 | 31 | If you actually do want to use an array as your state you'll have to wrap it within an effectful state array to make it work. 32 | 33 | ```js 34 | [["a", "b", "c"]] 35 | ``` 36 | 37 | The actions page also [talks about it](actions.md#transitioning-array-state). 38 | 39 | --- 40 | 41 | ## Visualization 42 | 43 | The primary [view](views.md) of your app, as set by the [`view:`](../api/app.md#view) property of [`app()`](../api/app.md), will receive the current state for it to use to determine what gets rendered. Any changes to the state are automatically reflected there as well. 44 | 45 | --- 46 | 47 | ## Other Considerations 48 | 49 | ### Serializability 50 | 51 | While you can put anything you want in the state we recommend avoiding things that are unserializable such as symbols, functions, and recursive references. This helps to ensure compatibility with things like saving to persistent local storage, or using other tools especially ones that are potentially Hyperapp-specific. 52 | 53 | ### State Type 54 | 55 | You can of course choose to make your state some basic type such as a string or number. However, it's recommended to use an object because of the expressivity it gives you in defining your state shape. 56 | 57 | ### Direct Mutation 58 | 59 | Since we are ultimately using JavaScript you can technically edit parts of the state mutably. However, state changes should be thought of in terms of snapshots: New versions of the state get created to reflect the changes made at a certain moment in time. This perspective naturally calls for immutability. 60 | 61 | Fresh new state should be returned from an [action](actions.md). If an action returns nothing your app will [stop](actions.md#stopping-your-app). If you mutate the current state and return it you are returning the same object reference as the state was earlier. Hyperapp cannot tell from this that any changes have occurred. Hence the action will do nothing. 62 | -------------------------------------------------------------------------------- /docs/architecture/subscriptions.md: -------------------------------------------------------------------------------- 1 | # Subscriptions 2 | 3 | **_Definition:_** 4 | 5 | > A **subscription** function represents a dependency your app has on some external process. 6 | 7 | As with [effects](effects.md), subscriptions deal with impure, asynchronous interactions with the outside world in a safe, pure, and immutable way. They are a streamlined way of responding to events happening outside our application such as time or location changes. They handle resource management for us that we would otherwise need to worry about like adding and removing event listeners, closing connections, etc. 8 | 9 | **_Signature:_** 10 | 11 | ```elm 12 | Subscription : [SubscriberFn, Payload?] 13 | ``` 14 | 15 | **_Naming Recommendation:_** 16 | 17 | Subscriptions are recommended to be named in `camelCase` prefixed by `on`, for instance `onEvery` or `onMouseEnter` in order to reflect their event handling character. 18 | 19 | --- 20 | 21 | ## Using Subscriptions 22 | 23 | Subscriptions are setup and managed through the [`subscriptions:`](../api/app.md#subscriptions) property used with [`app()`](../api/app.md) when instantiating your app. 24 | 25 | ```js 26 | import { onEvery } from "./time" 27 | 28 | // ... 29 | 30 | app({ 31 | init: { delayInMilliseconds: 1000 }, 32 | subscriptions: (state) => [ 33 | // Dispatch `RequestResource` every `delayInMilliseconds`. 34 | onEvery(state.delayInMilliseconds, RequestResource), 35 | ], 36 | }) 37 | ``` 38 | 39 | You can control if subscriptions are active or not by using boolean values. 40 | 41 | ```js 42 | app({ 43 | subscriptions: (state) => [ 44 | state.toBe && onEvery(state.delay, ThatIsTheQuestion), 45 | state.notToBe || onEvery(state.delay, ThatIsTheQuestion), 46 | ], 47 | }) 48 | ``` 49 | 50 | 51 | 52 | ### Subscriptions Array Format 53 | 54 | Hyperapp expects the subscriptions array to be of a fixed size with each entry being either a boolean value or a particular subscription function that stays in the same array position. Using dynamic arrays won't work. Inlining subscription functions also won't work because they would just reset on every state change. 55 | 56 | ### Subscription Lifecycle 57 | 58 | On every state change, Hyperapp will check each subscriptions array entry to see if they're active and compare that with how they were in the previous state. This comparison determines how subscriptions are handled. 59 | 60 | | Previously Active | Currently Active | What Happens | 61 | | ----------------- | ---------------- | -------------------------------------------- | 62 | | no | no | Nothing. | 63 | | no | yes :100: | Subscription starts up. | 64 | | yes :100: | no | Subscription shuts down and gets cleaned up. | 65 | | yes :100: | yes :100: | Subscription remains active. | 66 | 67 | To restart a subscription you must first deactivate it and then, during the next state change, reactivate it. 68 | 69 | --- 70 | 71 | ## Custom Subscriptions 72 | 73 | There may be times when an official Hyperapp subscription package is unavailable for our needs. For those scenarios we'll need to make our own custom subscriptions. 74 | 75 | ### Subscribers 76 | 77 | **_Definition:_** 78 | 79 | > A **subscriber** is a function which implements an active subscription. 80 | 81 | **_Signature:_** 82 | 83 | ```elm 84 | SubscriberFn : (DispatchFn, Payload?) -> CleanupFn 85 | ``` 86 | 87 | As with [effecters](effects.md#effecters), subscribers are allowed to use side-effects and can also manually [`dispatch`](dispatch.md) actions in order to inform your app of any pertinent results from their execution. 88 | 89 | Subscribers can be given a data `payload` for their use. 90 | 91 | Well-formed subscribers, as it is with effecters, should be as generic as possible. However, unlike with effecters, they should return a function that handles cleaning up the subscription if it gets cancelled. 92 | 93 | ### Example 94 | 95 | Let's say we're embedding our Hyperapp application within a legacy vanilla JavaScript project. 96 | 97 | Somewhere within the legacy portion of our project a custom event gets emitted: 98 | 99 | ```js 100 | // Somewhere in our legacy app... 101 | 102 | const triggerSpecialEvent = () => { 103 | dispatchEvent(new CustomEvent("secret", { detail: 42 })) 104 | } 105 | 106 | // ... 107 | 108 | triggerSpecialEvent() 109 | ``` 110 | 111 | 112 | 113 | Our embedded Hyperapp application will need a custom subscription to be able to deal with custom events: 114 | 115 | ```js 116 | // ./subs.js 117 | 118 | const listenToEvent = (dispatch, props) => { 119 | const listener = (event) => 120 | requestAnimationFrame(() => dispatch(props.action, event.detail)) 121 | 122 | addEventListener(props.type, listener) 123 | return () => removeEventListener(props.type, listener) 124 | } 125 | 126 | export const listen = (type, action) => [listenToEvent, { type, action }] 127 | ``` 128 | 129 | In case you're wondering why `listenToEvent()`'s listener is using `requestAnimationFrame`, it has to do with [synchronization](actions.md#synchronization). 130 | 131 | Now we can use our custom subscription in our Hyperapp application. Since it will be embedded we'll wrap our call to [`app()`](../api/app.md) within an exported function our legacy app can make use of: 132 | 133 | ```js 134 | import { h, text, app } from "hyperapp" 135 | import { listen } from "./subs" 136 | 137 | const Response = (state, payload) => ({ ...state, payload }) 138 | 139 | export const myApp = (node) => 140 | app({ 141 | init: () => ({ payload: null }), 142 | view: ({ payload }) => 143 | h("main", {}, [ 144 | payload && h("p", {}, text(`Payload received: ${JSON.stringify(payload)}`)), 145 | ]), 146 | subscriptions: () => [listen("secret", Response)], 147 | node: document.querySelector("main"), 148 | }) 149 | ``` 150 | 151 | --- 152 | 153 | ## Other Considerations 154 | 155 | ### Destructuring Gotcha 156 | 157 | Since a well-formed subscriber returns a cleanup function, it's possible that the cleanup function would want to communicate back to your app that the cleanup took place. 158 | 159 | ```js 160 | const listenToEvent = (dispatch, props) => { 161 | const listener = (event) => 162 | requestAnimationFrame(() => dispatch(props.action, event.detail)) 163 | 164 | addEventListener(props.type, listener) 165 | return () => { 166 | removeEventListener(props.type, listener) 167 | dispatch(props.action, "") 168 | } 169 | } 170 | ``` 171 | 172 | So, using `props` directly works well. However, if instead you tried to use destructuring then the cleanup function won't be able to communicate back to your app in all scenarios: 173 | 174 | ```js 175 | const listenToEvent = (dispatch, { action, type }) => { 176 | const listener = (event) => 177 | requestAnimationFrame(() => dispatch(action, event.detail)) 178 | 179 | addEventListener(type, listener) 180 | return () => { 181 | removeEventListener(type, listener) 182 | dispatch(action, "cleaned-up") // <-- uh, oh! 183 | } 184 | } 185 | ``` 186 | 187 | The reason is because destructuring the `props` parameter will create local copies of the props listed. This means the cleanup function's closure will be referring to the `action` function that existed at the moment the cleanup function was created and returned, not the moment the cleanup function gets invoked. This is a subtle yet significant difference depending on how you use your actions with this type of subscriber. 188 | 189 | The scenario in which this comes into play is if you use an anonymous function for the `action`. An example of where you may consider doing this is if you wanted a way to selectively prevent default event behavior when a subscriber responds to an event. 190 | 191 | ```js 192 | // ./fx.js 193 | 194 | const runPreventDefault = (dispatch, payload) => { 195 | payload.event.preventDefault() 196 | dispatch(payload.action) 197 | } 198 | 199 | export const preventDefault = (action, event) => 200 | [runPreventDefault, { action, event }] 201 | ``` 202 | 203 | ```js 204 | // ./actions.js 205 | 206 | import { preventDefault } from "./fx" 207 | 208 | export const skipDefault = (action) => (state, event) => 209 | [state, preventDefault(action, event)] 210 | 211 | export const MyAction = (state) => ({ ...state }) 212 | ``` 213 | 214 | ```js 215 | // ./subs.js 216 | 217 | const subOnThatThing = (dispatch, props) => { 218 | // Do stuff... 219 | } 220 | 221 | export const onThatThing = (action, props) => [subOnThatThing, { ...props, action }] 222 | ``` 223 | 224 | ```js 225 | // ./main.js 226 | 227 | import { onThatThing } from "./subs" 228 | import { skipDefault } from "./actions" 229 | 230 | app({ 231 | subscriptions: (state) => [ 232 | state.isActive && 233 | onThatThing(skipDefault(MyAction), { 234 | foo: 42 + state.index, 235 | }), 236 | ], 237 | }) 238 | ``` 239 | 240 | Now when the subscription function runs per state update, the wrapped action is generated anew which results in a new function reference for the subscription's `action`. So, `subOnThatThing` must use `props` instead of destructuring to ensure the right function reference is available. 241 | -------------------------------------------------------------------------------- /docs/architecture/views.md: -------------------------------------------------------------------------------- 1 | # Views 2 | 3 | **_Definition:_** 4 | 5 | > A **view** is a declarative description of what should get rendered and is usually influenced by the current [state](state.md). 6 | 7 | A view is implemented as a pure function that accepts the current state and returns a [virtual DOM node (VNode)](#virtual-dom). When [state transitions](state.md#state-transitions) happen your views are automatically updated accordingly. 8 | 9 | **_Signature:_** 10 | 11 | ```elm 12 | View : (State) -> VNode 13 | ``` 14 | 15 | --- 16 | 17 | ## Describing Views 18 | 19 | The [`h()`](../api/h.md), [`text()`](../api/text.md), and [`memo()`](../api/memo.md) functions are the building blocks of your views. 20 | 21 | [`h()`](../api/h.md) not only describes what HTML elements are being used but also what [actions](actions.md) are wired up if any. 22 | 23 | ```js 24 | const view = (state) => 25 | h( 26 | "button", 27 | { 28 | class: { "calling-acid-burn": state.beingFramed }, 29 | onclick: FindThatDisk, 30 | }, 31 | text("It's in that place where I put that thing that time.") 32 | ) 33 | ``` 34 | 35 | 36 | 37 | [`text()`](../api/text.md) just creates text nodes so the views it can create on its own are necessarily simplistic. 38 | 39 | ```js 40 | const view = () => text("Go home and be a family man!") 41 | ``` 42 | 43 | 44 | 45 | [`memo()`](../api/memo.md) is designed to be used with other functions that produce VNodes, so it doesn't really describe a view by itself. 46 | 47 | ```js 48 | const view = (state) => memo(scenicView, state.vacationSpot) 49 | ``` 50 | 51 | 52 | 53 | --- 54 | 55 | ## Components 56 | 57 | Views are naturally composable so they can be as simple or complicated as you need. Simpler apps probably just need a single view but in more complicated apps there could be plenty of subviews. 58 | 59 | **_Definition:_** 60 | 61 | > A **component** in Hyperapp can either be a subview or some other function that generates VNodes. 62 | 63 | **_Signature:_** 64 | 65 | ```elm 66 | Component : (GlobalState | PartialState) -> VNode | [...VNodes] 67 | ``` 68 | 69 | 70 | 71 | You would typically make components for widgets that provide the building block elements of your app's UI. Components for larger UI segments such as dashboards or pages would make use of these widgets. 72 | 73 | In the following example, `coinsDisplay` is a component in the form of a subview while `questionBlock` is a component in the form of some function that returns a VNode. Notice the former cares directly about the state while the latter doesn't: 74 | 75 | ```js 76 | // Component : (GlobalState) -> VNode 77 | const coinsDisplay = (state) => 78 | h("div", { class: "coins-display" }, text(state.coins)) 79 | 80 | // Component : (PartialState) -> VNode 81 | const questionBlock = (opened) => 82 | opened 83 | ? h("button", { class: "question-block opened" }, text("?")) 84 | : h( 85 | "button", 86 | { 87 | class: "question-block", 88 | onclick: [ 89 | HitBlockFromBottom, 90 | { revealItem: "beanstalk" }, 91 | ], 92 | }, 93 | text("?") 94 | ) 95 | 96 | // Component : (GlobalState) -> VNode 97 | const level = (state) => 98 | h("div", { class: "level" }, [ 99 | coinsDisplay(state), 100 | questionBlock(state.onlyQuestionBlockOpened), 101 | ]) 102 | ``` 103 | 104 | 105 | 106 | **_Naming Recommendation:_** 107 | 108 | Components are recommended to be named in `camelCase` using a noun that concisely describes the (purpose of the) composed group of contained elements best, for instance `articleHeader` or `questionBlock`. 109 | 110 | ### Components Returning Multiple VNodes 111 | 112 | Components are allowed to return an array of VNodes. However, to make use of such components in a list of other siblings, you'll need to spread their result. 113 | 114 | ```js 115 | // Component : () -> [...VNodes] 116 | const finishingMoveOptions = () => [ 117 | h("button", { onclick: FinishHim }, text("Fatality")), 118 | h("button", { onclick: FinishHimAsAnAnimal }, text("Animality")), 119 | h("button", { onclick: TurnHimIntoABaby }, text("Babality")), 120 | h("button", { onclick: BefriendHim }, text("Friendship")), 121 | ] 122 | 123 | const view = () => h("div", {}, [ 124 | h("em", {}, text("Finish them:")), 125 | ...finishingMoveOptions(), 126 | ]) 127 | ``` 128 | 129 | 130 | 131 | --- 132 | 133 | ## Using Views 134 | 135 | ### Top-Level View 136 | 137 | Every Hyperapp application has a base view that encompasses all others. This is the **top-level view** that's defined by the [`view:`](../api/app.md#view) property when using [`app()`](../api/app.md). Hyperapp automatically calls this view and gives it the current state when the state is initially set or anytime it's updated. 138 | 139 | ```js 140 | app({ 141 | // ... 142 | view: (state) => 143 | h("main", {}, [ 144 | earthrealm(state), 145 | edenia(state), 146 | ]), 147 | }) 148 | ``` 149 | 150 | 151 | 152 | ### Conditional Rendering 153 | 154 | Elements of a view can be shown or hidden conditionally. 155 | 156 | ```js 157 | const view = (state) => 158 | h("div", {}, [ 159 | state.flying && h("div", {}, text("Flying")), 160 | state.notSwimming || h("div", {}, text("Swimming")), 161 | ]) 162 | ``` 163 | 164 | ### Recycling 165 | 166 | Hyperapp supports hydration of views out of the box. This means that if the mount node you specify is already populated with DOM elements, Hyperapp will recycle and use these existing elements instead of throwing them away and creating them again. You can use this for doing SSR or pre-rendering of your applications, which will give you SEO and performance benefits. 167 | 168 | --- 169 | 170 | ## Virtual DOM 171 | 172 | **_Definition:_** 173 | 174 | > The **virtual DOM**, or **VDOM** for short, is an in-memory representation of the [DOM](https://dom.spec.whatwg.org/) elements that exist on the current page. 175 | 176 | Hyperapp uses it to determine how to efficiently update the actual DOM. The virtual DOM is a tree data structure where each of its nodes represent a particular VDOM element that may or may not get rendered. 177 | 178 | We've already seen how [`h()`](../api/h.md), [`text()`](../api/text.md), and [`memo()`](../api/memo.md) all return different types of VNodes. 179 | 180 | ### Patching the DOM 181 | 182 | When Hyperapp is ready to update the DOM it will do so starting at the element that corresponds to the root VNode of the [top-level view](#top-level-view). Hyperapp checks if there were changes made to that VNode representing that element. If so, the element gets rerendered the process repeats recursively for every child of that VNode. 183 | 184 | ### Keys 185 | 186 | Sometimes Hyperapp needs help determining how certain elements have changed. This is generally the case for VNodes that are rendered based on arrays in the state. This is because array items may have shifted around a lot during a state change, so when they get rendered the VNodes that currently represent them might be completely different than before. 187 | 188 | Since Hyperapp can't know for sure it must assume everything had changed requiring a full render every time. 189 | 190 | For an example, look at the [`key:`](../api/h.md#key) documentation for [`h()`](../api/h.md). 191 | 192 | ### Memoization 193 | 194 | The optimization technique known as **memoization**, is where the result of a calculation is stored somewhere to be used again in the future without incurring the cost of calculating again. 195 | 196 | Memoization in Hyperapp concerns how VNodes are rendered and is implemented using [`memo()`](../api/memo.md). When memoized views are rerendered the "state" they receive is actually the props defined for the view when the memoization was setup. 197 | 198 | Immutability in Hyperapp guarantees that if two things are referentially equal, they must be identical. This makes it safe for Hyperapp to only re-compute your memoized components when values passed through their props change. 199 | 200 | For an example, look at the documentation for [`memo()`](../api/memo.md#example). 201 | 202 | #### Performance 203 | 204 | Memoization exists to help improve rendering performance but it's not a panacea. If it was used with nodes that need to update on every state change, the cost of checking if the memoization's props had changed before carrying out the rendering would be a net loss of performance over time. 205 | 206 | Memoization was designed for nodes that don't need to update at all or just occasionally. 207 | 208 | As always when it comes to optimizations, be sure to measure the performance of your app to make sure you're getting true benefits and adjust if necessary. 209 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | ## API 4 | 5 | - [`h()`](api/h.md) creates a virtual DOM node (VNode) that gets rendered. 6 | - [`text()`](api/text.md) turns a string into a VNode. 7 | - [`app()`](api/app.md) initializes a Hyperapp app and mounts it. 8 | - [`memo()`](api/memo.md) creates a special VNode that is lazily rendered. 9 | 10 | ## Architecture 11 | 12 | - [State](architecture/state.md) represents your application's data. 13 | - [Views](architecture/views.md) represent your state visually. 14 | - [Actions](architecture/actions.md) cause state transitions and trigger effects. 15 | - [Effects](architecture/effects.md) are triggered by actions to interact with external processes. 16 | - [Subscriptions](architecture/subscriptions.md) dispatch actions in response to external events. 17 | - [Dispatch](architecture/dispatch.md) controls action dispatching. 18 | 19 |     [Flowchart](architecture/flowchart.md) showing the general flow of a hyperapp app. 20 | 21 |

Glossary

22 | 23 | - [Action](architecture/actions.md): An app behavior that transitions state and invokes effects. 24 | - [Action Descriptor](architecture/actions.md#payloads): A tuple representing an action with its payload. 25 | - [Component](architecture/views.md#components): A view with a specific purpose. 26 | - [Dispatch Function](architecture/dispatch.md#dispatch): The process that executes actions, applies state, and calls effects. 27 | - [Dispatch Initializer](architecture/dispatch.md#dispatch-initializer): A function that controls dispatch. 28 | - [Effect](architecture/effects.md): A generalized encapsulation of an external process. 29 | - [Effecter](architecture/effects.md#effecters): A function that carries out an effect. 30 | - [Event Payload](architecture/actions.md#event-payloads): A payload specific to an event. 31 | - [Memoization](architecture/views.md#memoization): In Hyperapp, the delayed rendering of VNodes. 32 | - [Mount Node](api/app.md#node): The DOM element that holds the app. 33 | - [Payload](architecture/actions.md#payloads): Data given to an action. 34 | - [State](architecture/state.md): The unified set of data your Hyperapp application uses and maintains. 35 | - [State Transition](architecture/state.md#state-transitions): An evolutionary step for the state. 36 | - [Subscriber](architecture/subscriptions.md#subscribers): A function that carries out a subscription. 37 | - [Subscription](architecture/subscriptions.md): A binding between the app and external events. 38 | - [Top-Level View](architecture/views.md#top-level-view): The main view which is given the state. 39 | - [VDOM](architecture/views.md#virtual-dom): The virtual DOM, an in-memory representation for the DOM of the current page. 40 | - [View](architecture/views.md): A function describing the desired DOM, represented by a VNode, as a function of the current state. 41 | - [Wrapped Action](architecture/actions.md#wrapped-actions): An action that is returned by another action. 42 | -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial # 2 | 3 | If you're new to Hyperapp, this is a great place to start. We'll cover all the essentials and then some, as we incrementally build up a simplistic example. To begin, open up an editor and type in this html: 4 | 5 | ```html 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
19 | 20 | 21 | ``` 22 | 23 | Save it as `hyperapp-tutorial.html` on your local drive, and in the same folder create the `hyperapp-tutorial.css` with the following css: 24 | 25 |
26 | (expand tutorial css) 27 | 28 | ```css 29 | @import url('https://cdn.jsdelivr.net/npm/water.css@2/out/light.css'); 30 | 31 | :root { 32 | --box-width: 200px; 33 | } 34 | 35 | main { position: relative;} 36 | 37 | .person { 38 | box-sizing: border-box; 39 | width: var(--box-width); 40 | padding: 10px 10px 10px 40px; 41 | position: relative; 42 | border: 1px #ddd solid; 43 | border-radius: 5px; 44 | margin-bottom: 10px; 45 | cursor: pointer; 46 | } 47 | 48 | .person.highlight { 49 | background-color: #fd9; 50 | } 51 | .person.selected { 52 | border-width: 3px; 53 | border-color: #55c; 54 | padding-top: 8px; 55 | padding-bottom: 8px; 56 | } 57 | 58 | .person input[type=checkbox] { 59 | position: absolute; 60 | cursor: default; 61 | top: 10px; 62 | left: 7px; 63 | } 64 | .person.selected input[type=checkbox] { 65 | left: 5px; 66 | top: 8px; 67 | } 68 | 69 | .person p { 70 | margin: 0; 71 | margin-left: 2px; 72 | } 73 | .person.selected p { 74 | margin-left: 0; 75 | } 76 | 77 | .bio { 78 | position: absolute; 79 | left: calc(var(--box-width) + 2rem); 80 | top: 60px; 81 | color: #55c; 82 | font-style: italic; 83 | font-size: 2rem; 84 | text-indent: -1rem; 85 | } 86 | .bio:before {content: '"';} 87 | .bio:after {content: '"';} 88 | 89 | ``` 90 | 91 |
92 | 93 | Keep the html file open in a browser as you follow along the tutorial, to watch your progress. At each step there will be a link to a live-demo sandbox yo may refer to in case something isn't working right. (You could also use the sandbox to follow the tutorial if you prefer) 94 | 95 | ## Hello world ## 96 | 97 | Enter the following code: 98 | 99 | ```js 100 | import {h, text, app} from "https://cdn.skypack.dev/hyperapp" 101 | 102 | app({ 103 | view: () => h("main", {}, [ 104 | h("div", {class: "person"}, [ 105 | h("p", {}, text("Hello world")), 106 | ]), 107 | ]), 108 | node: document.getElementById("app"), 109 | }) 110 | ``` 111 | 112 | Save the file and reload the browser. You'll be greeted by the words "Hello world" framed in a box. 113 | 114 | 115 | 116 | [Live Demo][1-hello-world] 117 | 118 | Let's walk through what happened: 119 | 120 | You start by importing the three functions `h`, `text` and `app`. 121 | 122 | You call `app` with an object that holds app's definition. 123 | 124 | The `view` function returns a _virtual DOM_ – a blueprint of how we want the actual DOM to look, made up of _virtual nodes_. `h` creates virtual nodes representing HTML tags, while `text` creates representations of text nodes. The equivalent description in plain HTML would be: 125 | 126 | ```html 127 |
128 |
129 |

Hello world

130 |
131 |
132 | ``` 133 | 134 | `node` declares _where_ on the page we want Hyperapp to render our app. Hyperapp replaces this node with the DOM-nodes it generates from the description in the view. 135 | 136 | ## State, View, Action ## 137 | 138 | ### Initializing State ### 139 | 140 | Add an `init` property to the app: 141 | 142 | ```js 143 | app({ 144 | init: { name: "Leanne Graham", highlight: true }, 145 | ... 146 | }) 147 | ``` 148 | 149 | Each app has an internal value called _state_. The `init` property sets the state's initial value. The view is always called with the current state, allowing us to display values from the state in the view. 150 | 151 | Change the view to: 152 | 153 | ```js 154 | state => h("main", {}, [ 155 | h("div", {class: "person"}, [ 156 | h("p", {}, text(state.name)), 157 | h("input", {type: "checkbox", checked: state.highlight}), 158 | ]), 159 | ]) 160 | ``` 161 | 162 | Save and reload. Rather than the statically declared "Hello world" from before, we are now using the name "Leanne Graham" from the state. We also added a checkbox, whose checked state depends on `highlight`. 163 | 164 | 165 | 166 | [Live Demo][2-render-with-state] 167 | 168 | 169 | ### Class properties ### 170 | 171 | Change the definition of the div: 172 | 173 | ```js 174 | h("div", {class: {person: true, highlight: state.highlight}}, [ ... ]) 175 | ``` 176 | 177 | The class property can be a string of space-separated class-names just like in regular HTML, _or_ it can be an object where the keys are class-names. When the corresponding value is truthy, the class will be assigned to the element. 178 | 179 | The `highlight` property of the state now controls both wether the div has the class "highlight" and wether the checkbox is checked. 180 | 181 | 182 | 183 | [Live Demo][3-class-objects] 184 | 185 | However, clicking the checkbox to toggle the highlightedness of the box doesn't work. In the next step we will connect user interactions with transforming the state. 186 | 187 | ### Actions ### 188 | 189 | Define the function: 190 | 191 | ```js 192 | const ToggleHighlight = state => ({ ...state, highlight: !state.highlight }) 193 | ``` 194 | 195 | It describes a _transformation_ of the state. It expects a value in the shape of the app's state as argument, and will return something of the same shape. Such functions are known as _actions_. This particular action keeps all of the state the same, except for `highlight`, which should be flipped to its opposite. 196 | 197 | Next, assign the function to the `onclick` property of the checkbox: 198 | 199 | ```js 200 | h("input", { 201 | type: "checkbox", 202 | checked: state.highlight, 203 | onclick: ToggleHighlight, 204 | }) 205 | ``` 206 | 207 | Save and reload. Now, clicking the checkbox toggles not only checked-ness but the higlighting of the box. 208 | 209 | 210 | 211 | [Live Demo][4-toggle-highlight] 212 | 213 | 214 | ### Dispatching ### 215 | 216 | By assigning `ToggleHighlight` to `onclick` of the checkbox, we tell Hyperapp to _dispatch_ `ToggleHighlight` when the click-event occurs on the checkbox. Dispatching an action means Hyperapp will use the action to transform the state. With the new state, Hyperapp will calculate a new view and update the DOM to match. 217 | 218 | 219 | ### View components ### 220 | 221 | Since the view is made up of nested function-calls, it is easy to break out a portion of it as a separate function for reuse & repetition. 222 | 223 | Define the function: 224 | 225 | ```js 226 | const person = props => 227 | h("div", { 228 | class: { 229 | person: true, 230 | highlight: props.highlight 231 | } 232 | }, [ 233 | h("p", {}, text(props.name)), 234 | h("input", { 235 | type: "checkbox", 236 | checked: props.highlight, 237 | onclick: props.ontoggle, 238 | }), 239 | ]) 240 | ``` 241 | 242 | Now the view can be simplified to: 243 | 244 | ```js 245 | state => h("main", {}, [ 246 | person({ 247 | name: state.name, 248 | highlight: state.highlight, 249 | ontoggle: ToggleHighlight, 250 | }), 251 | ]) 252 | ``` 253 | 254 | Here, `person` is known as a _view component_. Defining and combining view components is a common practice for managing large views. Note, however, that it does not rely on any special Hyperapp-features – just plain function composition. 255 | 256 | [Live Demo][5-view-component] 257 | 258 | ### Action payloads ### 259 | 260 | This makes it easier to have multiple boxes in the view. First add more names and highlight values to the initial state, by changing `init`: 261 | 262 | ```js 263 | { 264 | names: [ 265 | "Leanne Graham", 266 | "Ervin Howell", 267 | "Clementine Bauch", 268 | "Patricia Lebsack", 269 | "Chelsey Dietrich", 270 | ], 271 | highlight: [ 272 | false, 273 | true, 274 | false, 275 | false, 276 | false, 277 | ], 278 | } 279 | ``` 280 | 281 | next, update the view to map over the names and render a `person` for each one: 282 | 283 | ```js 284 | state => h("main", {}, [ 285 | ...state.names.map((name, index) => person({ 286 | name, 287 | highlight: state.highlight[index], 288 | ontoggle: [ToggleHighlight, index], 289 | })), 290 | ]) 291 | ``` 292 | 293 | Notice how instead of assigning just `ToggleHighlight` to `ontoggle`, we assign `[ToggleHighlight, index]`. This makes Hyperapp dispatch `ToggleHighlight` with `index` as the _payload_. The payload becomes the second argument to the action. 294 | 295 | Update `ToggleHighlight` to handle the new shape of the state, and use the index payload: 296 | 297 | ```js 298 | const ToggleHighlight = (state, index) => { 299 | // make shallow clone of original highlight array 300 | let highlight = [...state.highlight] 301 | 302 | // flip the highlight value of index in the copy 303 | highlight[index] = !highlight[index] 304 | 305 | // return shallow copy of our state, replacing 306 | // the highlight array with our new one 307 | return { ...state, highlight} 308 | } 309 | ``` 310 | 311 | Save & reload. You now have five persons. Each can be individually highlighted by toggling its checkbox. 312 | 313 | 314 | 315 | [Live Demo][6-action-payloads] 316 | 317 | Next, let's add the ability to "select" one person at a time by clicking on it. We only need what we've learned so far to achieve this. 318 | 319 | First, add a `selected` property to `init`, where we will keep track of the selected person by its index. Since no box is selected at first, `selected` starts out as `null`: 320 | 321 | ```js 322 | { 323 | ... 324 | selected: null, 325 | } 326 | ``` 327 | 328 | Next, define an action for selecting a person: 329 | 330 | ```js 331 | const Select = (state, selected) => ({...state, selected}) 332 | ``` 333 | 334 | Next, pass the `selected` property, and `Select` action to the `person` component: 335 | 336 | ```js 337 | person({ 338 | name, 339 | highlight: state.highlight[index], 340 | ontoggle: [ToggleHighlight, index], 341 | selected: state.selected === index, // <---- 342 | onselect: [Select, index], // <---- 343 | }) 344 | ``` 345 | 346 | Finally, we give the selected person the "selected" class to visualize wether it is selected. We also pass the given `onselect` property on to the `onclick` event handler of the div. 347 | 348 | ```js 349 | const person = props => 350 | h("div", { 351 | class: { 352 | person: true, 353 | highlight: props.highlight, 354 | selected: props.selected, // <--- 355 | }, 356 | onclick: props.onselect, // <--- 357 | }, [ 358 | h("p", {}, text(props.name)), 359 | h("input", { 360 | type: "checkbox", 361 | checked: props.highlight, 362 | onclick: props.ontoggle, 363 | }), 364 | ]) 365 | ``` 366 | 367 | Save, reload & try it out by clicking on the different persons. 368 | 369 | 370 | 371 | [Live Demo][7-with-selection] 372 | 373 | ### DOM-event objects ### 374 | 375 | But now, when we toggle a checkbox, the person also selected. This happens because the `onclick` event bubbles up from the checkbox to the surrounding div. That is just how the DOM works. If we had access to the event object we could call the `stopPropagation` method on it, to prevent it from bubbling. That would allow toggling checkboxes without selecting persons. 376 | 377 | As it happens, we _do_ have access to the event object! Bare actions (_not_ given as `[action, payload]`) have a default payload which is the event object. That means we can define the `onclick` action of the checkbox as: 378 | 379 | ```js 380 | onclick: (state, event) => { 381 | event.stopPropagation() 382 | //... 383 | } 384 | ``` 385 | 386 | But what do we do with the `props.ontoggle` that used to be there? – We return it! 387 | 388 | ```js 389 | h("input", { 390 | type: "checkbox", 391 | checked: props.highlight, 392 | onclick: (_, event) => { 393 | event.stopPropagation() 394 | return props.ontoggle 395 | }, 396 | }) 397 | ``` 398 | 399 | When an action returns another action, or an `[action, payload]` tuple instead of a new state, Hyperapp will dispatch that instead. You could say we defined an "intermediate action" just to stop the event propagation, before continuing to dispatch the action originally intended. 400 | 401 | Save, reload and try it! You should now be able to highlight and select persons independently. 402 | 403 | 404 | 405 | [Live Demo][8-separate-highlight-selection] 406 | 407 | 408 | ### Conditional rendering ### 409 | 410 | Further down we will be fetching the "bio" of selected persons from a server. For now, let's prepare the app to receive and display the bio. 411 | 412 | Begin by adding an initially empty `bio` property to the state, in `init`: 413 | 414 | ```js 415 | { 416 | ..., 417 | selected: null, 418 | bio: "", // <--- 419 | } 420 | ``` 421 | 422 | Next, define an action that saves the bio in the state, given some server data: 423 | 424 | ```js 425 | const GotBio = (state, data) => ({...state, bio: data.company.bs}) 426 | ``` 427 | 428 | And then add a div for displaying the bio in the view: 429 | 430 | ```js 431 | state => h("main", {}, [ 432 | ...state.names.map((name, index) => person({ 433 | name, 434 | highlight: state.highlight[index], 435 | ontoggle: [ToggleHighlight, index], 436 | selected: state.selected === index, 437 | onselect: [Select, index], 438 | })), 439 | state.bio && // <--- 440 | h("div", { class: "bio" }, text(state.bio)), // <--- 441 | ]) 442 | ``` 443 | 444 | The bio-div will only be shown if `state.bio` is truthy. You may try it for yourself by setting `bio` to some nonempty string in `init`. 445 | 446 | [Live Demo][9-conditional-rendering] 447 | 448 | This technique of switching parts of the view on or off using `&&` (or switching between different parts using ternary operators `A ? B : C`) is known as _conditional rendering_ 449 | 450 | ## Effects ## 451 | 452 | ### Effecters ### 453 | 454 | In order to fetch the bio, we will need the id associated with each person. Add the ids to the initial state for now: 455 | 456 | ```js 457 | { 458 | ... 459 | selected: null, 460 | bio: "", 461 | ids: [1, 2, 3, 4, 5], // <--- 462 | } 463 | ``` 464 | 465 | We want to perform the fetch when a person is selected, so update the `Select` action: 466 | 467 | ```js 468 | const Select = (state, selected) => { 469 | 470 | fetch("https://jsonplaceholder.typicode.com/users/" + state.ids[selected]) 471 | .then(response => response.json()) 472 | .then(data => { 473 | console.log("Got data: ", data) 474 | 475 | /* now what ? */ 476 | }) 477 | 478 | return {...state, selected} 479 | } 480 | ``` 481 | 482 | > We will be using the JSONPlaceholder service in this tutorial. It is a free & open source REST API for testing & demoing client-side api integrations. Be aware that some endpoints could be down or misbehaving on occasion. 483 | 484 | If you try that, you'll see it "works" in the sense that data gets fetched and logged – but we can't get it from there in to the state! 485 | 486 | Hyperapp actions are not designed to be used this way. Actions are not general purpose event-handlers for running arbitrary code. Actions are meant to simply calculate a value and return it. 487 | 488 | The way to run arbitrary code with some action, is to wrap that code in a function and return it alongside the new state: 489 | 490 | ```js 491 | const Select = (state, selected) => [ 492 | {...state, selected}, 493 | () => 494 | fetch("https://jsonplaceholder.typicode.com/users/" + state.ids[selected]) 495 | .then(response => response.json()) 496 | .then(data => { 497 | console.log("Got data: ", data) 498 | /* now what ? */ 499 | }) 500 | ] 501 | ``` 502 | 503 | When an action returns something like `[newState, [function]]`, the function is known as an _effecter_ (a k a "effect runner"). Hyperapp will call that function for you, as a part of the dispatch process. What's more, Hyperapp provides a `dispatch` function as the first argument to effecters, allowing them to "call back" with response data: 504 | 505 | 506 | ```js 507 | const Select = (state, selected) => [ 508 | {...state, selected}, 509 | dispatch => { // <--- 510 | fetch("https://jsonplaceholder.typicode.com/users/" + state.ids[selected]) 511 | .then(response => response.json()) 512 | .then(data => dispatch(GotBio, data)) // <--- 513 | } 514 | ] 515 | ``` 516 | 517 | Now when a person is clicked, besides showing it as selected, a request for the persons's data will go out. When the response comes back, the `GotBio` action will be dispatched, with the response data as payload. This will set the bio in the state and the view will be updated to display it. 518 | 519 | 520 | 521 | [Live Demo][10-effecter-with-dispatch] 522 | 523 | ### Effects ### 524 | 525 | There will be other things we want to fetch in a similar way. The only difference will be the url and action. So let's define a reusable version of the effecter where url and action are given as an argument: 526 | 527 | ```js 528 | const fetchJson = (dispatch, options) => { 529 | fetch(options.url) 530 | .then(response => response.json()) 531 | .then(data => dispatch(options.action, data)) 532 | } 533 | ``` 534 | 535 | Now change `Select` again: 536 | 537 | ```js 538 | const Select = (state, selected) => [ 539 | {...state, selected}, 540 | [ 541 | fetchJson, 542 | { 543 | url: "https://jsonplaceholder.typicode.com/posts/" + state.ids[selected], 544 | action: GotBio, 545 | } 546 | ] 547 | ] 548 | ``` 549 | 550 | A tuple such as `[effecter, options]` is known as an _effect_. The options in the effect will be provided to the effecter as the second argument. Everything works the same as before, but now we can reuse `fetchJson` for other fetching we may need later. 551 | 552 | [Live Demo][11-effect] 553 | 554 | ### Effect creators ### 555 | 556 | Define another function: 557 | 558 | ```js 559 | const jsonFetcher = (url, action) => [fetchJson, {url, action}] 560 | ``` 561 | 562 | It allows us to simplify `Select` even more: 563 | 564 | ```js 565 | const Select = (state, selected) => [ 566 | {...state, selected}, 567 | jsonFetcher("https://jsonplaceholder.typicode.com/users/" + state.ids[selected], GotBio), 568 | ] 569 | 570 | ``` 571 | 572 | Here, `jsonFetcher` is what is known as an _effect creator_. It doesn't rely any special Hyperapp features. It is just a common way to make using effects more convenient and readable. 573 | 574 | [Live Demo][12-effect-creator] 575 | 576 | ### Effects on Init ### 577 | 578 | The `init` property works as if it was the return value of an initially dispatched action. That means you may set it as `[initialState, someEffect]` to have the an effect run immediately on start. 579 | 580 | Change `init` to: 581 | 582 | ```js 583 | [ 584 | {names: [], highlight: [], selected: null, bio: "", ids: []}, 585 | jsonFetcher("https://jsonplaceholder.typicode.com/users", GotNames) 586 | ] 587 | ``` 588 | 589 | This means we will not have any names or ids for the persons at first, but will fetch this information from a server. The `GotNames` action will be dispatched with the response, so implement it: 590 | 591 | ```js 592 | const GotNames = (state, data) => ({ 593 | ...state, 594 | names: data.slice(0, 5).map(x => x.name), 595 | ids: data.slice(0, 5).map(x => x.id), 596 | highlight: [false, false, false, false, false], 597 | }) 598 | ``` 599 | 600 | With that, you'll notice the app will now get the names from the API instead of having them hardcoded. 601 | 602 | [Live Demo][13-init-effect] 603 | 604 | ## Subscriptions ## 605 | 606 | Our final feature will be to make it possible to move the selection up or down using arrow-keys. First, define the actions we will use to move the selection: 607 | 608 | 609 | ```js 610 | const SelectUp = state => { 611 | if (state.selected === null) return state 612 | return [Select, state.selected - 1] 613 | } 614 | 615 | const SelectDown = state => { 616 | if (state.selected === null) return state 617 | return [Select, state.selected + 1] 618 | } 619 | ``` 620 | 621 | When we have no selection it makes no sense to "move" it, so in those cases both actions simply return `state` which is effectively a no-op. 622 | 623 | You may recall from earlier, that when an action returns `[otherAction, somePayload]` then that other action will be dispatched with the given payload. We use that here in order to piggy-back on the fetch effect already defined in `Select`. 624 | 625 | Now that we have those actions – how do we get them dispatched in response to keydown events? 626 | 627 | If effects are how an app affects the outside world, then _subscriptions_ are how an app _reacts_ to the outside world. In order to subscribe to keydown events, we need to define a _subscriber_. A subscriber is a lot like an effecter, but wheras an effecter contains what we want to _do_, a subscriber says how to start listening to an event. Also, subscribers must return a function that lets Hyperapp know how to _stop_ listening: 628 | 629 | ```js 630 | const mySubscriber = (dispatch, options) => { 631 | /* how to start listening to something */ 632 | return () => { 633 | /* how to stop listening to the same thing */ 634 | } 635 | } 636 | ``` 637 | 638 | Define this subscriber that listens to keydown events. If the event key matches `options.key` we will dispatch `options.action`. 639 | 640 | ```js 641 | const keydownSubscriber = (dispatch, options) => { 642 | const handler = ev => { 643 | if (ev.key !== options.key) return 644 | dispatch(options.action) 645 | } 646 | addEventListener("keydown", handler) 647 | return () => removeEventListener("keydown", handler) 648 | } 649 | ``` 650 | 651 | Now, just like effects, let's define a subscription creator for convenient usage: 652 | 653 | ```js 654 | const onKeyDown = (key, action) => [keydownSubscriber, {key, action}] 655 | ``` 656 | 657 | A pair of `[subscriber, options]` is known as a _subscription_. We tell Hyperapp what subscriptions we would like active through the `subscriptions` property of the app definition. Add it to the app call: 658 | 659 | ```js 660 | app({ 661 | ..., 662 | subscriptions: state => [ 663 | onKeyDown("ArrowUp", SelectUp), 664 | onKeyDown("ArrowDown", SelectDown), 665 | ] 666 | }) 667 | ``` 668 | 669 | This will start the subscriptions and keep them alive for as long as the app is running. 670 | 671 | But we don't actually want these subscriptions running all the time. We don't want the arrow-down subscription active when the bottom person is selected. Likewise we don't want the arrow-up subscription action when the topmost person is selected. And when there is no selection, neither subscription should be active. We can tell Hyperapp this using logic operators – just as how we do conditional rendering: 672 | 673 | ```js 674 | app({ 675 | ..., 676 | subscriptions: state => [ 677 | 678 | state.selected !== null && 679 | state.selected > 0 && 680 | onKeyDown("ArrowUp", SelectUp), 681 | 682 | state.selected !== null && 683 | state.selected < (state.ids.length - 1) && 684 | onKeyDown("ArrowDown", SelectDown), 685 | ], 686 | }) 687 | ``` 688 | 689 | Each time the state changes, Hyperapp will use the subscriptions function to see which subscriptions should be active, and start/stop them accordingly. 690 | 691 | 692 | 693 | 694 | 695 | [Live Demo][14-subscriptions] 696 | 697 | ## Conclusion ## 698 | 699 | That marks the completion of this tutorial. Well done! We've covered state, actions, views, effects and subscriptions – and really there isn't much more to it. You are ready to strike out on your own and build something amazing! 700 | 701 | 702 | [1-hello-world]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgUEQhMawfACehFp8WgDmpPDwbjzFkYHWKbHWCZ7Wzq7uPOYgBM4IbXJcwgDsSABMOCAAvni8AkIiAFZcMnIKSsIQYMbkpOJawKJ4WgoAHuKHJsYTWqwUYFqGEuLGhEhWVsRQPBiEANa5xnxiD8MFB4BorKJ-vBSBdDA4HBcABTAGoaCDwADuSC0iIAlFpdOYtKJEYYwHwIDxDIdgFMtDQat4SYZbNS9sQcoQXvcQMZoYQ5IY6QyvIEmaTeWzaYcTuIJQAJeBUchaDHbWBQQy43F4RlaJg6moG3WinjkUHYqDkYhxQTyDDFCoAUQQdvEACFcgBJKAS2EgQ08Ca4hxdECyTYcaHKABGfBjSrDPRcbn6ygAzEgAIx0SbTED8QTKDDELlh2TyRTiZQAAQ2Wx2WjipFgiIA5I9nq93p8MItQSkNKQMDwKlYeMYwFYMXwFMPS4QayMrOQ4uIrClihIS1y27iANzwnhICjkXYo0UAWkvMfIx0vGOg4lE2JGdDoxmOh6DR-JlL2WhbIQECpseWhlDkbgaPA+4TEeGB8qQApeBe3i3vewEAF6UsU2K3qQoKkDed7ft4j5QM+2IaHwpCIte6EPk+ogHjUAIwDh2I5p+WhcccPEfnxKACaRgHkMBoHYhBs6aDBNT4YRnHcQAxDAUBaAKKRQCJ8nQpeMLqHE3I4J++7eDU5KkMUlLEeI4jkGAnHCTUNpIds2JbJSc7fnBVJfIhyFiBAW6bhIexyYCPylKuPBQJesiwG5WhKawUBYN5DgIfycjfEqKbwGpqFaDpRHkZRWjpiZcnbIRcWUIlSk4DgxAiWx6hVJednGNiAAclWiq1OE2XZDlaL1X4OD5GX+XIOgTmuNDiFCujEKILg-OhTBhf1Ykge02Lxhpa6yaKLkCqQlrwKwfAJOIImdY5fXeAgrDiNigx9T5mWuV8yauPls3GPNi18stq1AhtW1PZdr1aMZ42ivdo0ffB01eMYkNaBZVlgXQIlY9Zz0wyMyN+VlP25X9ano4V+M8JehPYrjE33L5GAxu4GNAbtcj7TGh0KCJDNaMQfCwMQiLUbR9F3oxFHMVoADUWgjGUYAsQj5BdVoaBOSddXnUljXNTUrArJehCLQg2IgaLEDG6KpvyObECYfAr6q3d8CnJelKgvI2KXlmHsTRl7PhAmptlOyKxVtibaGG2sGh+4SB8C90LR5W-taPHICJz5SYU6BAwgHQSB0JeOBYOXkxMBMQA 703 | 704 | [2-render-with-state]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgUEQhMawfACehFp8WvyChTxQhVrEoi4A1gBG5AAekYHWKbHWCZ7Wzq7uPOYgBM4I-XJcwgDsSADMaCAAvni8AkIiAFZcMnIKSsIQYMbkpOJawKJ4WgpN4lcmxotarBRgWoYS4saESFZWxFAeBhCLVcsY+MRahgoPANFZRGD4KQHoYHA4HgAKYCtCA8CDiJDnYprQmGAAy8D4PB48C0AHFkaIBIYrqIIABzUQpTkE66kOK05atDQQeAAd0JhHEfAUWl05i0ogxhjAfFxLPOyy0NFa3iVhlsGuAxByhB+7xAxiRhDkhi1Oq8gT1ystRq1N3EGKlMvgGBK8AAlAG8LrAvqQLjjHFxEbxIjSSBqnVGi1hlUapD4FBJdKFGIOVyC+JFsHQ0xS47yyHHTxyDDCVByMQ4oJ5Bh2fBxABRBCt8QAIVyAEkoC6USAKyWHMNE+Qjhwkcp6nx6vBpEQ1y43BNlFhUABaFBIAAcSxWIH9ygwxDNM9k8kU4mUAAFDsdTlo4qRYBiAOSfb5fn+QEMC2GEUg0Ug-U7KweGMMArDFH0oJvQhnwAJischoysbkJGvM1fwDABuNEeCQChyDObFHX3fcU33MVoHEURCXQug6GMJpSJ4RYyNVXEiWOQh8QGQlSDXGVNHgYi+J4BwMCtUgbS8GjvAYkSAC9cXZQlGlIGFSHo5oeO8JioBYwkND4UgMTohjzJYkjWnBGAdMJABGTimi0LyuN87ytBQbzTK0YTRLkcTJLcDQZNafTDM8-yAGIYAqG0UigUKEqRfdkXUOJzRwLjiO8VpVVIdlcWM8RxDnTyQtaZtlJOQljlxBRSB4uSFKUlT805PDqPiiFanZCg4nKfdZFgVqtGS1goCwbreutORgU3Vws3OeKTkMxjmNYrRZhK3aDNyma5uSnAcGIULXPUHh2X3OrjEJY9TsdB6dJquqwHez6eqBPq5B0ODoxoOMrV0JNIRTJgdq+8gRO3cjCnqDLozix1mptUgG3gVg+AScRQtehrPu8BBWF5KZAdWlqgVGLdtsjCGofgGGMwaZoEbUrRqd5YruNacmtA+kXeLIxS1q8YxEe8CqqrRuhQqV6rBbY+ngdljaxgUCp5f59WeH3TWtFVhwnml+p3AVsLkYitGV0xhRQvN4g+FgYgMWs2z7OaA6LNEAMtAAai0dCJLAZzHTFtBGpxygrpuu7WlYXZ9ylXIEEJfEvYgNPHQz+Qs4gTT4DY6OyfgW591xGF5EJfcPOrq2FNt8JVwziTznvPZeV-Qxf1kjv3CQPgaaRPvdkfQkh5AEe5JnZnxh4SYQDoJA6H3HA9zoJYmEWIA 705 | 706 | [3-class-objects]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgUEQhMawfACehFp8WvyChTxQhVrEoi4A1gBG5AAekYHWKbHWCZ7Wzq7uPOYgBM4I-XJcwgDsSADMaCAAvni8AkIiAFZcMnIKSsIQYMbkpOJawKJ4WgpN4lcmxotarBRgWoYS4saESFZWxFAeBhCLVcsY+MRahgoPANFZRGD4KQHoYHA4HgAKYCtCA8CDiJDnYprQmGAAy8D4PB48C0AHFkaIBIYrqIIABzUQpTkE66kOK05atDQQeAAd0JhHEfAUWl05i0ogxhjAfFxLPOyy0NFa3iVhlsGuAxByhB+52MSMIckJ4n58FZHK5Tt5Upl8DETu5EkWWp1XkCeuVIDMw01Vxu4gxboUGBK8AAlAm8LrAvqQLjjHFxEbxIjSSBqnVGi0w0XIfAoJLpbG2ZzveJFsnU0xmwHWymAzxyDDCVByMQ4oJ5Bh2fBxABRBDD8QAIVyAEkoMGUSA202HMNC+Qjhwkcp6nx6vBpEQTy43BNlDhUABaG9oKZLFYgePKDDEM1b2TyRTiZQAAKHMcpxaHEpCwBiADknzfL8-yAhgWwwikGikHG45WDwxhgFYYruuhn6EABABMVjkNmVgNh+ZpQQmADcaI8EgFDkGc2IBret4lreYrQOIoiEiRdB0MYTSMTwixMaquJEschD4gMhKkCeMqaPA9FSTwDgYJapDWl4HHeDxCkAF64uyhKNKQMKkNxzQSd4fFQAJhIaHwpAYlxPHOQJDGtOCMAWYSACMolNFoYViZF4VaCg4WOVo8mKTaWgqTkbgaBprTWbZoXRQAxDAFTWikUCJblSK3si6hxOaOBifR3itKqpDsri9niOIO6hQlrSDvpJyEscuIKKQElaTpekGZ69YuucOUQrU7IUHE5S3rIsBDVoBWsFAWATVNVpyMC56uJWC0BpVdm+YJWizI1OUnLZG2UNtBU4DgxCJYF6g8Oyt7dcYhIAByPQGv0WZ13VgKD4OTUC01yDo2HZjQeaWro5YNM0TCXd4yWXsxhT1KV2bZQGA3WqQfbwKwfAJOIiVA714PeAgrC8lM8NHYNQKjBeF2ZmjGPwFjNSQiWeNGVoHO8g14mtCzWhg4rklMbpx1eMY+NaK17XE3QiX6x1ctCTziNa6dYwKBUOsyybPC3mbWhGw4Twa-U7i64TSkk2TCiJS7xB8LAxAYu5nnec0vH8aICZaAA1FoJEqWA-kBsraB9ZTb00ztn3fa0rC7LeUq5AghL4qHEBFwGJfyGXECmfAQlp8z8C3LeuIwvIhK3iF7fuzpXvhMeJcqecP57LyUGGFBmkj+4SB8JzSJT7sf6EnPIAL1pW4C+MPCTCAdBIHQ95YOfSxMIsQA 707 | 708 | [4-toggle-highlight]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgQAq5ADmWQha4qLwWsSFxADWAEbkAB755HU5eQVFohBZoint4pGB1imx1gme1s6u7jzmIATOCGNyXMIA7EgAjABMIAC+eLwCQiIAVlwycgpKwhBgxuSk4lrAonj58NXiTybGm1qsFGBahhJxMZCEgrFZiFAeBhCGUAJ7GPjlDBQeAaKyieHwUgfQwOByyHiEO6ZRrwAASbQ6lLuui0RL4Ci0unMWgAFMAtBgufSFE9Wu1OhIkFoAIQ8+BiSmCu6bACUeP4pnZPQgPAg4mFHP4gmFhgAMvA+DweEUAOLY0QCQx8qXU4XiUhxIrbHoaCDwADuwvFTJZolZhjAfFV1vu2y0NB63n9hlsoeAxFgfEIIPuxixhDk9sd8BtArtdPEDIl-KpXU24cjXkC0YDIDMUzDTwUr1Z4ow2vgstleCjgRjIFVxji3UbwD7NfEmN1IBKLkqNWtE8Cc-K8Cg3qLCkl+a6verNe8ckTEHKwpJuXJtr3y7l+5rTB7PUf960PHIKOFUHIxDignkGBZPA4gAKIIP+4gAEKwgAklAdY4iAT48HKDhTLO5BXBwWLKBUfAVPA0hEIRLhuPMyjrEgADMVFbDsICdsoGDECm6EEmc4jKAAApc1y3FocSkLArIAOSAsCoLgpCGBHCiKQaKQHbAVYPDGGAVgesWiksYQXFrFY5AjlY0rMSmImygA3AqSAUOQdzjtWAC0jlVNUjketABTCmsdB0MY1RWShCpBqq9xaNchDquMwqkIRDKaPAFmbAqGDpqQmZeA53iuY5kUAF6qlkwpVKQKKkC5NSBd4HlQF5WgaHwpCss5OU1QUlk9AiMCFcKKx+bUfX+Vog21Cg-VVeF5CRWRPAxXFbgaIlPQlWVvVDQAxDAUB0pQ0ATStWKOdi6hxKmOD+RZ3g9EGpBZKqFXiOImG9eNPS-ulNzCtcqoKKQgXJTwDipRmcg7mWEj3MtiJlFkFBxDwUCObIsCfVo62sFAWD-UDaUZdCJGuOukPVgd5VtaIwpURdy03GVSOUKj604DgxATV16g8FkjlPcYwoABzU9W7OFQ9T1gPzgsAzjINeEOI40FO6a6KuC7VEwxPeBFUVZlo+GZrAI5LdW72ZqQX7wKwfAJOIE08y9gveAgrAaloiyS9LH1QjMpFE3L4gK5iyulKr6tZVoTsu+dAU9HbWgC9HQWA1CuNyOFGtaDdd2zVodATZn90R957vJzL+OzAo23GOn+c8I5hc5-9-xJxgFTuOnWszcKeuUIbE318QfCwMQrINU1LU1O5nmiLKWgANRaGssVgB11ax2gr3GwzZto8zrM9Kwpy5VOCDCuqg+nhNB-yLlEB5fA3lL7bLziI5qoovIwqOSsj8OADLfuEgAiB9Yr3HYooF2IlDAiSSkDVu4Q+DOyxKA044DhSQJANAgG6FvZzEJMoOgSA6CORwFgQhWwmCbCAA 709 | 710 | [5-view-component]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgQAq5ADmWQha4qLwWsSFxADWAEbkAB755HU5eQVFohBZoint4pGB1imx1gme1s6u7jzmIATOCGNyXMIA7EgAjABMIAC+eLwCQiIAVlwycgpKwhBgxuSk4lrAonj58NXiTybGm1qsFGBahhJxMZCEgrFZiFAeBhCGUAJ7GPjlDBQeAaKyieHwUgfQwOBxgrQAWmJWgAggBhdIASQA8gA5ADKROJeJ4sh4hDumUa8AAEm0OgK7rotJy+AotLpzFoABTALQYRVihRPVrtToSJBaACEyvgYgFGrumwAlKyCcTCVoAGpUgCiAHUtOSaQBZAAK9LtdPSTMtrPZnK0xixhDkkuDFGBktiohlhlshiewB63mIsD4hBB91TgRDpDDPC14lIcXgeFz3jVgq6WuMUcIBvVQtzmx62y0NFzccMZim9w7CleMvr5GBGH4ghNJorXkCPZAEB4xji3X7KbngXymK1hhKLkqNSTleKpTK8CgdYbTZrElnW+8cnTEHKV7HjdO2Vy5dbM56TDNHh8SsZkrQAJQAVTpUDWQ+OUeiXCBxC1eVJ3gXcQAAGXgPgeB4IoAHFsVEAQky0asjWLUsim2HoNAgeAAHctT1GNyPjcA+CXMjgA7LtN2DUM5HggTvDQljxHFfU0PvB8KKFCSpJvI1ZK3T8eS1blv35ZsulUrRTVkgDZJ4cgUS1KByGIOJBHkDAsngcQ7QQWzxAAIVhKkoA4nEQD-HhTQcKYQFkK4OCxZQKj4Cp4GkIhYpcNx5mUNYAA5Vg2bZdkEZQMGILNgvZM5xGUAABS5rluLQ4lIWAZQAckBYFQXBSEMCOFEUg0UgJ0cqxlzAKxGKknr8sIUq1ischVysI08qzeqTQAblZJAKHIO4N28YkqmqQlGOgAotTWOg6GMaoVoC1kwC4rx5WuQgkPGLVSFi8VNHgJa2yAqF80LHM512wlHoALyXLItSqUgUVIQldsu7wDqgI6tA0PhSBlHaan2w7RGWnoERgcGtRWM7alJ86tAp2oUDJhHg3IR6kqLLRXozNwNE+nooZhknKYAYhgKBRUoaB6Z5rFCWxdQ4mzHBzqW7wehu0gsiXOGNvEcgwBJumemsgsbjrcglwUUhLu+hwMD+uRlKFAHvCi8osgoOIeCgQlZFgI2tH51goCwC2rZtqEZkSi8Ha0CXYaRlGAGYFe5m4Yc9ygff5nAcGIenCfUHgskJLXjC1VLE7nXPwY18QtZ1rRS4uhxLd+oSvCXFdxBocRMV0fdyl2phI4ep65C1aKw1gVcubnA2w1ICz4FYPgEnEemi91svvAQVhkK0RYy++62W+hBLXAjtvV077ve8PaoB62rQt53+WG7nNe6-31lD8NrxjEjlW1ZZnQem-91aP2Oh-Zu39j6zAUMLX+98QE8EJGArQQDG7-B+hgCo7hB6M2HizMelBJ70xQcQPgsBiAyjRhjLGe1Y54y0AAai0GsV6YB8avzHFqNAetp5pznr7TO2ceisFOMDLuCAtRIXIS+emoj5DAwgCDdCLC2GrxeOIQkS4UTyC1ISFYajG5W2weEGKojXr3CKooHe9VDD1S+sY9wSA+DbyxJY041itS2JAPY76wUw5zA5MoOgSA6CEhwFgUJWwmCbCAA 711 | 712 | 713 | [6-action-payloads]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgQDCoi4A1lriWVoARuQAHlo88CXieeRafOUC8DVaohAA5qIpHdUQ4pGB1imx1gme1s6u7jzmIATOCJNyXMIA7Eg4dCAAvni8jcoYAFZcMnIKSsIQYMbkpNXAonh5leJPJsZbWqwUYFqGEuJjIQkFYrMQoDwMIRsgBPYx8YjZDBQeAaKyiOHwUjvQwOBygrQAWmJWgAgukACoASQA8gA5ADKROJeJ4sh4hGqFPIbTaCAAEu1OkLqrotAAKTl8BRPCA8FElACUWl05i0wH6BLAfGyTUIoj4VHIAHctMRYHImuRWFpbu05YaWkKuhI6qRsTD+ghqq0Oi7RVoaBhg1KFGJnSKmKzvATWCljHkCr7hd0tBpDXErTa5QqdF58k1ZMZPV4nX6RTQc5UmCqtABCZP+yvy6v9TVWLSkeDiOKkLz6w0W01FmG2m3kXtaUPwJ5d4ywBFytp-UsEgtllOuvjuvij429US2ycVU2W-pdnt99VaYNQ8TSmcb-1bBwvnj4jvEwlaABqVIAogA6lo6Q0gAsgACvS-50hSTJfqy7KcloxhYoQci1sYFBAiqsSiOKhi2IYTwaqW3jmnwhDAuq-SBChaFyEgeSkJmeC0YEjYikxWHkEC4blt07Fvt4OyBux+GGGYszqqJChVOKPF8fwgiKoqbFkS0BEgHKxhxH00mkXR3jiJiTGGMQWSIsUJTEex5GWbqUDcdhhD8Zurx2babIpIizm8a5Zw8nyM5CWp-RMIqrIEl+WgAEoAKp0syhKsu84qGXmvRMRl3jKfAwIAAU0J5hgADLwHwPAVFoADi2IGmAtkad4hj-qQGhylo-ImvAVBNUZfwgOkCCCPIcpNAAQnwcQWf1RmGBB0qkBAxAQPU5WFIQCLZHNdGGJkvXOKOQQQN2y2zbM7EsOJEbdExxXNV8hrOOpA3iCxIWPawz2fQN32wC9nn-YDGnXaWOz9B18DGkx064ZphjanKxEyU8D10be04YHlrnasY4rinlsotkq8OoaQ6E8OlnlE55nF3VO95hvTEjNgqYMDYFvIIPd3Lc-AgoCRIxPs69gRbKpYsRWLPDkCiTFQOQxBxKN4gYG03b-iNijiBNMJUlAWk4iAYU8BLDizCAsjXBwWLKIUfCFL1lvzC4bhLMoADMSCbDseyCAcxBUZb7LnOIygAAJXDcdxaL2sDigA5ACQIgmCEJHIQKIpBopDY92Vg8MYYBWMaD550HhARwATFYE7iFY-oYJXieKgA3KySAUOQ9z9MS1mEvuUD5Ex1d0HQxglB3ZuskjXjACh5CEL0UxMV2C5uBo8Bt2+DgYOTlM0aWA-LwAXkuTHFKQKKkIS1nT94Q8j2m27iv3pSD9A+Tt-08IwBfWgACME8yjAMnkAkBWgUAgIfovZe7seBr16tKTQ29+hXxvkxQB4CADEMAoBTkoNAWBGCsSEmxOoOI1EcCTzbi1Us2pSBtDlHfHu4hyBgCwTA-oysKa3G4uQOUChSDT13pCA+cg3L+iPt4B2iI2gUDiPKQksgLSkCYjg1gUAsCiL3hIyErtXDwAIRlUht8n6iCYp7Wh6Dbg3xUZQfhWgcE4BwMQWBf91A8DaISdhxgmIAA4bGlk8UuVh4h2GcK0EEqer5WT7wYl4HSekaAmVQroCyORrI1gyjceBq86ibUoHpNBpZeHoXUVoFE30EjiFgX4rhwTvAIFYOIJiKxglvgSXwgxvU3bGLzLpcQqTMQZIctkmRWgWltK0DQ2JpYGnRM6fE-RKFJmMOYYgrQdBYEbJYdM0eyzxGJKhH0oxBCEwZT2TwQkBztmiJXHvQo7hJl5JXoxQp6FYAlNgXc4ghpiDinTKQN+rCSif2HqIZUABqLQ1cuxgB-gs3iTE0DcLKY4ypLi3GwNYGcQknIYQ8x0PeHyuL8Vn3gKPBF9SXiEirPIJihJAE0riZCZ54QnZ4q7OqUOOsmKJ0MInHeTz3BID4K0rEvKzj8q0IKkAwq3wuzOQg5YIA6A+0JDgLAPtthMC2EAA 714 | 715 | [7-with-selection]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgQDCoi4A1lriWVoARuQAHlo88CXieeRafOUC8DVaohAA5qIpHdUQ4pGB1ilpKkP9gVrpKcS5cnUNgs1gfNlNvXkFhs4IrvBQhvPwYz5Wo17HCZ7Wzq7uPOYgBFsubnJcwgCMdEg4AEwgAL54XiNZQYABWXBkcgUSmEEDAxnIpGqwFEeDylXE6JMxn+WlYFDAWkMEnExkISCsVmIUB4GEI2QAnsY+NMMFB4BorKJmfBSDjDA4HFStABacVaACC6QAKgBJADyADkAMpi8VCniyHiEaoy8htNoIAAS7U6Zuqui0AApdXwFOiIDwOSUAJRaXTmLTAfoIaqtDpdCQerQ0DDhu0KMRmoPiJj9APm7o0J0upghgCEidjKedlXjZ1I8HEcVIXmAWnD9PE9vg6OzFv+DibPAc2t1WhV8G2lptkbrWieOyg7s9NuAVf76KHCig-1dmpF4tFWgAanKAKIAdQmCoAsgAFZUbpUytXLzXt6rGPmEWZW4wUcke2Kia2GWyGdE+s7eYiwPhCApb0jm8G9SDvHgkDyUg4jrUCWhjC1oMfchyWjQMLTwBCZ12FCn0Ielu2eXZ0UCEUVAvX8tEBI45H-CBpnwtDCNeYjXDI8YtAoqjvEBUMjjfQwzAeb1+IUKprVQ9D+EEV1XWw6ihJAJ1jDiPpRJ-Li8l5aDDGILJpmKEovwQgycjwrRpMIhtukU7T6KmbJmPQ6EDSNeDqPneytCYBdWx4JcJQAJQAVSVdVRU1HFrS0nQeF6aC4u8WT4ApAACmgEMMAAZeA+B4CotAAcX5UQBFM6jvEMDdSA0J0tGNcgAHdu1gSrtMMSZQkUNwioAIT4OIDI6rjDAPe1SEYiB6jywpCFZbJRvGLqslgZxGS0IIIGLKaRoeI4WEEpDumgrKqvxPh1s87TxFgm6uNYK7nB88Ynuu17Anel7Ds+3CoGgngElgdF+los56vgZroP7F8WnfcA+CdL8xPRc7xknGso1SwilmMa1rVSx08zdOHwMg2KEKJhDbIkGGsfgDCkwkXM00+7w3MNBAzv1Ln4FNTC7PitmcPY2d6drIie12D1dCtVNKnRHjeK4tiezOrse2JtntPGZWNS8+SfL8nyeHIDloKgchiDiQR5AwNpiw3BA7fEfrGTlKAEYFEAFObfyHhAWQEQ4PllEKPhCm7QOZ1uN4QHeABmJA6ABIEQFS0FiCAwPtRhcRlAAAXhRFkS0UtYGtAByUlyUpalaXBQgORSDRSAwCpxCsHhjDAKxmtrdvs8IQufisch1JOC0MGHqvXQAbk1JAKHIFF+nFYzRWa6B8mgn46DoYwSkXngWwcJYGorRFCF6W5oKLAC3A0eB57PulydmOLN5vgAvJ02mgsUUgHJSCimMifbw28oC7y0BoPgpBrQb1KFvHeogF79BZDAf+0FPhHy0LgsoBCtAoEPsfDB5Ab4vCgloB+9pNAv36EAkBOC8EAGIYBQEHJQaAECihIhAaKfk6g4jARwEfee1UzhLFIG0J0YDV7iHIGAHBpDeE2wgkiFC5AnQKFICfN+GAP50lpmvM4EdphtAoHEZ0opZCwE0VoVhrAoBYH0Q4Qxt45BSxIpwr+-C+QoOgaIaCidxGMP8aAuxDjWE4BwMQXhmD1A8DaKKRRxhoIAA4wlnESf-eR4hFHKK0Fkshp9NQeI0V4VS6kaDiF5LocyRlSjpjitfW+choKRzvLAdSDCzjqLvKQS28AnoJHELwtJKjsneAQKwcQ0EADs2SWwVMgt44c8U1LiFqfUxp2RjItN9CM+ZWgxGlO8JM4pyzylGKsiBKR8DZHULoLw6RTzRSzJOT8a579PF0n+ncuKby5GfOgi85sxIAoYEKO4e5YEKHtOoV0ygvTeGgq0MQK6xBrRwIQUgkogT8jugANRaB+EWMA6CziXLQKo-oUShmONifE-orBoSil1IybmOgaxTF4Wy+QHKIA-3gHvClEzMSigVvIaCop3jiubO4mF4Qo5sqLN6POvVoJV0MFXV+Sr3BID4HMvkGroRaq0DqkAeqWwxzFnHZQXw6CihwFgFOAImD-CAA 716 | 717 | 718 | [8-separate-highlight-selection]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgQDCoi4A1lriWVoARuQAHlo88CXieeRafOUC8DVaohAA5qIpHdUQ4pGB1ilpKkP9gVrpKcS5cnUNgs1gfNlNvXkFhs4IrvBQhvPwYz5Wo14Dp+PekxDTEDxtc8RZ08VlUOTwhOXk1Vsu1fkmvxBEdBql+tYEp5rM5XO4eOYQAQ-nC5FxhABGADMSBwdBAAF88LxGsoMAArLgyOQKJTCCBgYzkUjVYCiPB5SriDkmYwErSsChgLSGCTiYyEJBWKzEKA8DCEbIAT2MfGmGCg8A0VlEKvgpF5hgcDmlWgAtBatABBdIAFQAkgB5AByAGVzRbjTxZDxCNVbeQ2m0EAAJdqdcPVXRaAAUfr4Cg5d01JQAlFpdOYtMB+ghqq0Ol0JBmtDQMOX4woxOGi+ImP0CxHujRk5UmCWAISN2stngp+tnUjwcRxUheYBacsK8QJ+Ac7uRgkOJc8Bw+v1aV3wbZR2OVudaFEKKDpzOx4BT-cco+7Ampr2mi1mrQANXtAFEAOoTR0AWQACi677Ora7pPl667VMY+qELM0bGBQEoZrEogxoYtiGByOZnN4xCwHwhCStmRzeNBpCwTwSB5KQcRziRLQ1pGVEIeQErVoWkZ4PRN5QMxiGEAq27-LsXE4VoRJHHIeE3NkfGsQJaJCa4oneESpZHKhhhmEi2ZqQoVQxixbHAvAqapipgSaSAdzGHEfQ6dhlx5HqVGGE8OSvJh9HudMuxyWxC7dBZ4xSVMsmxgA+hyWqKOIp5Zo5TlaDF8jTqx-6IXwbQJvCMb3mJ4xDiOY5aEZCnyIGwaHAV4nBeJ5n9Ew+UmlYHrPgASgAqs6bVeryMaJXcvRUYl3gmZKAAFND0YYAAy8B8DwFRaAA4gaogCF5NWGO+pAaHcWghuQADu26wFtTmGJMoSxXcTQAEJ8HETwXZchj-gmpA3BA9TzYUhBqtkr3jFdWSwM4SpaEEEDDl9L1IkcLAaYx3RUdNNWsHw4N0TV4g0TjTmY9jdXeETzgkwKWPk4jdU8VRPAJLAHL9BJZz7fAx1UfuyEtGh4B8HcmG6Ry6PjJeM5VuNGBLMYMYxiZSZ9pU8WlTBcgDfRCv0YFEhcxL8DsU2Ei9v2FM0pVCBowGQahijEiK6b3FKceeuzoJO67BmujRq2JRm76zto1uO4O22dV3g1ZxNRZPDkJqVHvMQcSCKlbTDu+CAp+I91KvaUB84aICR3eDhIiAsiMhw+rKIUfCFNuZdHvC6IgAATAAHEgrf4kSJKCGSxCEWXPq0uIygAAIMkyLJaKOsAxgA5GKEpSjKcoUoQmopBopAYBU4hWDwxhgFYx2zrvg+EOPrdWOQdknJGGCXwvqYANxekgFA-MRZwWq8ZrHWgPkKi3c6DGBKO-HgK4HBLAOhOJkhBejwiokOfCbgNDwFftA+UZEKI-28P-RBAAvO4bQqLFFIJqUgZpXiQO8IAqAwCtAaD4KQGMf9SgAKAaIN+-RVQwFIVRDEYCyjCPAVoMRZQUAiLoaVcgiC3ByBQduHKGDZEUKoUI8RABiGAUBDyUGgOo5kVCzQGnUHEIiOBwGv28P0JYpA2h3BoT8cQ5AwBCJkf0JO5FmTMXIHcBQpBIHYIwLguQhtaz4KKIDNoFA4h9jNLIWAfitDaNYFALAISHBhLVvKHi0SNH6i4Yw0QVEsQ2P6EU6hyTUnaJwDgYgsj+HqHuGaNxxgqLt0qWcFppCXHiDcR4rQ3SIHLi9Lk3xXgbJ2RoOIPUugfLZFeO2RKCCkFKLqP9SgdlMHeNHLBUgCd4CYwSOIWRHTPE9O8AgVg4gqIAHYekrkmRRd2wl9EzPEHMhZSyVnRNufcrQ1ixlnEuSM55EzwleGMNEhxTjKJaDoLI+FzjAUgMhTgvJ7ydj6NhYlVFPAzToqRSEkUq55SFHcNE9ZijEV11grAXZsiSXECxsQGMLC2EcJKCU-I6YADUWhW5DjALwsFrEqJoC8WcWpRy0kNKaf0VgNIzR+iVJbHQM4piyJVfINVEAiHwBAaKi5XIzStnkFRM0GJTXjMpe4JA9cVVDmzCPWKVEF6GAXlgnJVLwh8DufqN1NIPVaC9SAH1K5G7O2bsoOgSA6BmhwFgRNhImAEiAA 719 | 720 | [9-conditional-rendering]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgQDCoi4A1lriWVoARuQAHlo88CXieeRafOUC8DVaohAA5qIpHdUQ4pGB1ilpKkP9gVrpKcS5cnUNgs1gfNlNvXkFhs4IrvBQhvPwYz5Wo14Dp+PekxDTEDxtc8RZ08VlUOTwhOXk1Vsu1fkmvxBEdBql+tYEp5rM5XO4eOYQAQ-nC5FxhABGADMSBwdBAAF88LxGsoMAArLgyOQKJTCCBgYzkUjVYCiPB5SriDkmYwErSsChgLSGCTiYyEJBWKzEKA8DCEbIAT2MfGmGCg8A0VlEKvgpF5hgcDmlWgAtBatABBdIAFQAkgB5AByAGVzRbjTxZDxCNVbeQ2m0EAAJdqdcPVXRaAAUfr4Cg5d01JQAlFpdOYtMB+ghqq0Ol0JBmtDQMOX4woxOGi+ImP0CxHujRk5UmCWAISN2stngp+tnUjwcRxUheYBacsK8QJ+Ac7uRgkOJc8Bw+v1aV3wbZR2OVudaFEKKDpzOx4BT-cco+7Ampr3r6oAcR+ACF3CW4zPE1ooAm+KeWYxheFbfgehTuEgv7-hgsiMnwPBKhghSEHeXqmhaZpaAAavaACiADqEyOgAsgACi6eHOra7qYQ+aLVMY+qELM0bGBQEoZrEogxoYtiGByOZnN4xCwHwhCStmRzeExpAsTwUHiKQcRztJLQ1pGUHseQErVoWkZ4GpN5QFpHGEAq27-LshnCVoRJHHIok3Nkpk6eZaKWa4NneESpZHDxhhmEi2a+QoVQxtpunAvAqapt5gQBSAdzGHEfTBUJlx5HqUGGE8OSvAJal5dMuyubpC7dPF4yOVMLmxgA+hyWqKOIgFSbZ4zNfI046WRHF8G0CbwjG94dYEQ4jmOWiRe58iBsGhxjfZtl3vFTCjSaVgelhABKACqzrbV6vLAf0dy9FBGWBNFkoAAU0GphgADLwAhFRaE+BqiAIhVjYYeGkBodxaCG5AAO7brAv2ZYYkyhC1dxNK+fBxE80OXIYZEJqQNwQPUL0oWq2To+MsNZLAzhKloQQQMOONo0iRwsP5GndFBD1jawfAU6pY1KSpVWBFzPOC94wvOKLArcxLTOC8ZUE8AksAckcEHhCKIACZlWimiodFnMt3hA-AYNQfuXEtLx4B8HcWvAL5HPjJeYEYDdGBLMYMYxtFSZ9pUbWyfJp1jT7akVRIZsu+H4i9v2ks0vNCDswGQahqzEi+3HRmecekezhZO67BmujRq2JTx76Ofs1uO6Z22gt3nFRz7shH4AGRt9rXfdz3uv6+MiX8elWiieJkmGGr+yhVyX752rsUcjrW1656ZzrfFPDkJqUHvMQcSCN1bTDnhCAH+Ir5KvaUBW4aIBNzwaE8EiIBwcYHD6sohR8IU27P0e8LohADgHASAMT4iJCSQQZJiASWfj6Wk4hlAAAEGRMhZFoUcsAYwAHIxQSilDKOUFJCCahSBoUgrthxWB4MYMAVgwazgoTAwgSCABMVhyCpROJGWCElsGpgANxeiQBQH47VvAWleGaMG0B8hQVYXQOgxgShCIfl6JYwMJxMkIL0eEUEhxiTcBoeAAiVwOAwIHWYV0pE6IAF53DaFBYopBNSkDNK8VR3gZFQDkVoDQfBSAxkkaUaRsjRCCP6KqGADioJgOUVoOJZRElaBQEolRkTyA6LcHIfR24hrGM8UUZkrjYnxIAMQwCgIeSg0BCnONcWaA06g4iSRwMogR3h+hLFIG0O47ifjiHIGAWJaTCl7zksyLS5A7gKFIKosx8pLHymjuIooRM2gUDiH2M0shYCTK0GU1gUAsDzPMUsguVkqnWOKfqUJPjRBQSxO0-o9Tbm7P2WU4BxBClRPUPcM0gzjBQQABzPLOL8hx-TxCDOGVoUF6S1GrkWcxWYyVUo0HEHqXQxVsivHbFdbRuicl1BQpQVKJj+jjJYqQHe8AuYJHEIUwFIywXeAQKwcQUEADsYKVwWJRfKYyOgaHosxUxbFzxcWlHxbmOlnKtBtIRd4ZlcLeVen5RMrwxhVndN6QpLQdBCm6r6ey+VrC1XIs1RcnYVTtVXWNTwM0pqoKGuXCKJFrdagEsyUS-V38WKwHJYU51I9ubEBjP4wJwSSh3PyOmAA1FoVhQ4wARLOCqtAozKWUA+V8wprAaRmj9EqJOOgZxTHzYWux8B5EpqZVyM0rZ5BQTNBiOty5zFqyQD-AtQ5szwJalBbBhhsGmM7ZBPgHL9T9ppIOrQw6QCjpXH-HOADlB0CQHQM0OAsCbsJEwAkQA 721 | 722 | [10-effecter-with-dispatch]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgQDCoi4A1lriWVoARuQAHlo88CXieeRafOUC8DVaohAA5qIpHdUQ4pGB1ilpKkP9gVrpKcS5cnUNgs1gfNlNvXkFhs4IrvBQhvPwYz5Wo14Dp+PekxDTEDxtc8RZ08VlUOTwhOXk1Vsu1fkmvxBEdBqlQRdLgBleDbNz3ObApqseDiJ6fOo8KBadSEYywPgATy+gKK7i0rHIpHWfGqxngpEIclBJ3BZ2sCU81mcrncPHMIAIf15ci4wgAjAB2JBYEAAXzwvEaygwACsuDI5AolMIIGBjFTqsBRHg8pVxKaTMY5RSKGAtIYJOJjIQkFYrMQoDwMIRsoTjHxphgoPANFZRP6GVbDA4HO6tABaJNaACC6QAKgBJADyADkoYmk7GeLIeIRqunyG02ggABLtToN6q6LQACnLtPgpruIZKAEotLpzFpgP0ENVWh0uhJB1oaBgFx2FGIG9PxEx+pPG90aD3KkxZwBCLdr3dY-f9UiouKkLzALQLn3iTumk9NuUOD88Byl8taGFwrO7bPgoprCgoUADkOc79MAj5Ll2WjgbsCr9DQuIBmioiDsO96XPhBGEfGKhJgmRwolhraOuIzquu66pyPigbwKIlAhqQGDiP6NzkCGGCyGAVhxM4jJWPsADUSEgfAGDQIQNDIVATB9kcnFZDwrZXniopNNBWkGmWMkMRpfYqWc3hqYorZQLS9TQRhtJPK2ADiPwAELuKaNnPqZWjEaR-RyhuPDBT+orVK54gebULbAS+OK2VBw6tnBi7SaahTuEgCXPvx5D6nwPCEhghSEHKZlxlYhYJloABqmYAKIAOoTNmACyAAKeYNbm6YFgF34luFWj0oyswtsYFAujhm5USAtiGKao7mVoxAEoQrojkc3ijUyPDZeIpBxF220tKuTbZZN5AuiuU5Nngp2KZdU2ED6sL-LsD0rahK1yGtNzZM912vTpcJfd4CowStohzWYgojpDChVK2V03Uipng4EMOGHcxhxH08PLfhXH0tlhjoi8pSLadFMrFAQM3W+3SY+Mf1TIDbYAPqmqGijiElW0reMvPyE+10dVNfBtLSfKtmZhFXuIN5eKjIPyFWNaHELWg-eM5WY8pxbxqRWgAEoAKq5tVxZWil-R3L02VE4ESKugABTQp2GAAMvAhUVFozmkHwogCNT2uGA1pAaHcWi1uQADusKwOH+GGJMoR83cTRuXwcRPKnlyGB1tKkDcED1L7pWBtkhfjOnWSwM4hJaEEEComXBeCkcLBHEzEjZZ72usHwTcndrh3HSzgQj2P0-eLPzjzxSo9Lz309PeUCSwKaRyZeEDogHXOhQJtNDiqaABMpoAMymigpo4CwflVSRRZnLrWgx-ACfZQhOEtDmksO4i0EamiHuMeC0kMCuwwEsYwrZWxIm7OefsADdpyDttrZBp1+7iD-tAvBZ5ey921lqDWCBB6VmrHWc6zMdCoNIfhTeCE3pwl2IOXQLY9wlGXqDf4g8AL-BQSQ6e5U+wb2gfvLQAAyGRfc5oLUJqtdam1DD732Ijc0cVlz7wxv0ZSmMeC8XgNld4xA4iCFFm0VEDUEBWKioSTMUA5rRhABIz8ZlBQgAEsYDgDJlCFD4IUWE3jwJ8jFCALASA6DykVCAJEKpiAbW8aWbU4hlAAAE9QGlINUG8sBWwAHInQujdB6L0apCAhhSBoDiFRxBWB4MYQSCdOwcWSYQTJl8rDkHxqybo-ENpFL7AAbmLEgCgPxBbeCTK8BMCdoD5GypfOgdBjAlHGTwL8DhgF3hGuQQgvQ+TZSvASNwGh4CjJ2d6DBd5+jzKOQALzuG0bKxRSDsQTK8LZ3hFlQGWV-PgpBWxzNKAspZogxn9ADDAV52VxTrLKIijZWgUVlBQEi35ByjluDkKc2EMtLnYo+exBFqKADEMBsRMhSFAElVIvnB3UMJbKOANmjO8P0JYpA2h3G+T8cQ+UEVYv6BYsapBLrkDuAoUgWybkYDubdbcM5nZBOmG0CgcQsQJlkLAKk2UKWsCgFgeVDhFUMj2mwj62I1WMoZBCgFohso3w5Q8+1pBdWUANVoClOAcDEGxbC9Q9wExCuMNlAAHG6s4wbXkCposKrQ0bNmfmLBasaXhcb4xoCTeAuhaavAPM7A0uKTl1FKpQfGVyxU3iZJKnE8AR4JHENi8NIqY3eAQKwfBWhJQxq-Bmq1ikGF43ELmyMBbnjZCLTMrQ3be3stTWcdtyaB3pruSNOdPK+X7S0HQbFO7+ULpWeu25lq5DWp2NiYw27gW7oTCe-d8qHSDRKuSEthzjn4orbS6t2Kn3EFHsQVsGhgWgoFSUR1+QBySUvleMA0KV3XWymgUVZw9U+r9QG7FlJ5AJnLISShOhnxTFw1qAjEAnmmK0PB0IbbzQJj3PIbKCZxQIbNd6feSAQmUivCONJfNspFMMEU655ruN8B7QyATWohNaBEyAMTX4wnvRFGWZQdAYkJhwNE2JQU5RAA 723 | 724 | [11-effect]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgQDCoi4A1lriWVoARuQAHlo88CXieeRafOUC8DVaohAA5qIpHdUQ4pGB1ilpKkP9gVrpKcS5cnUNgs1gfNlNvXkFhs4IrvBQhvPwYz5Wo14Dp+PekxDTEDxtc8RZ08VlUOTwhOXk1Vsu1fkmvxBEdBqlQRdLgBleDbNz3ObApqseDiJ6fOo8KBadSEYywPgATy+gKK7i0rHIpHWfGqxngpEIclBJ3BZ2sCU81mcrncPHMIAIf15ci4wgAjAB2JBYEAAXzwvEaygwACsuDI5AolMIIGBjFTqsBRHg8pVxKaTMY5RSKGAtIYJOJjIQkFYrMQoDwMIRsoTjHxphgoPANFZRP6GVbDA4HLIeIRqii0aIAFJMry6LQAClxAZTpvIxjcooAlFpdOYtMB+smntmiyWExg4qRYKX+hhATxs6RPgaE01K1o+3jRfA1Rns6WO2cu1ke1BafVh3nafXG3zCBhA03TUvxHwZw45bGeO6tABaa9aACC6QAKgBJADyADkoVfr2f44mtA-yDaNoEAACXaTpwOqLNs0TWl4FNO4QxKcthxrM4EGqVoOi6CQKy0GgMEI2CFDEcCcPEJh+iwiDuhoRDKiYPCAEJqPIuisQY-o+3EVsvGALRCJ9Q8FFNVjINPHgJLjUVqhhOE8Jg4T4K0YUFCgFCqxofpgEE4jlNU3YFX6GgHTObw6zTDM8CONDLi0VtYCQB0QCdF03SsdU5HxQN4FESgQ1ILt-RucgQwwWQwCsOJnEZKx9gAahUpSMGgQgaAMqAWCObxdz5JyAHEfgAIXcayzK0CTvEonhqukhNqkK8QStqaC9P3ZcNJzHSiKU01CncJyDz4cLyH1PgeEJDBCkIOVZwcC9r0vLQADUnwAUQAdQmF8AFkAAV3zWt8H0-RafxkrR6UZWYs2MCgXQrWJRGzQxbEMU1bMCYgCUIV1q2yy6GQzJzxFIOJ4IBsTuicu6i23KGJDKuyMph+7twypHAiM8q5G+m5slRuGMHHOFMYVfCjmewwzEFatyYUKps1hl0MCRGdMe8KmQDuYw4j6WnPvGcRIycwx0ReUp3oB8WVigQmWYRi0AdxqYCZzAB9U1Q0UcROsFy5tfkISiz2+6+DaWk+WnAHvG43jLrR4n5EA4DDnK7xsfGWbMaYObzysL8loAJQAVTfQOzytbNPruXonP1pFXQAAq093nIAGXgcaKi0fLSD4UQBCltPDDW0gNDuLQQPIAB3WFYGLuzDEmUIdbuJoir4OInkby5DD22lSBuCB6kz6bA2yXvxmbrJYGcQktCCCBUSHnvBSOLLysVpzU7s1g+DniG09B8GOcCffD7P8yD+cK+KRvo-xk38YUfKBJYFNI5+vCZyp50KA-o0HFKaAATKaAAzKaFApocAsC0BeFQZ0ziey0BXeANcnJ6Uei0F64A+B3HenTU0u9Ai6WSonDASxjDZmzEiBCHFkLYKulOfW3g6GQzIpBTByVFbsSQs-S4WoXYIB3gBICoFOHdHofwu+r89I+lhP8XYFZdBZnoiUO+JN-g7zkv8aRDEz6zVLGfeR38tAADJzGU1wW9AWWhvp8F+qLEA399j03NIpOCU13Ds36L7TGPBQrwEGuQYgcRBBGzaKiNaCAIlNUJE+KAuDowgGMSeWcgoQARWMBwBkyhCh8EKLCTJqktzKHFEgOg8pFQgCRCqYgv1Mnxm1OIZQAABPUBpSDVActmAA5K5V07pPTenVCGFIGhAoVHEFYHgxhIo1zgoFBphA2kgKsOQPmrJujhV+n00sABuM8SAKA-H+mca8rxLw12gPkJyIC6B0GMCUI5kkzxLErvxA0hBeh5RHLCS2Gh4AHKkt6ZhsxPpXJ+QALzuG0JyxRSABUvK8V53gblQDuagvgpBsyXNKNc25ohDn9ADDAOFTlxRPLKFS55WhaVlBQNStFl1yA-KbE5PsBI3BApZYigKlK6UAGIYDYiZCkKAfKqTIvzuoaKTkcDPIOd4foSxSBtDuCin44hRqUuZf0MJ11SAw3IHcBQpBXmgowOC70itzneAKdMNoFA4hYkvLIWAVInJCtYFALAlqHDWqBnIBRcJlGQulQyQlmLRBOXAUq-o-Ko0eq9VoIVOAcDEBZWS9Q9xLw6uME5AAHAms4Oa4VavEDqsAxbS1WptToOZfMaDC3pLoGWrxGKfW+b8uQTlCnir5sCg1rYmTGpxPAfeCRxAsoLXq0t3gECsHEE5SUdbA02tDUo7EPNm2tvgO2542RO32q0EuldWhFUvP6HOrQJbr1vJ4Bu4NXhjCnrVRqngTk6Aso-Zq899z11gpfVunY2I32fT-TwS8AGtA-pPKZQNZju1st7V+uo01KBDpZbB4gB9iDZg0DivFWqSjRvyOWRKIC+xgBJWcW9aB9VnBTeO9NmaWWUnkJeRMhIRE6EPFMDjWpuMQGhcErQ1HQizvNJeei8gnKXnFDRgN3pv5ICKZSPs1Zmk6ycn0wwfSQVIYGnwZdDJtNal01ofTIBDMSRKYokUCZlB0EqZeHAWBKnyiYHKIAA 725 | 726 | [12-effect-creator]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgQDCoi4A1lriWVoARuQAHlo88CXieeRafOUC8DVaohAA5qIpHdUQ4pGB1ilpKkP9gVrpKcS5cnUNgs1gfNlNvXkFhs4IrvBQhvPwYz5Wo14Dp+PekxDTEDxtc8RZ08VlUOTwhOXk1Vsu1fkmvxBEdBqlQRdLgBleDbNz3ObApqseDiJ6fOo8KBadSEYywPgATy+gKK7i0rHIpHWfGqxngpEIclBJ3BZ2sCU81mcrncPHMIAIf15ci4wgAjAB2JBYEAAXzwvEaygwACsuDI5AolMIIGBjFTqsBRHg8pVxKaTMY5RSKGAtIYJOJjIQkFYrMQoDwMIRsoTjHxphgoPANFZRP6GVbDA4HO6tABaJNaABiAFEACrpAASWgAUlCAPIAOS0aZT6fSGcTSdjPFkPEI1RRaNEeaZXl0WgAFLiA63TeRjG5RQBKLS6cxaYD9FtPbtDkeNjBxUiwUf9DCAnjd0ifA2NpqTrR7vGi+Bqjvd0cbs5brI7qC0+rHvu0+eLvmEDCBpemp-iHwN4OHKdYNk2WjqnIKaoui1Jdt2q6wJaIo8OOx40HObYdqawBIShS5ykwdbxkmCZaAAglWACSJZQjWCZgaK1QZuQbRtAg2btJ03HVAhTa0vApp3CGJToVOM5nAg1StB0XQSBOWg0BgKkCQoYjcfJ4jEWcsk8d0NAiZUTCKQAhHpWmGVixn9Hu4irl4wBaCpPqAQopoWbxoE8N5DjgdUMJwop3ZqUJWjCgoUDiUp-TAC5oWmhFuwKv0UE8DBrYMt2jriM6rruml+KBvAoiUCGpBbv6NzkCGGCyGAVhxM4jJWPsADU4VuRe0CEDQSVQCwWgAOI-AAQu4o54A4Ol+cxw1jeS-Fdf+z7Rd2cWqctZLhDiz51eQ+p8DwhIYIUhByrecZWAxWgAGrUWmADqEyFgAsgACiWabFhm9FkUxjZ0gyHaKcYFAuhOsSiNlIC2IYuFHMQBKEK605HN49KMnISB5KQcRCejLSabxONg0O36ed0U1nOM-Wk+D379dT4wpTTWhyEjNzZPT5MYOecLM1oCoxWz0OGGYgrTsLChVN2ZMuhgSI3oL3hiyAdzGHEfSS5Jlx5JGOOGOiLylPDhPGysUA8wrlMSCrgQc1M3M9gA+qaoaKOI0W63rWge-IrlDu94N8G0tJ8tehPeHZDlaPL35amxHGHGz3isyzk39Ewl08KRyYAEoAKqlv9PAOFa639HcvQ4z73hIq6AAFNCE4YAAy8BHRUw2kHwogCGbqcOiAaakBodxaNm5AAO6wrAg964YkyhJ7dxNKNfBxE8C+XIY720qQNwQPUHdnYG2Q7+MS9ZLAziEloQQQKih-b4KRwsEctviDjLdD6wfC3wJkPcQeMgF63-oA+23gIHOCgRSABsD372zpuUBIyEHRs0KO4Q2IBL46CgKjGg4pTQACZTQAGZTQoFNDgD+Zx05aHHvAaeONQqQxaDDJYdx4ZS1NL-cY8UuqK0aN+JYxhuzdiRMJayYl2GYyvHXQIUjCZf1YUIr+VlRJ0L1ondiCAf6sT0fALicleLSK0XAlBoUfSwn+LsCcuguxGRKHA-m-wf6BX+OY4y9sLqZzZtYrBtQABkwTP4wzhjrLQSM+AoxwUE-Y0tzQhSEUE5WWd-H1xqvAHG7xiBxEEAHNoqI0wIEKeIUahJqJQBhtGEA-iLoOEFCAeqxgOAMmUIUPghRYTNIil+ZQJCSFIBIfKRUIAkQqmICjZpDZtTiGUAAAT1AaUg1QkLdgAOROhdG6D0XpLwhhSBoCqFRxBWB4MYBq09BIVWmYQRZJCrDkC1qybodUUabNHAAbjrEgCgPw0ZnCTK8BM09oD5BxiQugdBjAlF+T5OsXDHJx3IIQXofIcZ7gJG4DQ8Bvm+W9PI2YPtQXooAF53DaDjYopByoJleAi7w4KoCQsYXwUg3YQWlDBRC0QPz+gBhgFSnG4pYVlDFXCrQkqygoHFUy1F6KlxYthOHPFCraXlVFVKgAxDAbETIUhQA1VSelvd1BNRxjgOF3zvD9CWKQNodwGU-HEAdUV8r+j5KxqQUm5A7gKFIAiwlGBiXei-kC7wXTphtAoHELECZZCwCpDjHVrAoBYGDQ4UNwM5A2LhPY0lpqGS8tZaIHG5CbX9E1SWpNKatA6pwDgYgCqhXqHuAmN1xgcYAA4q1nDbVSl1uV3VaD7fCkCdYc1Yy8BrLWNBxCRl0BbV4JkfYGiVZiuoZ1KBa3xV61cTJfU4ngP-BI4gFVdo9f27wCBWDfy0JKft3lp0dnzXY7Ec7xALqXSu0oa7+h3ofdaidZwr1jufVOsNcdI1aAdU6ngOM6AKvg86oDULINEtzd6fqMGfaoZ4AmdDWhkMgQwdmoJsGN0Yuxtuw1e6FXEeIAA4g3YNAcq5S6kopb8jjg6iQvcYABVgaHDjNAnqzh1uPY25tCrKTyATE2Qk+idCASmHJrUimIDkpyVoAToRL3mgTEZeQOMEzikE1m70QSkA9MpHuacczPY402YYTZBKKPYL4PehkjmtTOa0K5kA7nvJ9NsahMUIA6BIDoAmHAWAYvyiYHKIAA 727 | 728 | [13-init-effect]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgQDCoi4A1lriWVoARuQAHlo88CXieeRafOUC8DVaohAA5qIpHdUQ4pGB1ilpKkP9gVrpKcS5cnUNgs1gfNlNvXkFhs4IrvBQhvPwYz5Wo14Dp+PekxDTEDxtc8RZ08VlUOTwhOXk1Vsu1fkmvxBEdBqlQRdLgBleDbNz3ObApqseDiJ6fOo8KBadSEYywPgATy+gKK7i0rHIpHWfGqxngpEIclBJ3BZ2sCU81mcrncPHMIAIf15ci4wgAjAB2JBYEAAXzwvEaygwACsuDI5AolMIIGBjFTqsBRHg8pVxKaTMY5RSKGAtIYJOJjIQkFYrMQoDwMIRsoTjHxphgoPANFZRP6GVbDA4HO6tABaJNaABiAFEACrpAASWgAUlCAPIAOS0aZT6fSGcTSdjPFkPEI1RRaNEeaZXl0WgAFLiA63TeRjG5RQBKLS6cxaYD9FtPbtDkeNjBxUiwUf9DCAnjd0ifA2NpqTrR7vGi+Bqjvd0cbs5brI7qC0+rHvu0+eLvmEDCBpemp-iHwN4OHKdYNk2WjqnIKaoui1Jdt2q6wJaIo8OOx40HObYdqawBIShS5ykwdbxkmCZaAAglWACSJZQjWCZgaK1QZuQbRtAg2btJ03HVAhTa0vApp3CGJToVOM5nAg1StB0XQSBOWg0BgKkCQoYjcfJ4jEWcsk8d0NAiZUTCKQAhHpWmGVixn9Hu4irl4wBaCpPqAQopoWbxoE8N5DjgdUMJwop3ZqUJWjCgoUDiUp-TAC5oWmhFuwKv0UE8DBrYMt2jriM6rruml+KBvAoiUCGpBbv6NzkCGGCyGAVhxM4jJWPsADU4VuRe0CEDQSVQCwWgAOI-AAQu4o54A4Ol+cxw1jeS-Fdf+z7Rd2cWqctZLhDiz51eQ+p8DwhIYIUhByres2NtUI3iMWjRfEtgkrYBa2Sd48XLf0SKurtgE+lM8DdgwWg4KOGBLMY3ZlMeJQYEik39D1SB-XwAM3EDINgxDJjQxOU5w9AiO6ZpvEo5hfCwM4pqsJT1MUnTYW01TTOMywIGXTwpHJgAatRaYAOoTIWACyAAKJZpsWGb0WRTHXVo9KMrMXbGBQLr4-0ojZSAtiGLhRzEAShC-e9lxKx2KPiKQcRCUc3ied0KNq0O36OxIU1nOM-XO+r379Z74wpV7WhyEbNzZL7rsYOecKB1oCoxSH2uGGYgrTonChVN2LsuvDjQ3vHDs63cxhxH06dm+M4iRijhjoi8pT6-bWgNysUBR3n7sWi3YdTJHPYAPqmqGijiNFVeXKP8iuUOYvq3wbS0ny14t94dkOYrfsx-IbEcYcIfeMHQfE94TCc9z5EAEoAKqlnLPAOFa61IzwvTk0cwA-eTg3dz-iWwn+LsFGPAEjIW2nXEA+sdBQF+jQJgx9AhpQynBHWToXRuisIVAkxASplQZJVYw1Var1Uas1Qg0Dbr3UEIQW8Z944aAgPAAA7ijUK+MWg6yWHcaBwBE40COJ9QS+caE4yht2JEwlrJiQ4RbOQL9D4HCLoEP+nVhHdysqJdmiitR7wQOTVi7FOKk26FIrRyjvA+zUepfqE5dBdiMiUCxodDxwnJoFf4ZjjLKIuqfQIoVTrkgAGRBKOCnXWmheGt2Nr9QwhR3D7EzuaEKXVAnkELv0c+8ceA1XgCjd4xA4iCBnm0VEaYEDFPEKNQk1EoA62jCAYmF0HCChAPVIhCBSDKEKHwQosJWkRS-MoNAdAkBoHlIqEASIVTEBNq0hs2pxDKAAAJ6gNKQaoSFuwAHJ0H5Q9F6S8IYUgaAqhUcQVgeDGAaswwSFVZmEGWQAJisOQcurJuh1RNts0cABuOsSAKA-GnP0JMrwEzMOgPkFGTy6B0GMCUf5Pk6zcMcorcghBeh8hRnuAkbgNDwF+b5b0cjHL9HBZigAXncNoKNiikHKgmV4SLvCQqgNCrQGg+CkG7GC0oEKoWiD+f0AMMAaUo3FPCsokqEVaBlWUFAUqWXosxUuHFsJl4EuVfS8qErZUAGIYDYiZCkKA2qqSMtIHwdQTUUY4ARb87w-QlikDaHcJlPxxAHQlUq-ohTlakGduQO4ChSBIuJRgUlGk5K8RBWcHp0w2gUDiFiBMshYBUhRvq1gUAsDhocJGhkHYfSAJ2NiM2OqGQCvZaIFGABmB15KLVVvTZmrQ+qcA4GIMq0V6h7gJi9cYFGAAORtZxe00o9blb1WhR2IpAnWQtysvCl3LjQGu9JdBt1eCZM2BpVXYrqGdSg5dCV+tXEyQNOJ4C0wSOIZVg6fVju8AgVg4gUaSjHd5JdxbbGrvEOuyMW7njZB3XGl9N732g2fTUIds6v2LtJYrcDWgXVup4CjOgyq0PutfVBp5CGSVFrkCWuEuxkNmxwzwBMeHMPhodI-b08Tah7oxViuQKNekmtPcq2jrdKbEG7FynlfKSjVvyOODqTy9xgGFWcR9WgRkwdbVejtXblWUnkAmJshJ9E6EAlMDTWptMQEpXkrQ0nQgPvNAmIy8gUYJnFDJ-NTH3BID6ZSPc04FljxRtsww2yiUFuY0gPgb6GTea1L5rQ-mQCBe8gM0tS4xQgFGXQBMOAsBIDoPKBBQA 729 | 730 | [14-subscriptions]: https://flems.io/#0=N4IgtglgJlA2CmIBcAWAbAOgJwFYA0IAzgMYBOA9rLMgNoAMedAugQGYQKG2gB2AhmERIQGABYAXMNQLFyPcfHnIQAHigQAbgAJoAXgA6IPgAdjhgHwqA9Oo3n9PFYXjFxEOfZ5atK0Sk-e3gAi8GDkSA7e1n4BPgCusLFRsBBJgQDCoi4A1lriWVoARuQAHlo88CXieeRafOUC8DVaohAA5qIpHdUQ4pGB1ilpKkP9gVrpKcS5cnUNgs1gfNlNvXkFhs4IrvBQhvPwYz5Wo14Dp+PekxDTEDxtc8RZ08VlUOTwhOXk1Vsu1fkmvxBEdBqlQRdLgBleDbNz3ObApqseDiJ6fOo8KBadSEYywPgATy+gKK7i0rHIpHWfGqxngpEIclBJ3BZ2SbMuWgAqsYbOQAO5ePikCgCrQrYlaMIaJp-VzuHgsyHWBKeazOBUeEAEeVuORcYQARiwSAAzFgQABfPC8RrKDAAKy4MjkCiUwggYGMVOqwFEeDylXEgZMxitFIoYC0hgk4mMhCQVisxCgPAwhGyhOMfGmGCg8A0VlE2YZYcMDgcya0AFo61oANIAUQAmkEAPIAdQAcloodyAEJQ9IAJQAkgAFAAqY-bvbrNcrPFkPEI1Ul7yFULihRIpAghQZWl0WgAFLic2iA1pyMZ9auAJTH8xaYD9Fdrlp8LEIaknwvPq+RwQKwZ6FhgkpaAAhLoJ63vehAQfAhJPqQqJxKQSrsjiEB4rSTynvBiqIbm94Pv0Vr9HwMBNrK8gADK4e6DKnoYG6CkqOpfj+DLkWcaHiBhXink+ugvmhMrwLRijiIxa6KCxbHIZunGBqI35wLxDiUVhH7VHIDbIUEHHHmekqhlqPCiS+NDsVuO57geDKBsA5l1JZVpMEu1YLloABiTZTukAASWgAFJQnOWhNn5AXpFOtZ1kuekUqiTxhUyXgnueuGXk8gZEQa1lAWcKJXoRd7ERgGGwHx3gYICPCnmheIGk0YlaC1PqrvATqZSJdVaA1WRNVAtL1B1F74aIFUIRgpGKoGY3iHwD58TpDgpc6ch+WlWR-meNUWWRgE0GV6WZS5R3ufenneVYiU1loACC8Wzt2UKPclBrVFO5BtG0CDBe0nQg9U2VrrS8CBncBYlMVb5nAg1StB0XQSKZNAYNjkMKGIIPo+IXlnKjoPdDQsOVEwplQaThMU1iVP9AJQmvkNOMrQoakE2DOkbcuP19rC-ymaeuPQ1oeq7MVND9MA2MZpzEtS1ANr9NtPC7VeikgHGCZJlYGv4rm8CiJQBakA12Y3OQBYYLIYBWHEziMlY+wANSS0rGDQIQNAqywWgAOI-AO7gPngDjE5tgsh+IYe1BDStLeNxWnvLHNQ4GhTuEgOLjfb5Det+hIYLuVp8THq7VHH3aNF8SdZ-nK1p4j9WZ1z-RIomzd8BmUzwKeDBaDgD4YEsxinmUHUlBgSIR-0vt58tfeEAPQ+BqP48mFPgGz9AC8kzz3R52dfCwM4gasOfl8UjfEvXxfD-3yw2mVwL1dC3CvKmeLgFtzoUCYtvYq2PLBcoCRaqdXQphL2UNmYwK8DQGEcJAziwzMLHY2InpGmJvzFKKD-jGSFL-JW-9F5APQaA2CJ4eCQNQoguBCgEGCVgcgzBIYmG9VAZ7XB2l7qPS0AANTHE2TsEx2wAFkJxzibN2Kcn0FzfU-vSRkswTzGAoAmZ8-QZqGFsIYFyRxiAEkID3AB4xVGZTzuIUgcRoZHG8HTMGedNG3kQs47okdsLeBVq4rRiEVbePGGrbCcgTE3GyP49xGA2qoIooGWW2E9EgDMFxYANogxVFPG4hMc9GhrWCYEFJdxjBxD6OkxxeRSx50MOiF4pRDFVPqSsKA0S8meIkEU8Y4SphRLPAAfUDIWGSCMqneBGfIRWt4JxaL4G0WkioRLjOgawrwuTEJun+oDQ4PitChJCYfbwTB34+XrCObk84kpYTDOnRePBeinyOMAbup9A6dPEG8tBHDdh5zoVQbOucYwgEMToKAPcaBMAOYEDWWt0SkFYrrcQ8ZEzJiNgSYgptzYMitsYG2dsHZOxdoQUFtd66DVfmcDQEB4ACjzn-DqKSlh3FBRkxJRwFboO7tvSep4kQw0ZvDQCVi5B3L2fyqpHz6Xew+QzOGlKuRbIBggU+f1lXwGBmjMGAr5XdMCH4rhGC4S7DASeSmJQ9XeDif8U+hDXA6qpnqiuRz9XexzrUAAZB6o4KSDHpK0CYvgZjakgHdfsTJChsnoPdYU-oJyik8FtvAZe5BiBxEEFMtoqImwIAzfHQkY4oCIvLCAF1hAHJkAgJVA00qoanSXOMKhPzsQwVoZArQXqjhNuNdiF8dAO3erCTwQyhJiFNUMM9UUgpeSgrteIXkh8u0gObdBcB-zYADqXVDI1-wTUqDPOg32GAED3HyLWLQRonydqHSOsdiLJ1ijHbOjhY6XWUorg4HUIAHZ4t-MoQofBDzSCIBw4iygsAAA4kBGiNNaW0IAkQOmIGYr9K53TiGUAAAS9D6Ug1QaqngAOR61RSmNMfUCwpA0JbCo4grA8GMI7AUUNLbIcIJhgATFYcg5TWTdHtmYwjD4ADcS4kAUB+CVbwdZXg1gFNAfIecON0DoMYEoomeD82ZV4YAWgfSEF6IqPOaECRuFlMJ-mGARU6f6LJgzAAvO4bQ87FFIBbGsrwNPeHk1ARTWgNAilPDJ0ocmFOiBE-0HMMAnN5yNKpsocW1MXvi1oFA8WvN6fIAZ+8xnYSLPM7ZqkFtYtJYAMQwGxEyFIUAMuufc6QaiEBnZ5xwGp4T3h+hLFIG0O4HmfjiCLrF9L74MJMlIK48gdwFCkA05Z6z+MtXdCk0UXM2Q2gUDiFiGsshYBUjzqV1gUAsCzYcFZhkmUd1YOW3VhkoXfOiDzmaNrhW3O3Z23trQpWcA4GIBlqL6h7g1gG8YPOEHntnH+05vryLBtaDB+p-hWEztqK8KU8pNBxCll0C0141MAH6cM3IPOgGqvlPgBltNajxs4ngNfBI4gMvA6G+D7wCBWCfK0AAdnBzpZHF3QFo-EBjrHOPSh4-6GzjnrWEdnCZ3DnnS4+ezGMMtrrPWeB5zoBltXvXJdKYV+meboCVcAJ1zwGseutBa+0jGJH7rlsE5y3UXclAycZct8Qc+xBTwBYRcFkod38hPk9hxiSEXZe3jzmgYbZx3vU6+z9jLlJ5A1jXISFVOgVpTCT26VPEB7PJq0KH0IjPgw1kpvIPONYjQSRO+md1SBDyUjQq+NDMk86EcMIRizp2G98HZ0eYAbfK9aE7yAbvOkv16jA8IOgSA6A1hwKaOg1ooVAA 731 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Minimum TypeScript Version: 4.2 2 | 3 | // This requires every property of an object or none at all. 4 | type AllOrNothing = T | { [K in keyof T]?: never } 5 | 6 | // This ensures at least one property in an object is present. 7 | type AtLeastOne = { [K in keyof T]: Pick }[keyof T] 8 | // Credit: https://stackoverflow.com/a/59987826/1935675 9 | 10 | // This ensures at least one object property group is present. 11 | type AtLeastSomething = U | AtLeastOne & AllOrNothing 12 | 13 | // Most event typings are provided by TypeScript itself. 14 | type EventsMap = 15 | & { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] } 16 | & { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] } 17 | & { onsearch: Event } 18 | 19 | // Indexable values are able to use subscripting. 20 | type Indexable = string | unknown[] | Record 21 | 22 | // This validates plain objects while invalidating array objects and string 23 | // objects by disallowing numerical indexing. 24 | type IndexableByKey = Record 25 | 26 | // Empty strings can cause issues in certain places. 27 | type NonEmptyString = T extends "" ? never : T 28 | 29 | // ----------------------------------------------------------------------------- 30 | 31 | declare module "hyperapp" { 32 | // `app()` initiates a Hyperapp instance. Only `app()`'s `node:` property and 33 | // effecters and subscribers are allowed to have side effects. 34 | function app(props: App): Dispatch 35 | 36 | // `h()` builds a virtual DOM node. 37 | function h( 38 | tag: NonEmptyString, 39 | props: CustomPayloads & Props, 40 | children?: MaybeVNode | readonly MaybeVNode[] 41 | ): ElementVNode 42 | 43 | // `memo()` stores a view along with any given data for it. 44 | function memo( 45 | view: (data: D) => VNode, 46 | data: D 47 | ): VNode 48 | 49 | // `text()` creates a virtual DOM node representing plain text. 50 | function text( 51 | // Values, aside from symbols and functions, can be handled. 52 | value: T extends symbol | ((..._: unknown[]) => unknown) ? never : T 53 | ): TextVNode 54 | 55 | // --------------------------------------------------------------------------- 56 | 57 | // This lets you make a variant of `h()` which is aware of your Hyperapp 58 | // instance's state. The `_ extends never` ensures that any state-aware 59 | // `h()` doesn't have an explicit state type that contradicts the 60 | // state type it actually uses. 61 | interface TypedH { 62 | <_ extends never, C = unknown, T extends string = string>( 63 | tag: NonEmptyString, 64 | props: CustomPayloads & Props, 65 | children?: MaybeVNode | readonly MaybeVNode[] 66 | ): ElementVNode 67 | } 68 | 69 | // --------------------------------------------------------------------------- 70 | 71 | // An action transforms existing state and/or wraps another action. 72 | type Action = (state: S, payload: P) => Dispatchable 73 | 74 | // A Hyperapp instance typically has an initial state and a top-level view 75 | // mounted over an available DOM element. 76 | type App = 77 | Readonly 80 | 81 | // The subscriptions function manages a set of subscriptions. 82 | subscriptions: (state: S) => 83 | readonly (boolean | undefined | Subscription)[] 84 | 85 | // Dispatching can be augmented to do custom processing. 86 | dispatch: (dispatch: Dispatch) => Dispatch 87 | }, { 88 | // The top-level view can build a virtual DOM node depending on the state. 89 | view: (state: S) => VNode 90 | 91 | // The mount node is where a Hyperapp instance will get placed. 92 | node: Node 93 | }>> 94 | 95 | // The `class` property represents an HTML class attribute string. 96 | type ClassProp = 97 | | boolean 98 | | string 99 | | undefined 100 | | Record 101 | | ClassProp[] 102 | 103 | // This lets event-handling actions properly accept custom payloads. 104 | type CustomPayloads = { 105 | [K in keyof T]?: 106 | K extends "style" 107 | ? StyleProp 108 | : T[K] extends [action: Action, payload: unknown] 109 | ? readonly [action: Action, payload: P] 110 | : T[K] 111 | } 112 | 113 | // Dispatching will cause state transitions. 114 | type Dispatch = (dispatchable: Dispatchable, payload?: unknown) => void 115 | 116 | // A dispatchable entity is used to cause a state transition. 117 | type Dispatchable = 118 | | S 119 | | [state: S, ...effects: MaybeEffect[]] 120 | | Action 121 | | readonly [action: Action, payload: P] 122 | 123 | // An effecter is the function that runs an effect. 124 | type Effecter = ( 125 | dispatch: Dispatch, 126 | payload: P 127 | ) => void | Promise 128 | 129 | // An effect is where side effects and any additional dispatching may occur. 130 | type Effect = 131 | | Effecter 132 | | readonly [effecter: Effecter, payload: P] 133 | 134 | 135 | // Effects can be declared conditionally. 136 | type MaybeEffect = null | undefined | boolean | "" | 0 | Effect 137 | 138 | 139 | // Event handlers are implemented using actions. 140 | type EventActions = { 141 | [K in keyof EventsMap]: 142 | | Action 143 | | readonly [action: Action, payload: unknown] 144 | } 145 | 146 | // In certain places a virtual DOM node can be made optional. 147 | type MaybeVNode = boolean | null | undefined | VNode 148 | 149 | // Virtual DOM properties will often correspond to HTML attributes. 150 | type Props = 151 | Readonly< 152 | Partial< 153 | Omit & 158 | ElementCreationOptions & 159 | EventActions 160 | > & 161 | { 162 | [_: string]: unknown 163 | class?: ClassProp 164 | key?: VNode["key"] 165 | style?: StyleProp 166 | 167 | // By disallowing `_VNode` we ensure values having the `VNode` type are 168 | // not mistaken for also having the `Props` type. 169 | _VNode?: never 170 | } 171 | > 172 | 173 | // The `style` property represents inline CSS. This relies on TypeScript's CSS 174 | // property definitions. Custom properties aren't covered as well as any newer 175 | // properties yet to be recognized by TypeScript. The only way to accommodate 176 | // them is to relax the adherence to TypeScript's CSS property definitions. 177 | // It's a poor trade-off given the likelihood of using such properties. 178 | // However, you can use type casting if you want to use them. 179 | type StyleProp = IndexableByKey & { 180 | [K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null 181 | } 182 | 183 | // A subscription reacts to external activity. 184 | type Subscription = readonly [ 185 | subscriber: (dispatch: Dispatch, payload: P) => Unsubscribe, 186 | payload: P 187 | ] 188 | 189 | // An unsubscribe function cleans up a canceled subscription. 190 | type Unsubscribe = () => void 191 | 192 | // A virtual DOM node (a.k.a. VNode) represents an actual DOM element. 193 | type ElementVNode = { 194 | readonly props: Props 195 | readonly children: readonly MaybeVNode[] 196 | node: null | undefined | Node 197 | 198 | // Hyperapp takes care of using native Web platform event handlers for us. 199 | events?: 200 | Record< 201 | string, 202 | Action | readonly [action: Action, payload: unknown] 203 | > 204 | 205 | // A key can uniquely associate a VNode with a certain DOM element. 206 | readonly key: string | null | undefined 207 | 208 | // A VNode's tag is either an element name or a memoized view function. 209 | readonly tag: string | ((data: Indexable) => VNode) 210 | 211 | // If the VNode's tag is a function then this data will get passed to it. 212 | memo?: Indexable 213 | 214 | // VNode types are based on actual DOM node types: 215 | // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType 216 | readonly type: 1 217 | 218 | // `_VNode` is a phantom guard property which gives us a way to tell `VNode` 219 | // objects apart from `Props` objects. Since we don't expect users to make 220 | // their own VNodes manually, we can take advantage of this trick which 221 | // is unique to TypeScript type definitions for JavaScript code. 222 | _VNode: true 223 | } 224 | 225 | // Certain VNodes specifically represent Text nodes and don't rely on state. 226 | type TextVNode = { 227 | readonly props: {} 228 | readonly children: [] 229 | node: null | undefined | Node 230 | readonly key: undefined 231 | readonly tag: string 232 | readonly type: 3 233 | _VNode: true 234 | } 235 | 236 | // VNodes may represent either Text or Element nodes. 237 | type VNode = ElementVNode | TextVNode 238 | } 239 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var SSR_NODE = 1 2 | var TEXT_NODE = 3 3 | var EMPTY_OBJ = {} 4 | var EMPTY_ARR = [] 5 | var SVG_NS = "http://www.w3.org/2000/svg" 6 | 7 | var id = (a) => a 8 | var map = EMPTY_ARR.map 9 | var isArray = Array.isArray 10 | 11 | var createClass = (obj) => { 12 | var out = "" 13 | 14 | if (typeof obj === "string") return obj 15 | 16 | if (isArray(obj)) { 17 | for (var k = 0, tmp; k < obj.length; k++) { 18 | if ((tmp = createClass(obj[k]))) { 19 | out += (out && " ") + tmp 20 | } 21 | } 22 | } else { 23 | for (var k in obj) { 24 | if (obj[k]) out += (out && " ") + k 25 | } 26 | } 27 | 28 | return out 29 | } 30 | 31 | var shouldRestart = (a, b) => { 32 | for (var k in { ...a, ...b }) { 33 | if (typeof (isArray(a[k]) ? a[k][0] : a[k]) === "function") { 34 | b[k] = a[k] 35 | } else if (a[k] !== b[k]) return true 36 | } 37 | } 38 | 39 | var patchSubs = (oldSubs, newSubs = EMPTY_ARR, dispatch) => { 40 | for ( 41 | var subs = [], i = 0, oldSub, newSub; 42 | i < oldSubs.length || i < newSubs.length; 43 | i++ 44 | ) { 45 | oldSub = oldSubs[i] 46 | newSub = newSubs[i] 47 | 48 | subs.push( 49 | newSub && newSub !== true 50 | ? !oldSub || 51 | newSub[0] !== oldSub[0] || 52 | shouldRestart(newSub[1], oldSub[1]) 53 | ? [ 54 | newSub[0], 55 | newSub[1], 56 | (oldSub && oldSub[2](), newSub[0](dispatch, newSub[1])), 57 | ] 58 | : oldSub 59 | : oldSub && oldSub[2]() 60 | ) 61 | } 62 | return subs 63 | } 64 | 65 | var getKey = (vdom) => (vdom == null ? vdom : vdom.key) 66 | 67 | var patchProperty = (node, key, oldValue, newValue, listener, isSvg) => { 68 | if (key === "style") { 69 | for (var k in { ...oldValue, ...newValue }) { 70 | oldValue = newValue == null || newValue[k] == null ? "" : newValue[k] 71 | if (k[0] === "-") { 72 | node[key].setProperty(k, oldValue) 73 | } else { 74 | node[key][k] = oldValue 75 | } 76 | } 77 | } else if (key[0] === "o" && key[1] === "n") { 78 | if ( 79 | !((node.events || (node.events = {}))[(key = key.slice(2))] = newValue) 80 | ) { 81 | node.removeEventListener(key, listener) 82 | } else if (!oldValue) { 83 | node.addEventListener(key, listener) 84 | } 85 | } else if (!isSvg && key !== "list" && key !== "form" && key in node) { 86 | node[key] = newValue == null ? "" : newValue 87 | } else if (newValue == null || newValue === false) { 88 | node.removeAttribute(key) 89 | } else { 90 | node.setAttribute(key, newValue) 91 | } 92 | } 93 | 94 | var createNode = (vdom, listener, isSvg) => { 95 | var props = vdom.props 96 | var node = 97 | vdom.type === TEXT_NODE 98 | ? document.createTextNode(vdom.tag) 99 | : (isSvg = isSvg || vdom.tag === "svg") 100 | ? document.createElementNS(SVG_NS, vdom.tag, props.is && props) 101 | : document.createElement(vdom.tag, props.is && props) 102 | 103 | for (var k in props) { 104 | patchProperty(node, k, null, props[k], listener, isSvg) 105 | } 106 | 107 | for (var i = 0; i < vdom.children.length; i++) { 108 | node.appendChild( 109 | createNode( 110 | (vdom.children[i] = maybeVNode(vdom.children[i])), 111 | listener, 112 | isSvg 113 | ) 114 | ) 115 | } 116 | 117 | return (vdom.node = node) 118 | } 119 | 120 | var patch = (parent, node, oldVNode, newVNode, listener, isSvg) => { 121 | if (oldVNode === newVNode) { 122 | } else if ( 123 | oldVNode != null && 124 | oldVNode.type === TEXT_NODE && 125 | newVNode.type === TEXT_NODE 126 | ) { 127 | if (oldVNode.tag !== newVNode.tag) node.nodeValue = newVNode.tag 128 | } else if (oldVNode == null || oldVNode.tag !== newVNode.tag) { 129 | node = parent.insertBefore( 130 | createNode((newVNode = maybeVNode(newVNode)), listener, isSvg), 131 | node 132 | ) 133 | if (oldVNode != null) { 134 | parent.removeChild(oldVNode.node) 135 | } 136 | } else { 137 | var tmpVKid 138 | var oldVKid 139 | 140 | var oldKey 141 | var newKey 142 | 143 | var oldProps = oldVNode.props 144 | var newProps = newVNode.props 145 | 146 | var oldVKids = oldVNode.children 147 | var newVKids = newVNode.children 148 | 149 | var oldHead = 0 150 | var newHead = 0 151 | var oldTail = oldVKids.length - 1 152 | var newTail = newVKids.length - 1 153 | 154 | isSvg = isSvg || newVNode.tag === "svg" 155 | 156 | for (var i in { ...oldProps, ...newProps }) { 157 | if ( 158 | (i === "value" || i === "selected" || i === "checked" 159 | ? node[i] 160 | : oldProps[i]) !== newProps[i] 161 | ) { 162 | patchProperty(node, i, oldProps[i], newProps[i], listener, isSvg) 163 | } 164 | } 165 | 166 | while (newHead <= newTail && oldHead <= oldTail) { 167 | if ( 168 | (oldKey = getKey(oldVKids[oldHead])) == null || 169 | oldKey !== getKey(newVKids[newHead]) 170 | ) { 171 | break 172 | } 173 | 174 | patch( 175 | node, 176 | oldVKids[oldHead].node, 177 | oldVKids[oldHead], 178 | (newVKids[newHead] = maybeVNode( 179 | newVKids[newHead++], 180 | oldVKids[oldHead++] 181 | )), 182 | listener, 183 | isSvg 184 | ) 185 | } 186 | 187 | while (newHead <= newTail && oldHead <= oldTail) { 188 | if ( 189 | (oldKey = getKey(oldVKids[oldTail])) == null || 190 | oldKey !== getKey(newVKids[newTail]) 191 | ) { 192 | break 193 | } 194 | 195 | patch( 196 | node, 197 | oldVKids[oldTail].node, 198 | oldVKids[oldTail], 199 | (newVKids[newTail] = maybeVNode( 200 | newVKids[newTail--], 201 | oldVKids[oldTail--] 202 | )), 203 | listener, 204 | isSvg 205 | ) 206 | } 207 | 208 | if (oldHead > oldTail) { 209 | while (newHead <= newTail) { 210 | node.insertBefore( 211 | createNode( 212 | (newVKids[newHead] = maybeVNode(newVKids[newHead++])), 213 | listener, 214 | isSvg 215 | ), 216 | (oldVKid = oldVKids[oldHead]) && oldVKid.node 217 | ) 218 | } 219 | } else if (newHead > newTail) { 220 | while (oldHead <= oldTail) { 221 | node.removeChild(oldVKids[oldHead++].node) 222 | } 223 | } else { 224 | for (var keyed = {}, newKeyed = {}, i = oldHead; i <= oldTail; i++) { 225 | if ((oldKey = oldVKids[i].key) != null) { 226 | keyed[oldKey] = oldVKids[i] 227 | } 228 | } 229 | 230 | while (newHead <= newTail) { 231 | oldKey = getKey((oldVKid = oldVKids[oldHead])) 232 | newKey = getKey( 233 | (newVKids[newHead] = maybeVNode(newVKids[newHead], oldVKid)) 234 | ) 235 | 236 | if ( 237 | newKeyed[oldKey] || 238 | (newKey != null && newKey === getKey(oldVKids[oldHead + 1])) 239 | ) { 240 | if (oldKey == null) { 241 | node.removeChild(oldVKid.node) 242 | } 243 | oldHead++ 244 | continue 245 | } 246 | 247 | if (newKey == null || oldVNode.type === SSR_NODE) { 248 | if (oldKey == null) { 249 | patch( 250 | node, 251 | oldVKid && oldVKid.node, 252 | oldVKid, 253 | newVKids[newHead], 254 | listener, 255 | isSvg 256 | ) 257 | newHead++ 258 | } 259 | oldHead++ 260 | } else { 261 | if (oldKey === newKey) { 262 | patch( 263 | node, 264 | oldVKid.node, 265 | oldVKid, 266 | newVKids[newHead], 267 | listener, 268 | isSvg 269 | ) 270 | newKeyed[newKey] = true 271 | oldHead++ 272 | } else { 273 | if ((tmpVKid = keyed[newKey]) != null) { 274 | patch( 275 | node, 276 | node.insertBefore(tmpVKid.node, oldVKid && oldVKid.node), 277 | tmpVKid, 278 | newVKids[newHead], 279 | listener, 280 | isSvg 281 | ) 282 | newKeyed[newKey] = true 283 | } else { 284 | patch( 285 | node, 286 | oldVKid && oldVKid.node, 287 | null, 288 | newVKids[newHead], 289 | listener, 290 | isSvg 291 | ) 292 | } 293 | } 294 | newHead++ 295 | } 296 | } 297 | 298 | while (oldHead <= oldTail) { 299 | if (getKey((oldVKid = oldVKids[oldHead++])) == null) { 300 | node.removeChild(oldVKid.node) 301 | } 302 | } 303 | 304 | for (var i in keyed) { 305 | if (newKeyed[i] == null) { 306 | node.removeChild(keyed[i].node) 307 | } 308 | } 309 | } 310 | } 311 | 312 | return (newVNode.node = node) 313 | } 314 | 315 | var propsChanged = (a, b) => { 316 | for (var k in a) if (a[k] !== b[k]) return true 317 | for (var k in b) if (a[k] !== b[k]) return true 318 | } 319 | 320 | var maybeVNode = (newVNode, oldVNode) => 321 | newVNode !== true && newVNode !== false && newVNode 322 | ? typeof newVNode.tag === "function" 323 | ? ((!oldVNode || 324 | oldVNode.memo == null || 325 | propsChanged(oldVNode.memo, newVNode.memo)) && 326 | ((oldVNode = newVNode.tag(newVNode.memo)).memo = newVNode.memo), 327 | oldVNode) 328 | : newVNode 329 | : text("") 330 | 331 | var recycleNode = (node) => 332 | node.nodeType === TEXT_NODE 333 | ? text(node.nodeValue, node) 334 | : createVNode( 335 | node.nodeName.toLowerCase(), 336 | EMPTY_OBJ, 337 | map.call(node.childNodes, recycleNode), 338 | SSR_NODE, 339 | node 340 | ) 341 | 342 | var createVNode = (tag, { key, ...props }, children, type, node) => ({ 343 | tag, 344 | props, 345 | key, 346 | children, 347 | type, 348 | node, 349 | }) 350 | 351 | export var memo = (tag, memo) => ({ tag, memo }) 352 | 353 | export var text = (value, node) => 354 | createVNode(value, EMPTY_OBJ, EMPTY_ARR, TEXT_NODE, node) 355 | 356 | export var h = (tag, { class: c, ...props }, children = EMPTY_ARR) => 357 | createVNode( 358 | tag, 359 | { ...props, ...(c ? { class: createClass(c) } : EMPTY_OBJ) }, 360 | isArray(children) ? children : [children] 361 | ) 362 | 363 | export var app = ({ 364 | node, 365 | view, 366 | subscriptions, 367 | dispatch = id, 368 | init = EMPTY_OBJ, 369 | }) => { 370 | var vdom = node && recycleNode(node) 371 | var subs = [] 372 | var state 373 | var busy 374 | 375 | var update = (newState) => { 376 | if (state !== newState) { 377 | if ((state = newState) == null) dispatch = subscriptions = render = id 378 | if (subscriptions) subs = patchSubs(subs, subscriptions(state), dispatch) 379 | if (view && !busy) requestAnimationFrame(render, (busy = true)) 380 | } 381 | } 382 | 383 | var render = () => 384 | (node = patch( 385 | node.parentNode, 386 | node, 387 | vdom, 388 | (vdom = view(state)), 389 | listener, 390 | (busy = false) 391 | )) 392 | 393 | var listener = function (event) { 394 | dispatch(this.events[event.type], event) 395 | } 396 | 397 | return ( 398 | (dispatch = dispatch((action, props) => 399 | typeof action === "function" 400 | ? dispatch(action(state, props)) 401 | : isArray(action) 402 | ? typeof action[0] === "function" 403 | ? dispatch(action[0], action[1]) 404 | : action 405 | .slice(1) 406 | .map( 407 | (fx) => fx && fx !== true && (fx[0] || fx)(dispatch, fx[1]), 408 | update(action[0]) 409 | ) 410 | : update(action) 411 | ))(init), 412 | dispatch 413 | ) 414 | } 415 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperapp", 3 | "version": "2.0.22", 4 | "type": "module", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "description": "The tiny framework for building hypertext applications.", 8 | "repository": "jorgebucaran/hyperapp", 9 | "author": "Jorge Bucaran", 10 | "license": "MIT", 11 | "files": [ 12 | "*.[tj]s" 13 | ], 14 | "keywords": [ 15 | "framework", 16 | "hyperapp", 17 | "frontend", 18 | "vdom", 19 | "web", 20 | "app", 21 | "ui" 22 | ], 23 | "scripts": { 24 | "test": "c8 twist tests/*.js", 25 | "info": "node --print \"('$pkg' ? '@$npm_package_name/$pkg@' : '') + require('./${pkg:+packages/$pkg/}package').version\"", 26 | "deploy": "npm test && git commit --all --message $tag && git tag --sign $tag --message $tag && git push && git push --tags", 27 | "release": "tag=$(npm run --silent info) npm run deploy && cd ./${pkg:+packages/$pkg} && npm publish --access public" 28 | }, 29 | "devDependencies": { 30 | "twist": "*", 31 | "c8": "*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/dom/README.md: -------------------------------------------------------------------------------- 1 | # @hyperapp/dom 2 | 3 | > Inspect the DOM, focus and blur. 4 | 5 | ## Installation 6 | 7 | ```console 8 | npm install @hyperapp/dom 9 | ``` 10 | 11 | ```js 12 | import { focus, blur } from "@hyperapp/dom" 13 | ``` 14 | 15 | Or without a build step—import it right in your browser. 16 | 17 | ```html 18 | 21 | ``` 22 | 23 | ## License 24 | 25 | [MIT](../../LICENSE.md) 26 | -------------------------------------------------------------------------------- /packages/dom/index.js: -------------------------------------------------------------------------------- 1 | const justFocus = (_, { id, ...props }) => 2 | requestAnimationFrame(() => document.getElementById(id).focus(props)) 3 | 4 | const justBlur = (_, id) => document.getElementById(id).blur() 5 | 6 | export const focus = (id, { preventScroll } = {}) => [ 7 | justFocus, 8 | { id, preventScroll }, 9 | ] 10 | export const blur = (id) => [justBlur, id] 11 | -------------------------------------------------------------------------------- /packages/dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hyperapp/dom", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "index.js", 6 | "description": "Inspect the DOM, focus and blur.", 7 | "repository": "https://github.com/jorgebucaran/hyperapp/tree/main/packages/dom", 8 | "license": "MIT", 9 | "keywords": [ 10 | "hyperapp", 11 | "focus", 12 | "blur", 13 | "dom" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/events/README.md: -------------------------------------------------------------------------------- 1 | # @hyperapp/events 2 | 3 | > Subscribe to mouse, keyboard, window, and frame events. 4 | 5 | ## Installation 6 | 7 | ```console 8 | npm install @hyperapp/events 9 | ``` 10 | 11 | ## License 12 | 13 | [MIT](../../LICENSE.md) 14 | -------------------------------------------------------------------------------- /packages/events/index.js: -------------------------------------------------------------------------------- 1 | const listeners = {} 2 | 3 | const fx = (subscriber) => (action) => [subscriber, action] 4 | 5 | const globalListener = (event) => { 6 | for (const [dispatch, actions] of listeners[event.type]) 7 | for (const action of actions) dispatch(action, event) 8 | } 9 | 10 | const on = (type) => 11 | fx((dispatch, action) => { 12 | if (!listeners[type]) { 13 | listeners[type] = new Map() 14 | addEventListener(type, globalListener) 15 | } 16 | 17 | listeners[type].set( 18 | dispatch, 19 | (listeners[type].get(dispatch) || []).concat(action) 20 | ) 21 | 22 | return () => { 23 | const actions = listeners[type].get(dispatch).filter((a) => a !== action) 24 | 25 | listeners[type].set(dispatch, actions) 26 | 27 | if ( 28 | actions.length === 0 && 29 | listeners[type].delete(dispatch) && 30 | listeners[type].size === 0 31 | ) { 32 | delete listeners[type] 33 | removeEventListener(type, globalListener) 34 | } 35 | } 36 | }) 37 | 38 | export const onMouseMove = on("mousemove") 39 | export const onMouseDown = on("mousedown") 40 | export const onMouseUp = on("mouseup") 41 | export const onKeyDown = on("keydown") 42 | export const onKeyUp = on("keyup") 43 | export const onClick = on("click") 44 | export const onFocus = on("focus") 45 | export const onBlur = on("blur") 46 | -------------------------------------------------------------------------------- /packages/events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hyperapp/events", 3 | "version": "2.0.0", 4 | "type": "module", 5 | "main": "index.js", 6 | "description": "Subscribe to mouse, keyboard, window, and frame events.", 7 | "repository": "https://github.com/jorgebucaran/hyperapp/tree/main/packages/events", 8 | "license": "MIT", 9 | "keywords": [ 10 | "hyperapp", 11 | "events", 12 | "keyboard", 13 | "window", 14 | "mouse" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/html/README.md: -------------------------------------------------------------------------------- 1 | # @hyperapp/html 2 | 3 | > Write HTML with plain functions. 4 | 5 | Hyperapp's built-in `h()` function is intentionally primitive to give you the freedom to write views any way you like it. If you prefer a functional approach over templating solutions like JSX or template literals, here is a collection of functions—one for [each HTML tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element)—to make your views faster to write and easier to read. 6 | 7 | Here's the first example to get you started. [Try it in your browser](https://codepen.io/jorgebucaran/pen/MrBgMy?editors=1000). 8 | 9 | ```html 10 | 11 | 12 | 13 | 36 | 37 | 38 |
39 | 40 | 41 | ``` 42 | 43 | > Looking for [@hyperapp/svg](../svg) instead? 44 | 45 | ## Installation 46 | 47 | ```console 48 | npm install @hyperapp/html 49 | ``` 50 | 51 | Then with a module bundler like [Rollup](https://rollupjs.org) or [Webpack](https://webpack.js.org) import it in your application and get right down to business. 52 | 53 | ```js 54 | import { a, form, input } from "@hyperapp/html" 55 | ``` 56 | 57 | Don't want to set up a build step? Import it in a ` 63 | ``` 64 | 65 | ## License 66 | 67 | [MIT](../../LICENSE.md) 68 | -------------------------------------------------------------------------------- /packages/html/index.js: -------------------------------------------------------------------------------- 1 | import { h } from "hyperapp" 2 | 3 | const EMPTY_ARR = [] 4 | const EMPTY_OBJ = {} 5 | 6 | const tag = (tag) => ( 7 | props = EMPTY_OBJ, 8 | children = props.tag != null || Array.isArray(props) ? props : EMPTY_ARR 9 | ) => h(tag, props === children ? EMPTY_OBJ : props, children) 10 | 11 | export const a = tag("a") 12 | export const b = tag("b") 13 | export const i = tag("i") 14 | export const p = tag("p") 15 | export const q = tag("q") 16 | export const s = tag("s") 17 | export const br = tag("br") 18 | export const dd = tag("dd") 19 | export const dl = tag("dl") 20 | export const dt = tag("dt") 21 | export const em = tag("em") 22 | export const h1 = tag("h1") 23 | export const h2 = tag("h2") 24 | export const h3 = tag("h3") 25 | export const h4 = tag("h4") 26 | export const h5 = tag("h5") 27 | export const h6 = tag("h6") 28 | export const hr = tag("hr") 29 | export const li = tag("li") 30 | export const ol = tag("ol") 31 | export const rp = tag("rp") 32 | export const rt = tag("rt") 33 | export const td = tag("td") 34 | export const th = tag("th") 35 | export const tr = tag("tr") 36 | export const ul = tag("ul") 37 | export const bdi = tag("bdi") 38 | export const bdo = tag("bdo") 39 | export const col = tag("col") 40 | export const del = tag("del") 41 | export const dfn = tag("dfn") 42 | export const div = tag("div") 43 | export const img = tag("img") 44 | export const ins = tag("ins") 45 | export const kbd = tag("kbd") 46 | export const map = tag("map") 47 | export const nav = tag("nav") 48 | export const pre = tag("pre") 49 | export const rtc = tag("rtc") 50 | export const sub = tag("sub") 51 | export const sup = tag("sup") 52 | export const wbr = tag("wbr") 53 | export const abbr = tag("abbr") 54 | export const area = tag("area") 55 | export const cite = tag("cite") 56 | export const code = tag("code") 57 | export const data = tag("data") 58 | export const form = tag("form") 59 | export const main = tag("main") 60 | export const mark = tag("mark") 61 | export const ruby = tag("ruby") 62 | export const samp = tag("samp") 63 | export const span = tag("span") 64 | export const time = tag("time") 65 | export const aside = tag("aside") 66 | export const audio = tag("audio") 67 | export const input = tag("input") 68 | export const label = tag("label") 69 | export const meter = tag("meter") 70 | export const param = tag("param") 71 | export const small = tag("small") 72 | export const table = tag("table") 73 | export const tbody = tag("tbody") 74 | export const tfoot = tag("tfoot") 75 | export const thead = tag("thead") 76 | export const track = tag("track") 77 | export const video = tag("video") 78 | export const button = tag("button") 79 | export const canvas = tag("canvas") 80 | export const dialog = tag("dialog") 81 | export const figure = tag("figure") 82 | export const footer = tag("footer") 83 | export const header = tag("header") 84 | export const iframe = tag("iframe") 85 | export const legend = tag("legend") 86 | export const object = tag("object") 87 | export const option = tag("option") 88 | export const output = tag("output") 89 | export const select = tag("select") 90 | export const source = tag("source") 91 | export const strong = tag("strong") 92 | export const address = tag("address") 93 | export const article = tag("article") 94 | export const caption = tag("caption") 95 | export const details = tag("details") 96 | export const section = tag("section") 97 | export const summary = tag("summary") 98 | export const picture = tag("picture") 99 | export const colgroup = tag("colgroup") 100 | export const datalist = tag("datalist") 101 | export const fieldset = tag("fieldset") 102 | export const menuitem = tag("menuitem") 103 | export const optgroup = tag("optgroup") 104 | export const progress = tag("progress") 105 | export const textarea = tag("textarea") 106 | export const blockquote = tag("blockquote") 107 | export const figcaption = tag("figcaption") 108 | 109 | export { text } from "hyperapp" -------------------------------------------------------------------------------- /packages/html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hyperapp/html", 3 | "version": "2.0.0", 4 | "type": "module", 5 | "main": "index.js", 6 | "description": "Write HTML with plain functions.", 7 | "repository": "https://github.com/jorgebucaran/hyperapp/tree/main/packages/html", 8 | "license": "MIT", 9 | "keywords": [ 10 | "hyperapp", 11 | "html" 12 | ], 13 | "peerDependencies": { 14 | "hyperapp": "^2.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/svg/README.md: -------------------------------------------------------------------------------- 1 | # @hyperapp/svg 2 | 3 | > Draw SVG with plain functions. 4 | 5 | HTML's evil twin. Here's a collection of functions—one for [each SVG tag](https://developer.mozilla.org/en-US/docs/Web/SVG)—to help you get started with SVG in Hyperapp. 6 | 7 | Want to draw some circles? [Try this example in your browser](https://codepen.io/jorgebucaran/pen/preYMW?editors=1000). 8 | 9 | ```html 10 | 11 | 12 | 13 | 31 | 32 | 33 |
34 | 35 | 36 | ``` 37 | 38 | ## Installation 39 | 40 | ```console 41 | npm install @hyperapp/svg 42 | ``` 43 | 44 | Then with a module bundler like [Rollup](https://rollupjs.org) or [Webpack](https://webpack.js.org) import it in your application and get right down to business. 45 | 46 | ```js 47 | import { svg, use, circle } from "@hyperapp/svg" 48 | ``` 49 | 50 | Don't want to set up a build step? Import it in a ` 56 | ``` 57 | 58 | ## License 59 | 60 | [MIT](../../LICENSE.md) 61 | -------------------------------------------------------------------------------- /packages/svg/index.js: -------------------------------------------------------------------------------- 1 | import { h } from "hyperapp" 2 | 3 | const EMPTY_ARR = [] 4 | const EMPTY_OBJ = {} 5 | 6 | const tag = (tag) => ( 7 | props, 8 | children = props.tag != null || Array.isArray(props) ? props : EMPTY_ARR 9 | ) => h(tag, props === children ? EMPTY_OBJ : props, children) 10 | 11 | export const a = tag("a") 12 | export const g = tag("g") 13 | export const svg = tag("svg") 14 | export const use = tag("use") 15 | export const set = tag("set") 16 | export const line = tag("line") 17 | export const path = tag("path") 18 | export const rect = tag("rect") 19 | export const desc = tag("desc") 20 | export const defs = tag("defs") 21 | export const mask = tag("mask") 22 | export const tref = tag("tref") 23 | export const font = tag("font") 24 | export const stop = tag("stop") 25 | export const view = tag("view") 26 | export const text_ = tag("text") 27 | export const image = tag("image") 28 | export const mpath = tag("mpath") 29 | export const title = tag("title") 30 | export const glyph = tag("glyph") 31 | export const tspan = tag("tspan") 32 | export const style = tag("style") 33 | export const circle = tag("circle") 34 | export const marker = tag("marker") 35 | export const symbol = tag("symbol") 36 | export const feTile = tag("feTile") 37 | export const cursor = tag("cursor") 38 | export const filter = tag("filter") 39 | export const switch_ = tag("switch") 40 | export const ellipse = tag("ellipse") 41 | export const polygon = tag("polygon") 42 | export const animate = tag("animate") 43 | export const pattern = tag("pattern") 44 | export const feBlend = tag("feBlend") 45 | export const feFlood = tag("feFlood") 46 | export const feFuncA = tag("feFuncA") 47 | export const feFuncB = tag("feFuncB") 48 | export const feFuncG = tag("feFuncG") 49 | export const feFuncR = tag("feFuncR") 50 | export const feImage = tag("feImage") 51 | export const feMerge = tag("feMerge") 52 | export const polyline = tag("polyline") 53 | export const metadata = tag("metadata") 54 | export const altGlyph = tag("altGlyph") 55 | export const glyphRef = tag("glyphRef") 56 | export const textPath = tag("textPath") 57 | export const feOffset = tag("feOffset") 58 | export const clipPath = tag("clipPath") 59 | export const altGlyphDef = tag("altGlyphDef") 60 | export const feComposite = tag("feComposite") 61 | export const feMergeNode = tag("feMergeNode") 62 | export const feSpotLight = tag("feSpotLight") 63 | export const animateColor = tag("animateColor") 64 | export const altGlyphItem = tag("altGlyphItem") 65 | export const feMorphology = tag("feMorphology") 66 | export const feTurbulence = tag("feTurbulence") 67 | export const fePointLight = tag("fePointLight") 68 | export const colorProfile = tag("colorProfile") 69 | export const foreignObject = tag("foreignObject") 70 | export const animateMotion = tag("animateMotion") 71 | export const feColorMatrix = tag("feColorMatrix") 72 | export const linearGradient = tag("linearGradient") 73 | export const radialGradient = tag("radialGradient") 74 | export const feGaussianBlur = tag("feGaussianBlur") 75 | export const feDistantLight = tag("feDistantLight") 76 | export const animateTransform = tag("animateTransform") 77 | export const feConvolveMatrix = tag("feConvolveMatrix") 78 | export const feDiffuseLighting = tag("feDiffuseLighting") 79 | export const feDisplacementMap = tag("feDisplacementMap") 80 | export const feSpecularLighting = tag("feSpecularLighting") 81 | export const feComponentTransfer = tag("feComponentTransfer") 82 | -------------------------------------------------------------------------------- /packages/svg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hyperapp/svg", 3 | "version": "2.0.0", 4 | "type": "module", 5 | "main": "index.js", 6 | "description": "Draw SVG with plain functions.", 7 | "repository": "https://github.com/jorgebucaran/hyperapp/tree/main/packages/svg", 8 | "license": "MIT", 9 | "keywords": [ 10 | "hyperapp", 11 | "svg" 12 | ], 13 | "peerDependencies": { 14 | "hyperapp": "^2.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/time/README.md: -------------------------------------------------------------------------------- 1 | # @hyperapp/time 2 | 3 | > Subscribe to intervals, get the time now. 4 | 5 | ## Installation 6 | 7 | ```console 8 | npm install @hyperapp/time 9 | ``` 10 | 11 | ```js 12 | import { every, delay, now } from "@hyperapp/time" 13 | ``` 14 | 15 | Or without a build step—import it right in your browser. 16 | 17 | ```html 18 | 21 | ``` 22 | 23 | ## License 24 | 25 | [MIT](../../LICENSE.md) 26 | -------------------------------------------------------------------------------- /packages/time/index.js: -------------------------------------------------------------------------------- 1 | const timeout = (dispatch, props) => 2 | setTimeout(() => dispatch(props.action), props.delay) 3 | 4 | const interval = (dispatch, props) => { 5 | const id = setInterval(() => { 6 | dispatch(props.action, Date.now()) 7 | }, props.delay) 8 | return () => clearInterval(id) 9 | } 10 | 11 | const getTime = (dispatch, props) => dispatch(props.action, Date.now()) 12 | 13 | /** 14 | * @example 15 | * app({ 16 | * subscriptions: (state) => [ 17 | * // Dispatch RequestResource every delayInMilliseconds 18 | * every(state.delayInMilliseconds, RequestResource), 19 | * ], 20 | * }) 21 | */ 22 | export const every = (delay, action) => [interval, { delay, action }] 23 | 24 | /** 25 | * @example 26 | * const SlowClap = (state, ms = 1200) => [state, delay(ms, Clap)] 27 | */ 28 | export const delay = (delay, action) => [timeout, { delay, action }] 29 | 30 | /** 31 | * @example 32 | * now(NewTime) 33 | */ 34 | export const now = (action) => [getTime, { action }] 35 | -------------------------------------------------------------------------------- /packages/time/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hyperapp/time", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "index.js", 6 | "description": "Subscribe to intervals, get the time now.", 7 | "repository": "https://github.com/jorgebucaran/hyperapp/tree/main/packages/time", 8 | "license": "MIT", 9 | "keywords": [ 10 | "hyperapp", 11 | "interval", 12 | "delay", 13 | "time", 14 | "now" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import { h, text } from "../index.js" 2 | import { t, deepEqual } from "twist" 3 | 4 | export default [ 5 | t("hyperapp", [ 6 | t("hyperscript function", [ 7 | t("create virtual nodes", [ 8 | deepEqual(h("zord", { foo: true }, []), { 9 | children: [], 10 | key: undefined, 11 | node: undefined, 12 | props: { 13 | foo: true, 14 | }, 15 | type: undefined, 16 | tag: "zord", 17 | }), 18 | ]), 19 | ]), 20 | t("text function", [ 21 | deepEqual(text("hyper"), { 22 | children: [], 23 | key: undefined, 24 | node: undefined, 25 | props: {}, 26 | type: 3, 27 | tag: "hyper", 28 | }), 29 | ]), 30 | ]), 31 | ] 32 | --------------------------------------------------------------------------------