diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index fab411ae4..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: CI -on: push -jobs: - test: - env: - NODE_ENV: development - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [14.x] - steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Test - run: | - npm install -g codecov - npm install - npm test - codecov diff --git a/.gitignore b/.gitignore index 9ee768669..5fe10f5e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,19 @@ -*.html -*.map -*.br -*.gz +# IDE +.idea/ +.vscode/ +# Logs +*.log* +yarn.lock package-lock.json + +# Misc +examples +notes.md node_modules coverage -private +*.xml +# Dist +*.gz +dist/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..d45dd92d1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: node_js +node_js: + - "6" + - "8" + - "9" + +env: + - NODE_ENV=development + +before_install: + - npm i -g codecov + +install: + - npm install + +script: + - npm test + - codecov + +notifications: + email: false diff --git a/LICENSE.md b/LICENSE.md index 6ba7a0fbb..6a67a44a3 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,19 @@ -Copyright © Jorge Bucaran <> +Copyright © 2017-present [Jorge Bucaran](https://github.com/JorgeBucaran) -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: +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: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -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. +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. diff --git a/README.md b/README.md index 493affe02..437754a32 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,48 @@ -# Hyperapp +# [Hyperapp](https://codepen.io/hyperapp) -> The tiny framework for building hypertext applications. +[![Travis CI](https://img.shields.io/travis/hyperapp/hyperapp/master.svg)](https://travis-ci.org/hyperapp/hyperapp) [![Codecov](https://img.shields.io/codecov/c/github/hyperapp/hyperapp/master.svg)](https://codecov.io/gh/hyperapp/hyperapp) [![npm](https://img.shields.io/npm/v/hyperapp.svg)](https://www.npmjs.org/package/hyperapp) [![Slack](https://hyperappjs.herokuapp.com/badge.svg)](https://hyperappjs.herokuapp.com "Join us") -- **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. -- **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. -- **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. +Hyperapp is a JavaScript library for building frontend applications. -Here's the first example to get you started. [Try it here](https://codepen.io/jorgebucaran/pen/zNxZLP?editors=1000)—no build step required! +* **Minimal**: Hyperapp was born out of the attempt to do more with less. We have aggressively minimized the concepts you need to understand while remaining on par with what other frameworks can do. +* **Functional**: Hyperapp's design is inspired by [The Elm Architecture](https://guide.elm-lang.org/architecture). Create scalable browser-based applications using a functional paradigm. The twist is you don't have to learn a new language. +* **Batteries-included**: Out of the box, Hyperapp combines state management with a VDOM engine that supports keyed updates & lifecycle events — all with no dependencies. - -```html - +```jsx +import { h, app } from "hyperapp" -
-``` - -[Check out more examples](https://codepen.io/collection/nLLvrz?grid_type=grid) - -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. +const state = { + count: 0 +} -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. +const actions = { + down: () => state => ({ count: state.count - 1 }), + up: () => state => ({ count: state.count + 1 }) +} -## Installation +const view = (state, actions) => ( +
+

{state.count}

+ + +
+) -```console -npm install hyperapp +export const main = app(state, actions, view, document.body) ``` -## Documentation - -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). - -## Packages - -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. - -| Package | Status | About | -| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | -| [`@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. | -| [`@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. | -| [`@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. | -| [`@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. | -| [`@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. | -| [`@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)). | -| [`@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. | -| [`@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. | - -> Need to create your own effects and subscriptions? [You can do that too](docs/reference.md). - -## Help, I'm stuck! - -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. - -## Contributing - -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 +## Community -[![](https://opencollective.com/hyperapp/contributors.svg?width=1024&button=false)](https://github.com/jorgebucaran/hyperapp/graphs/contributors) +* [Slack](https://hyperappjs.herokuapp.com) +* [/r/Hyperapp](https://www.reddit.com/r/hyperapp) +* [Twitter](https://twitter.com/hyperappjs) ## License -[MIT](LICENSE.md) +Hyperapp is MIT licensed. See [LICENSE](LICENSE.md). diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100755 index 000000000..8c23b7240 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing to Hyperapp + +Thank you for taking the time to read our contribution guidelines. You can start contributing in many ways like filing bug reports, improving the documentation, and helping others. + +## Style + +* Hyperapp is written in ES5. +* Format your code before creating a new commit using `npm run format`. +* We prefer keeping all the moving parts inside as few files as possible. We don't have plans to break up the library into smaller modules. + +## Bugs + +* Before submitting a bug report, search the issues for similar tickets. Your issue may have already been discussed and resolved. Feel free to add a comment to an existing ticket, even if it's closed. +* Determine which repository the problem should be reported in. If you have an issue with the Router, you'll be better served in [hyperapp/router](https://github.com/hyperapp/router), etc. +* If you have a question or need help with something you are building, hop on [Slack](https://hyperappjs.herokuapp.com). +* Be thorough in your title and report, don't leave out important details, describe your setup and include any relevant code with your issue. +* Use GitHub [fenced code blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks/) when sharing code. If your code has JSX in it, please use ```jsx for best syntax highlighting. + +## Tests + +* We are currently using [Babel](https://babeljs.io) and [Jest](http://facebook.github.io/jest) for tests. +* Feel free to create a new `test/*.test.js` file if none of the existing test files suits your test case. +* Tests usually start by creating a small application and using a feature, then check if `document.body.innerHTML` matches some string. The app() call is async, so we often use [`oncreate`](lifecycle-events.md#oncreate) or [`onupdate`](lifecycle-events.md#onupdate) events to detect when the view has been rendered. + +## Humans + +Our open source community strives to be nice, welcoming and professional. Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by contacting us at [hyperappjs@gmail.com](hyperappjs@gmail.com). diff --git a/docs/README.md b/docs/README.md new file mode 100755 index 000000000..76a3defff --- /dev/null +++ b/docs/README.md @@ -0,0 +1,17 @@ +# Documentation + +* [Introduction](introduction/README.md) + * [Hello World](introduction/hello-world.md) + * [Installation](introduction/installation.md) +* [Concepts](concepts/README.md) + * [Virtual Nodes](concepts/vnodes.md) + * [Keys](concepts/keys.md) + * [Components](concepts/components.md) + * [Lifecycle Events](concepts/lifecycle-events.md) + * [Sanitation](concepts/sanitation.md) + * [Hydration](concepts/hydration.md) +* [Tutorials](tutorials/README.md) + * [TweetBox](tutorials/tweetbox.md) + * [Gif Search](tutorials/gif-search.md) + * [Countdown Timer](tutorials/countdown-timer.md) +* [Contributing](CONTRIBUTING.md) diff --git a/docs/api/app.md b/docs/api/app.md deleted file mode 100644 index 7fb1ba15d..000000000 --- a/docs/api/app.md +++ /dev/null @@ -1,163 +0,0 @@ -# `app()` - -Initializes and mounts a Hyperapp application. - -```elm -app : ({ Init, View, Node, Subscriptions?, Dispatch? }) -> DispatchFn -``` - -| Prop | Type | Required? | -| -------------------------------- | --------------------------------------------------------------------------- | -------------------------------- | -| [init:](#init) | | No | -| [view:](#view) | [View](../architecture/views.md) | No | -| [node:](#node) | DOM element | **Yes when `view:` is present.** | -| [subscriptions:](#subscriptions) | Function | No | -| [dispatch:](#dispatch) | [Dispatch Initializer](../architecture/dispatch.md#dispatch-initializer) | No | - -| Return Value | Type | -| --------------------------------------- | -------- | -| [dispatch](../architecture/dispatch.md) | Function | - -```js -import { app, h, text } from "hyperapp" - -app({ - init: { message: "Hello World!" }, - view: (state) => h("p", {}, text(state.message)), - node: document.getElementById("app"), -}) -``` - -## `init:` - -_(default value: `{}`)_ - -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. - -### Forms of `init:` - -- `init: state` - - Sets the initial state directly. - - ```js - app({ - init: { counter: 0 }, - // ... - }) - ``` - -- `init: [state, ...effects]` - - Sets the initial state and then runs the given list of [effects](../architecture/effects.md). - - ```js - app({ - init: [ - { loading: true }, - log("Loading..."), - load("myUrl?init", DoneAction), - ], - // ... - }) - ``` - -- `init: Action` - - Runs the given [Action](../architecture/actions.md). - - This form is useful when the action can be reused later. The state passed to the action in this case is `undefined`. - - ```js - const Reset = (_state) => ({ counter: 0 }) - - app({ - init: Reset, - // ... - }) - ``` - -- `init: [Action, payload]` - - Runs the given [Action](../architecture/actions.md) with a payload. - - ```js - const SetCounter = (_state, n) => ({ counter: n }) - - app({ - init: [SetCounter, 10], - // ... - }) - ``` - -## `view:` - -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. - -```js -app({ - // ... - view: (state) => h("main", {}, [ - outworld(state), - netherrealm(state), - ]), -}) -``` - - - -## `node:` - -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. - -```html -
-``` - -```js -app({ - // ... - node: document.getElementById("app"), -}) -``` - -## `subscriptions:` - -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. - -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. - -If `subscriptions:` is omitted the app has no subscriptions. It behaves the same as if you were using: `subscriptions: (state) => []` - -```js -import { onKey } from "./subs" - -app({ - // ... - subscriptions: (state) => [ - onKey("w", MoveForward), - onKey("a", MoveBackward), - onKey("s", StrafeLeft), - onKey("d", StrafeRight), - state.playingDOOM1993 || onKey(" ", Jump), - ], -}) -``` - - - -## `dispatch:` - -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. - -## Return Value - -`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. - -Calling the dispatch function with no arguments frees the app's resources and runs every active subscription's cleanup function. - -## Other Considerations - -- You can embed your Hyperapp application within another already existing Hyperapp application or an app that was built with some other framework. - -- 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). diff --git a/docs/api/h.md b/docs/api/h.md deleted file mode 100644 index c67077081..000000000 --- a/docs/api/h.md +++ /dev/null @@ -1,227 +0,0 @@ -

h()

- -**_Definition:_** - -> A function that creates [virtual DOM nodes (VNodes)](../architecture/views.md#virtual-dom) which are used for defining [views](../architecture/views.md). - -**_Import & Usage:_** - -```js -import { h } from "hyperapp" - -// ... - -h(tag, props, children) -``` - -**_Signature & Parameters:_** - -```elm -h : (String, Object, VNode? | [...VNodes]?) -> VNode -``` - -| Parameters | Type | Required? | -| --------------------- | ------------------------ | --------- | -| [tag](#tag) | String | yes :100: | -| [props](#props) | Object | yes :100: | -| [children](#children) | VNode or array of VNodes | no | - -| Return Value | Type | -| ---------------------------------------------------- | ----- | -| [virtual node](../architecture/views.md#virtual-dom) | VNode | - -`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. - -```js -const hobbit = (wearingElvenCloak) => - h("div", {}, [ - !wearingElvenCloak && h("p", {}, text("Frodo")), - ]) -``` - - - ---- - -## Parameters - -### `tag` - -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). - -### `props` - -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. - -```js -h("input", { - type: "checkbox", - id: "picard", - checked: state.engaging, -}) -``` - - - -Hyphenated props will need to be quoted in order to use them. The quotes are necessary to abide by JavaScript syntax restrictions. - -```js -h("q", { "data-zoq-fot-pik": "Frungy" }, text("The Sport of Kings!")) -``` - - - -Certain properties are treated in a special way by Hyperapp. - -#### `class:` - -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: - -- 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. - - ```js - h("div", { class: "muggle-studies" }) - ``` - - - -- As an object where the keys are the names of the classes while the values are booleans for toggling the classes. - - ```js - h("div", { class: { arithmancy: true, "study-of-ancient-runes": true } }) - ``` - - - -- As an array that contains any combination of the various formats including this one. - - ```js - h("div", { class: ["magical theory", "xylomancy"] }) - ``` - - - - This means the array format is recursive. - - ```js - h("input", { - type: "range", - class: [ - { dragonzord: state.green && !state.white }, - "mastodon", - state.pink && "pterodactyl", - [ - { triceratops: state.blue }, - "sabretooth-tiger", - state.red && "tyrannosaurus", - ], - ], - }) - ``` - - - -#### `style:` - -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. - -```js -h( - "span", - { - style: { - backgroundColor: "white", - color: "blue", - display: "inline-block", - "font-weight": "bold", - }, - }, - text("+\\") -) -``` - - - -#### `key:` - -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. - -```js -const pokedex = (pokemon) => - h( - "ul", - {}, - pokemon.map((p) => h("li", { key: p.id }, text(p.name))) - ) -``` - - - -#### Event Listeners - -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. - -Synthetic events can be added in the same way as long as their name starts with "on", so an event created with -```js -const buildEvent = new Event("build") -``` -can be used like this: -```js -h("button", { onbuild: BuildAction }, text("Click Me")) -``` - -### `children` - -The children of the VNode are other VNodes which are directly nested within it. - -`children` can either be given as a single child VNode: - -```js -h("q", {}, text("There is no spoon.")) -``` - - - -or as an array of child VNodes: - -```js -h("q", {}, [ - text("I know Kung Fu."), - h("em", {}, text("Show me.")), -]) -``` - - - ---- - -## Other Considerations - -### JSX Support - -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. - -```js -import { h, text } from "hyperapp" - -const jsxify = (h) => (type, props, ...children) => - typeof type === "function" - ? type(props, children) - : h( - type, - props || {}, - [].concat(...children).map((x) => - typeof x === "string" || typeof x === "number" ? text(x) : x - ) - ) - -const jsx = jsxify(h) /** @jsx jsx */ -``` diff --git a/docs/api/memo.md b/docs/api/memo.md deleted file mode 100644 index 50db6027c..000000000 --- a/docs/api/memo.md +++ /dev/null @@ -1,123 +0,0 @@ -# `memo()` - -**_Definition:_** - -> A wrapper function to cache your [views](../architecture/views.md) based on properties you pass into them. - -**_Import & Usage:_** - -```js -import { memo } from "hyperapp" - -// ... - -memo(view, props) -``` - -**_Signature & Parameters:_** - -```elm -memo : (View, IndexableData) -> VNode -``` - -| Parameters | Type | Required? | -| ------------- | ----------------------------------------------- | --------- | -| [view](#view) | [View](../architecture/views.md) | yes :100: | -| [data](#data) | anything indexable (i.e. Array, Object, String) | no | - -| Return Value | Type | -| ---------------------------------------------------- | ----- | -| [virtual node](../architecture/views.md#virtual-dom) | VNode | - -`memo()` lets you take advantage of a performance optimization technique known as [memoization](../architecture/views.md#memoization). - ---- - -## Parameters - -### `view` - -A [view](../architecture/views.md) you want [memoized](../architecture/views.md#memoization). - -### `data` - -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. - ---- - -## Example - -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. - -```js -import { h, text, app, memo } from "hyperapp" - -const randomHex = () => "0123456789ABCDEF"[Math.floor(Math.random() * 16)] -const randomColor = () => "#" + Array.from({ length: 6 }, randomHex).join("") - -const listView = (list) => - h("p", { - style: { - backgroundColor: randomColor(), - color: randomColor(), - }, - }, text(list)) - -const MoreItems = (state) => ({ ...state, list: [...state.list, randomHex()] }) -const Increment = (state) => ({ ...state, counter: state.counter + 1 }) - -app({ - init: { - list: ["a", "b", "c"], - counter: 0, - }, - view: (state) => - h("main", {}, [ - h("button", { onclick: MoreItems }, text("Grow list")), - h("button", { onclick: Increment }, text("+1 to counter")), - h("p", {}, text(`Counter: ${state.counter}`)), - h("p", {}, text("Regular view showing list:")), - listView(state.list), - h("p", {}, text("Memoized view showing list:")), - memo(listView, state.list), - ]), - node: document.querySelector("main"), -}) -``` - ---- - -## Other Considerations - -### Performance - -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! - -### Memo Data Gotcha - -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. - -We can modify parts of the example from earlier to illustrate this: - -```js -// ... - -const MoreItems = (state) => ({ - ...state, - list: Array.isArray(state.list) - ? [...state.list, randomHex()] - : state.list + "" + randomHex(), -}) - -const Increment = (state) => ({ - ...state, - counter: state.counter + 1, - - // The following should cause the memoized view to rerender but it doesn't. - list: Array.isArray(state.list) - ? state.list.join("") - : state.list.split(""), -}) - -// ... -``` diff --git a/docs/api/text.md b/docs/api/text.md deleted file mode 100644 index 09b2fa83a..000000000 --- a/docs/api/text.md +++ /dev/null @@ -1,56 +0,0 @@ -# `text()` - -**_Definition:_** - -> A function that creates a [virtual DOM node (VNode)](../architecture/views.md#virtual-dom) out of a given value. - -**_Import & Usage:_** - -You'll normally use it with [`h()`](./h.md). - -```js -import { text } from "hyperapp" - -// ... - -h("p", {}, text(content)) -``` - -**_Signature & Parameters:_** - -```elm -text : (String | Number) -> VNode -``` - -| Parameter | Type | Required? | Notes | -| ------------------- | ----------------------------------------------------- | -------------- | --------------------------------------- | -| [content](#content) | any (sort of), but meaningfully only String or Number | yes :100: | | -| node | DOM element | prohibited :x: | This is for internal Hyperapp use only! | - -| Return Value | Type | -| --------------------------------------------------------- | ----- | -| [virtual text node](../architecture/views.md#virtual-dom) | VNode | - -You would use `text()` to insert regular text content into your views. - -```js -h("p", {}, text("You must construct additional pylons.")) -``` - - - -Of course, this may include anything relevant from the [current state](../architecture/state.md). - -```js -h("p", {}, text(state.message)) -``` - -`text()` exists as the way of defining text nodes such that Hyperapp's implementation is kept simpler than it otherwise would have been. - ---- - -## Parameters - -### `content` - -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. diff --git a/docs/architecture/actions.md b/docs/architecture/actions.md deleted file mode 100644 index 8ce739e50..000000000 --- a/docs/architecture/actions.md +++ /dev/null @@ -1,246 +0,0 @@ -# Actions - -**_Definition:_** - -> An **action** is a message used within your app that signals the valid way to change [state](state.md). - -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. - -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. - -**_Signature:_** - -```elm -Action : (State, Payload?) -> NextState - | [NextState, ...Effects] - | OtherAction - | [OtherAction, Payload?] -``` - -**_Naming Recommendation:_** - -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. - ---- - -## Simple State Transitions - -The simplest possible action merely returns the current state: - -```js -// Action : (State) -> SameState -const Identity = (state) => state -``` - -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). - -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. - -```js -h("button", { onclick: Identity }, text("Do Nothing")) -``` - -The next simplest type of action merely sets the state. - -```js -// Action : () -> ForcedState -const FeedFace = () => 0xfeedface -``` - - - - -But you'll most likely want to do actual state transitions. - -```js -// Action : (State) -> NewState -const Increment = (state) => ({ ...state, value: state.value + 1 }) - -// ... - -h("button", { onclick: Increment }, text("+")) -``` - ---- - -## Payloads - -Actions can also accept an optional **payload** along with the current state. - -```js -// Action : (State, Payload?) -> NewState -const AddBy = (state, amount) => ({ ...state, value: state.value + amount }) -``` - -To give a payload to an action we'll want to use an **action descriptor**. - -```js -h("button", { onclick: [AddBy, 5] }, text("+5")) -``` - -### Event Payloads - -Actions used as event handlers receive the event object as the default payload. - -If we were to use our `AddBy` action without specifying its payload: - -```js -h("button", { onclick: AddBy }, text("+5")) -``` - -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. - -However, if we wanted to make proper use of the event object we have a couple options: - -- Rewrite `AddBy` to account for the possibility of receiving an event payload. -- Or preprocess the event object to make it work with `AddBy` as it is. - -The latter option is preferred because it lets our action remain unconcerned with how its payload is sourced thereby maintaining its reusability. - -Which brings us to... - ---- - -## Wrapped Actions - -Actions can return other actions. The simplest form of these basically acts like an alias. - -```js -// Action : () -> OtherAction -const PlusOne = () => Increment -``` - -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. - -```js -// Action : (State, EventPayload) -> [OtherAction, Payload] -const AddByValue = (state, event) => [AddBy, +event.target.value] -``` - -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: - -```js -h("input", { value: state, oninput: AddByValue }) -``` - -You can keep wrapping actions for as long as your sanity permits. The benefit is the ability to chain together payload adjustments. - -```js -const AddBy = (state, amount) => ({ ...state, value: state.value + amount }) -const AddByMore = (_, amount) => [AddBy, amount + 5] -const AddByEvenMore = (_, amount) => [AddByMore, amount + 10] - -// ... - -h( - "button", - { onclick: [AddByEvenMore, 1] }, - text("+16") -) -``` - ---- - -## Transforms - -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. - -```js -const Liokaiser = (state) => ({ - ...state, - combined: true, - leftArm: hellbat(state), - rightArm: guyhawk(state), - upperTorso: leozack(state), - lowerTorso: jallguar(state), - leftLeg: drillhorn(state), - rightLeg: killbison(state), -}) -``` - - - ---- - -## Stopping Your App - -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. - -```js -// Action : () -> undefined -const Stop = () => undefined -``` - -Once your app stops, several things happen: - -- All of the app's subscriptions stop. -- The DOM is no longer touched. -- Event handlers stop working. - -A stopped app cannot be restarted. - -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. - ---- - -## Other Considerations - -### Transitioning Array State - -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. - -There are a couple of options available: - -- 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. - - ```js - const ArrayAction = (state) => [[...state, "one"]] - ``` - -- 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. - - ```js - const ObjectAction = (state) => ({ ...state, list: [...state.list, "one"] }) - ``` - -### Nonstandard Usage - -- 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. - -- If you wanted to use curried functions to implement actions then you can use named function expressions. - - ```js - const Meet = (name) => - function AndGreet(state) { - return `${state.salutation}, my name is ${name}.` - } - ``` - -- If you have some special requirements you can customize how actions are [dispatched](dispatch.md). - -- 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. - - ```js - h("button", { onclick: 55 }, text("55")) - h("button", { onclick: [55, log] }, text("55 and log")) - ``` - - 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: - - ```js - const FiftyFive = () => 55 - const FiftyFiveAndLog = () => [55, log] - ``` - - ```js - h("button", { onclick: FiftyFive }, text("55")) - h("button", { onclick: FiftyFiveAndLog }, text("55 and log")) - ``` - - 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. - - That said, this type of usage is fascinating... - - ```js - h("button", { onclick: state.startingOver ? "Begin" : MyCoolAction }, text("cool")) - ``` diff --git a/docs/architecture/dispatch.md b/docs/architecture/dispatch.md deleted file mode 100644 index f98785bb3..000000000 --- a/docs/architecture/dispatch.md +++ /dev/null @@ -1,153 +0,0 @@ -# Dispatch - -**_Definition:_** - -> 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. - -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. - -**_Signature:_** - -```elm -DispatchFn : (Action, Payload?) -> void -``` - ---- - -## Dispatch Initializer - -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: - -```js -const boring = (dispatch) => dispatch -``` - -In your own initializer you'll likely want to return a variant of the regular dispatch. - ---- - -## Augmented Dispatching - -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. - -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. - -```js -// DispatchFn : (Action, Payload?) -> void -const dispatch = (action, payload) => { - // Do your custom work here. - // ... - - // Hand dispatch over to built-in dispatch. - dispatch(action, payload) -} -``` - -## Dispatch recursion - -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. - -A call to `dispatch([ActionFn, payload])` will recurse `dispatch(ActionFn, payload)`, which will recurse to `dispatch(ActionFn(currentState, payload))`. - ---- - -## Example 1 - Log actions - -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. - -```js -const logActionsMiddleware = dispatch => (action, payload) => { - - if (typeof action === 'function') { - console.log('DISPATCH: ', action.name || action) - } - - //pass on to original dispatch - dispatch(action, payload) -} -``` - ---- - -## Example 2 - Log state - -To log each state transformation, we first create a general state middleware and then use it to create an augmented dispatch for state logging: - -```js -const stateMiddleware = fn => dispatch => (action, payload) => { - if (Array.isArray(action) && typeof action[0] !== 'function') { - action = [fn(action[0]), ...action.slice(1)] - } else if (!Array.isArray(action) && typeof action !== 'function') { - action = fn(action) - } - dispatch(action, payload) -} - -const logStateMiddleware = stateMiddleware(state => { - console.log('STATE:', state) - return state -}) -``` - ---- - -## Example 3 - Immutable state - -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: - -```js -// a proxy prohibiting mutation -const immutableProxy = o => { - if (o===null || typeof o !== 'object') return o - return new Proxy(o, { - get(obj, prop) { - return immutableProxy(obj[prop]) - }, - set(obj, prop) { - throw new Error(`Can not set prop ${prop} on immutable object`) - } - }) -} - -export const immutableMiddleware = stateMiddleware(state => immutableProxy(state)) -``` - - -## Usage - -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. - -The only time the dispatch initializer gets used is once during the instantiation of your app. - -Only one dispatch initializer can be defined per app. Consequently, only one dispatch can be defined per app. - -Extending the example from above, the dispatch initializer would be used like this: - -```js -import { mwLogState } from "./middleware.js" - -app({ - // ... - dispatch: logActionsMiddleware -}) -``` - -And if you wanted to use all custom dispatches together, you can chain them like this: - -```js -import { logActionsMiddleware, logStateMiddleware, immutableMiddleware } from "./middleware.js" - -app({ - // ... - dispatch: dispatch => logStateMiddleware(logActionsMiddleware(immutableMiddleware(dispatch))) -}) -``` - - ---- - -## Other Considerations - -- [`app()`](../api/app.md) returns the dispatch function to allow [dispatching externally](../api/app.md#instrumentation). - -- 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. diff --git a/docs/architecture/effects.md b/docs/architecture/effects.md deleted file mode 100644 index 5de06b65a..000000000 --- a/docs/architecture/effects.md +++ /dev/null @@ -1,248 +0,0 @@ -# Effects - -_**Definition:**_ - -> An **effect** is a representation used by actions to interact with some external process. - -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. - -**_Signature:_** - -```elm -Effect : EffecterFn | [EffecterFn, Payload] -``` - -**_Naming Recommendation:_** - -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. - -## Using Effects - -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. - -```js -import { log } from "./fx" - -// Action : (State) -> [NextState, ...Effects] -const SayHi = (state) => [ - { ...state, value: state.value + 1 }, - log("hi"), - log("there"), -] - -// ... - -h("button", { onclick: SayHi }, text("Say Hi")) -``` - -Actions can of course receive payloads and use effects simultaneously. - -```js -// Action : (State, Payload) -> [NextState, ...Effects] -const SayBye = (state, amount) => [ - { ...state, value: state.value + amount }, - log("bye"), -] - -// ... - -h("button", { onclick: [SayBye, 1] }, text("Bye")) -``` - -## Excluding Effects - -If you don't include any effects in the return array then only the state transition happens. - -Here, `OnlyIncrement` both behaves and is used similarly to `Increment` [shown here](actions.md#actual-state-transition): - -```js -// Action : (State) -> [NextState] -const OnlyIncrement = (state) => [{ ...state, value: state.value + 1 }] - -// ... - -h("button", { onclick: OnlyIncrement }, text("+")) -``` - -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. - -For example, compare this: - -```js -const DoIt = (state) => { - let transition = { ...state, value: "MacGuffin" } - if (state.eating) { - transition = [transition, log("eating")] - } - if (state.drinking) { - transition = Array.isArray(transition) - ? [...transition, log("drinking")] - : [transition, log("drinking")] - } - return transition -} -``` - - - -with this: - -```js -const DoItBetter = (state) => { - let transition = [{ ...state, value: "MacGuffin" }] - if (state.eating) { - transition = [...transition, log("eating")] - } - if (state.drinking) { - transition = [...transition, log("drinking")] - } - return transition -} -``` - -Admittedly, these examples are a bit contrived but the latter is less complex. - -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. - -```js -const DoItBest = (state) => [ - { ...state, value: "MacGuffin" }, - state.eating && log("eating"), - state.drinking && log("drinking"), -] -``` - -## Defining Effects - -Syntactically speaking, an effect takes the form of a tuple containing its [effecter](#effecters) and any associated data. - -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. - -```js -const massFx = (data) => [runNormandy, data] -``` - - - -## Effecters - -**_Definition:_** - -> An **effecter** is the function that actually carries out an effect. - -**_Signature:_** - -```elm -EffecterFn : (DispatchFn, Payload?) -> void -``` - -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. - -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. - -To demonstrate this approach take this ill-formed effecter for example: - -```js -// This effecter is ill-formed. -const runHarvest = (dispatch, _payload) => { - const tiberium = document.getElementById("tiberium") - dispatch((state) => ({ ...state, tiberium })) -} -``` - - - -Sure it runs, but it's also coupled to our app's state and the element ID being referenced is also hard-coded. - -Let's address this by first decoupling our callback action from the effecter by leveraging our ability to give the effecter a payload: - -```js -const runHarvest = (dispatch, payload) => - dispatch(payload.action, document.getElementById("tiberium")) -``` - -Let's further utilize our payload by using it to pass in data our effecter needs to work: - -```js -const runHarvest = (dispatch, payload) => - dispatch(payload.action, document.getElementById(payload.id)) -``` - -Finally, we should rename the effecter to reflect its generic nature: - -```js -const runGetElement = (dispatch, payload) => - dispatch(payload.action, document.getElementById(payload.id)) -``` - -A well-formed effecter is as generic as it can be. - -### Synchronization - -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. - -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). - -Let's see an example of an ill-formed asynchronous effecter: - -```js -// This effecter is ill-formed. -const runBrotherhood = async (dispatch, payload) => { - const response = await fetch(payload.lookForKaneHere) - const kaneLives = response.json() - requestAnimationFrame(() => { - dispatch((state) => ({ - ...state, - message: kaneLives ? "One vision! One purpose!" : "", - })) - }) -} -``` - - - -Now let's see a more well-formed asynchronous effecter: - -```js -const runSimpleFetch = async (dispatch, payload) => { - const response = await fetch(payload.url) - requestAnimationFrame(() => dispatch(payload.action, response.json())) -} -``` - -### Custom Events - -The ideal scenario to use custom effects is when your Hyperapp application needs to communicate with a legacy app via custom events. - -We can have our Hyperapp application use a custom effect for triggering custom events. - -```js -// ./fx.js - -const runEmit = (_dispatch, payload) => - dispatchEvent(new CustomEvent(payload.type, { detail: payload.detail })) - -export const emit = (type, detail) => [runEmit, { type, detail }] -``` - -```js -import { h, text, app } from "hyperapp" -import { emit } from "./fx" - -app({ - view: () => - h("main", {}, [ - h( - "button", - { - onclick: (state) => [ - state, - emit("outgoing", { message: "hello" }) - ], - }, - text("Send greetings") - ), - ]), - node: document.querySelector("main"), -}) -``` diff --git a/docs/architecture/flowchart.md b/docs/architecture/flowchart.md deleted file mode 100644 index 3c8218b74..000000000 --- a/docs/architecture/flowchart.md +++ /dev/null @@ -1,29 +0,0 @@ -```mermaid -graph TD - init("     init     ") - init --- |"init:{state}
init:[state, effect(s)]
"|j0[ ] --- j1[ ] -->nextState - init --> |"init:Action
init:[Action, payload?]"|Action - - domevent("DOM/synthetic 
events
 (click/myevent) ") --> viewEvent - viewEvent(("    view    
event")) --> Action - externalEvents("global/external
processes
 (window resize) ") --> subscription - subscription(("subscription")) -->Action - - Action[" Action 
(state change)"] -->|"OtherAction
 [OtherAction, payload?]
"|Action - Action --- |"NextState
[NextState, ...Effects]"|j2[ ] ---> nextState - - nextState(("  next  
state")) --- j3[ ] - j3 --> |"view(state)"|newDom("   (re)render  
    DOM   ") - j3 --> |"subscriptions(state)"|recalcSubs(recalc
subscriptions) - j3 --> |"(dispatch, Payload?) -> void"|effect - effect("Effects
(impure code)") -.-> |"dispatch"|dispatchAction("Action") - - style j0 height:1px; - style j0 width:1px; - style j1 height:1px; - style j1 width:1px; - style j2 height:1px; - style j2 width:1px; - style j3 height:1px; - style j3 width:1px; -``` diff --git a/docs/architecture/state.md b/docs/architecture/state.md deleted file mode 100644 index 499874e05..000000000 --- a/docs/architecture/state.md +++ /dev/null @@ -1,61 +0,0 @@ -# State - -**_Definition:_** - -> 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. - -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. - ---- - -## Assignment - -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. - -### State Transitions - -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). - -## State With Effects - -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. - -So, Hyperapp will apply the state first and then will run the effects in the order they appear. - -```js -[state, log(state), log("MOAR")] -``` - -### Array State - -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. - -```js -[["a", "b", "c"]] -``` - -The actions page also [talks about it](actions.md#transitioning-array-state). - ---- - -## Visualization - -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. - ---- - -## Other Considerations - -### Serializability - -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. - -### State Type - -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. - -### Direct Mutation - -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. - -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. diff --git a/docs/architecture/subscriptions.md b/docs/architecture/subscriptions.md deleted file mode 100644 index 56fef80e4..000000000 --- a/docs/architecture/subscriptions.md +++ /dev/null @@ -1,240 +0,0 @@ -# Subscriptions - -**_Definition:_** - -> A **subscription** function represents a dependency your app has on some external process. - -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. - -**_Signature:_** - -```elm -Subscription : [SubscriberFn, Payload?] -``` - -**_Naming Recommendation:_** - -Subscriptions are recommended to be named in `camelCase` prefixed by `on`, for instance `onEvery` or `onMouseEnter` in order to reflect their event handling character. - ---- - -## Using Subscriptions - -Subscriptions are setup and managed through the [`subscriptions:`](../api/app.md#subscriptions) property used with [`app()`](../api/app.md) when instantiating your app. - -```js -import { onEvery } from "./time" - -// ... - -app({ - init: { delayInMilliseconds: 1000 }, - subscriptions: (state) => [ - // Dispatch `RequestResource` every `delayInMilliseconds`. - onEvery(state.delayInMilliseconds, RequestResource), - ], -}) -``` - -You can control if subscriptions are active or not by using boolean values. - -```js -app({ - subscriptions: (state) => [ - state.toBe && onEvery(state.delay, ThatIsTheQuestion), - state.notToBe || onEvery(state.delay, ThatIsTheQuestion), - ], -}) -``` - - - -### Subscriptions Array Format - -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. - -### Subscription Lifecycle - -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. - -| Previously Active | Currently Active | What Happens | -| ----------------- | ---------------- | -------------------------------------------- | -| no | no | Nothing. | -| no | yes :100: | Subscription starts up. | -| yes :100: | no | Subscription shuts down and gets cleaned up. | -| yes :100: | yes :100: | Subscription remains active. | - -To restart a subscription you must first deactivate it and then, during the next state change, reactivate it. - ---- - -## Custom Subscriptions - -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. - -### Subscribers - -**_Definition:_** - -> A **subscriber** is a function which implements an active subscription. - -**_Signature:_** - -```elm -SubscriberFn : (DispatchFn, Payload?) -> CleanupFn -``` - -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. - -Subscribers can be given a data `payload` for their use. - -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. - -### Example - -Let's say we're embedding our Hyperapp application within a legacy vanilla JavaScript project. - -Somewhere within the legacy portion of our project a custom event gets emitted: - -```js -// Somewhere in our legacy app... - -const triggerSpecialEvent = () => { - dispatchEvent(new CustomEvent("secret", { detail: 42 })) -} - -// ... - -triggerSpecialEvent() -``` - - - -Our embedded Hyperapp application will need a custom subscription to be able to deal with custom events: - -```js -// ./subs.js - -const listenToEvent = (dispatch, props) => { - const listener = (event) => - requestAnimationFrame(() => dispatch(props.action, event.detail)) - - addEventListener(props.type, listener) - return () => removeEventListener(props.type, listener) -} - -export const listen = (type, action) => [listenToEvent, { type, action }] -``` - -In case you're wondering why `listenToEvent()`'s listener is using `requestAnimationFrame`, it has to do with [synchronization](actions.md#synchronization). - -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: - -```js -import { h, text, app } from "hyperapp" -import { listen } from "./subs" - -const Response = (state, payload) => ({ ...state, payload }) - -export const myApp = (node) => - app({ - init: () => ({ payload: null }), - view: ({ payload }) => - h("main", {}, [ - payload && h("p", {}, text(`Payload received: ${JSON.stringify(payload)}`)), - ]), - subscriptions: () => [listen("secret", Response)], - node: document.querySelector("main"), - }) -``` - ---- - -## Other Considerations - -### Destructuring Gotcha - -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. - -```js -const listenToEvent = (dispatch, props) => { - const listener = (event) => - requestAnimationFrame(() => dispatch(props.action, event.detail)) - - addEventListener(props.type, listener) - return () => { - removeEventListener(props.type, listener) - dispatch(props.action, "") - } -} -``` - -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: - -```js -const listenToEvent = (dispatch, { action, type }) => { - const listener = (event) => - requestAnimationFrame(() => dispatch(action, event.detail)) - - addEventListener(type, listener) - return () => { - removeEventListener(type, listener) - dispatch(action, "cleaned-up") // <-- uh, oh! - } -} -``` - -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. - -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. - -```js -// ./fx.js - -const runPreventDefault = (dispatch, payload) => { - payload.event.preventDefault() - dispatch(payload.action) -} - -export const preventDefault = (action, event) => - [runPreventDefault, { action, event }] -``` - -```js -// ./actions.js - -import { preventDefault } from "./fx" - -export const skipDefault = (action) => (state, event) => - [state, preventDefault(action, event)] - -export const MyAction = (state) => ({ ...state }) -``` - -```js -// ./subs.js - -const subOnThatThing = (dispatch, props) => { - // Do stuff... -} - -export const onThatThing = (action, props) => [subOnThatThing, { ...props, action }] -``` - -```js -// ./main.js - -import { onThatThing } from "./subs" -import { skipDefault } from "./actions" - -app({ - subscriptions: (state) => [ - state.isActive && - onThatThing(skipDefault(MyAction), { - foo: 42 + state.index, - }), - ], -}) -``` - -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. diff --git a/docs/architecture/views.md b/docs/architecture/views.md deleted file mode 100644 index 6106815b8..000000000 --- a/docs/architecture/views.md +++ /dev/null @@ -1,208 +0,0 @@ -# Views - -**_Definition:_** - -> A **view** is a declarative description of what should get rendered and is usually influenced by the current [state](state.md). - -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. - -**_Signature:_** - -```elm -View : (State) -> VNode -``` - ---- - -## Describing Views - -The [`h()`](../api/h.md), [`text()`](../api/text.md), and [`memo()`](../api/memo.md) functions are the building blocks of your views. - -[`h()`](../api/h.md) not only describes what HTML elements are being used but also what [actions](actions.md) are wired up if any. - -```js -const view = (state) => - h( - "button", - { - class: { "calling-acid-burn": state.beingFramed }, - onclick: FindThatDisk, - }, - text("It's in that place where I put that thing that time.") - ) -``` - - - -[`text()`](../api/text.md) just creates text nodes so the views it can create on its own are necessarily simplistic. - -```js -const view = () => text("Go home and be a family man!") -``` - - - -[`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. - -```js -const view = (state) => memo(scenicView, state.vacationSpot) -``` - - - ---- - -## Components - -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. - -**_Definition:_** - -> A **component** in Hyperapp can either be a subview or some other function that generates VNodes. - -**_Signature:_** - -```elm -Component : (GlobalState | PartialState) -> VNode | [...VNodes] -``` - - - -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. - -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: - -```js -// Component : (GlobalState) -> VNode -const coinsDisplay = (state) => - h("div", { class: "coins-display" }, text(state.coins)) - -// Component : (PartialState) -> VNode -const questionBlock = (opened) => - opened - ? h("button", { class: "question-block opened" }, text("?")) - : h( - "button", - { - class: "question-block", - onclick: [ - HitBlockFromBottom, - { revealItem: "beanstalk" }, - ], - }, - text("?") - ) - -// Component : (GlobalState) -> VNode -const level = (state) => - h("div", { class: "level" }, [ - coinsDisplay(state), - questionBlock(state.onlyQuestionBlockOpened), - ]) -``` - - - -**_Naming Recommendation:_** - -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`. - -### Components Returning Multiple VNodes - -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. - -```js -// Component : () -> [...VNodes] -const finishingMoveOptions = () => [ - h("button", { onclick: FinishHim }, text("Fatality")), - h("button", { onclick: FinishHimAsAnAnimal }, text("Animality")), - h("button", { onclick: TurnHimIntoABaby }, text("Babality")), - h("button", { onclick: BefriendHim }, text("Friendship")), -] - -const view = () => h("div", {}, [ - h("em", {}, text("Finish them:")), - ...finishingMoveOptions(), -]) -``` - - - ---- - -## Using Views - -### Top-Level View - -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. - -```js -app({ - // ... - view: (state) => - h("main", {}, [ - earthrealm(state), - edenia(state), - ]), -}) -``` - - - -### Conditional Rendering - -Elements of a view can be shown or hidden conditionally. - -```js -const view = (state) => - h("div", {}, [ - state.flying && h("div", {}, text("Flying")), - state.notSwimming || h("div", {}, text("Swimming")), - ]) -``` - -### Recycling - -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. - ---- - -## Virtual DOM - -**_Definition:_** - -> 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. - -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. - -We've already seen how [`h()`](../api/h.md), [`text()`](../api/text.md), and [`memo()`](../api/memo.md) all return different types of VNodes. - -### Patching the DOM - -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. - -### Keys - -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. - -Since Hyperapp can't know for sure it must assume everything had changed requiring a full render every time. - -For an example, look at the [`key:`](../api/h.md#key) documentation for [`h()`](../api/h.md). - -### Memoization - -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. - -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. - -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. - -For an example, look at the documentation for [`memo()`](../api/memo.md#example). - -#### Performance - -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. - -Memoization was designed for nodes that don't need to update at all or just occasionally. - -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. diff --git a/docs/concepts/README.md b/docs/concepts/README.md new file mode 100644 index 000000000..07dcde782 --- /dev/null +++ b/docs/concepts/README.md @@ -0,0 +1,8 @@ +## Concepts + +* [Virtual Nodes](vnodes.md) +* [Keys](keys.md) +* [Components](components.md) +* [Lifecycle Events](lifecycle-events.md) +* [Sanitation](sanitation.md) +* [Hydration](hydration.md) diff --git a/docs/concepts/components.md b/docs/concepts/components.md new file mode 100755 index 000000000..eeb9f66dd --- /dev/null +++ b/docs/concepts/components.md @@ -0,0 +1,67 @@ +# Components + +A component is a pure function that returns a [virtual node](vnodes.md). Unlike a view, they are not pre-wired to your application state or actions. Components are reusable blocks of code that encapsulate markup, styles and behaviors that belong together. + +[Try it Online](https://codepen.io/hyperapp/pen/zNxRLy) + +```jsx +import { h, app } from "hyperapp" +import { state, actions } from "./todos" + +const TodoItem = ({ id, value, done, toggle }) => ( +
  • + toggle({ + value: done, + id: id + }) + } + > + {value} +
  • +) + +const view = (state, actions) => ( +
    +

    Todo

    +
      + {state.todos.map(({ id, value, done }) => ( + + ))} +
    +
    +) + +const main = app(state, actions, view, document.body) +``` + +If you don't know all the properties that you want to place in a component ahead of time, you can use the [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator). + +```jsx +const TodoList = ({ todos, toggle }) => ( + +) +``` + +Note that when using JSX, components [must be capitalized](https://facebook.github.io/react/docs/jsx-in-depth.html#user-defined-components-must-be-capitalized) or contain a `.` in their name. + +## Children Composition + +Components receive children elements in the second argument. + +```jsx +const Box = ({ color }, children) => ( +
    {children}
    +) +``` + +This lets you and other components pass arbitrary children down to them. + +```jsx +const HelloBox = ({ name }) => ( + +

    Hello, {name}!

    +
    +) +``` diff --git a/docs/concepts/hydration.md b/docs/concepts/hydration.md new file mode 100755 index 000000000..e764aec84 --- /dev/null +++ b/docs/concepts/hydration.md @@ -0,0 +1,21 @@ +# Hydration + +Hyperapp works transparently with SSR and pre-rendered HTML, enabling SEO optimization and improving your sites time-to-interactive. The process consists of serving a fully pre-rendered page together with your application. + +```html + + + + + + +
    +

    0

    + + +
    + + +``` + +Then instead of throwing away the server-rendered markdown, we'll turn your DOM nodes into an interactive application out of the box! diff --git a/docs/concepts/keys.md b/docs/concepts/keys.md new file mode 100755 index 000000000..f74f70be9 --- /dev/null +++ b/docs/concepts/keys.md @@ -0,0 +1,28 @@ +# Keys + +Keys help identify which nodes were added, changed or removed from a list when a view is rendered. A key must be unique among sibling-nodes. + +```jsx +const ImageGallery = ({ images }) => + images.map(({ hash, url, description }) => ( +
  • + {description} +
  • + )) +``` + +By setting the `key` property on a virtual node, you declare that the node should correspond to a particular DOM element. This allow us to re-order the element into its new position, if the position changed, rather than risk destroying it. + +Don't use an array index as key, if the index also specifies the order of siblings. If the position and number of items in a list is fixed, it will make no difference, but if the list is dynamic, the key will change every time the tree is rebuilt. + +```jsx +const PlayerList = ({ players }) => + players + .slice() + .sort((player, nextPlayer) => nextPlayer.score - player.score) + .map(player => ( +
  • + +
  • + )) +``` diff --git a/docs/concepts/lifecycle-events.md b/docs/concepts/lifecycle-events.md new file mode 100644 index 000000000..9d8ae3b07 --- /dev/null +++ b/docs/concepts/lifecycle-events.md @@ -0,0 +1,68 @@ +# Lifecycle Events + +You can be notified when a virtual node is created, updated or removed via lifecycle events. Use them for animation, data fetching and cleaning up resources. + +## oncreate + +This event is fired after the element is created and attached to the DOM. Use it to manipulate the DOM node directly, make a network request, create slide/fade in animation, etc. + +```jsx +const Textbox = ({ placeholder }) => ( + element.focus()} + /> +) +``` + +## onupdate + +This event is fired every time we update the element attributes. Use `oldProps` inside the event handler to check if any attributes changed or not. + +```jsx +const Textbox = ({ placeholder }) => ( + { + if (oldProps.placeholder !== placeholder) { + // Handle changes here! + } + }} + /> +) +``` + +## onremove + +This event is fired before the element is removed from the DOM. Use it to create slide/fade out animations. Call `done` inside the function to remove the element. + +This event is not called in its child elements. + +```jsx +const MessageWithFadeout = ({ title }) => ( +
    fadeout(element).then(done)}> +

    {title}

    +
    +) +``` + +## ondestroy + +This event is fired after the element has been removed from the DOM, either directly or as a result of a parent being removed. Use it for invalidating timers, canceling a network request, removing global events listeners, etc. + +```jsx +const Camera = ({ onerror }) => ( +