Merge pull request #117 from yoshuawuyts/restructure-docs
docs: restructure
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
## Thanks for contributing to Choo!
|
||||||
|
:steam_locomotive::train::train::train::train::train:
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
Browser tests can be run with the right credentials via the `npm run
|
||||||
|
test:browser` command. This will be run automatically when `npm version` is
|
||||||
|
executed.
|
||||||
|
|
||||||
|
You may skip the tests by providing `SKIP_TEST=true` when running the version
|
||||||
|
command.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
### Expected behavior
|
||||||
|
Write here.
|
||||||
|
|
||||||
|
### Actual behavior
|
||||||
|
Write here.
|
||||||
|
|
||||||
|
### Steps to reproduce behavior
|
||||||
|
Write here.
|
||||||
@@ -50,7 +50,19 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<div align="center">
|
||||||
|
<h3>
|
||||||
|
<a href="https://github.com/yoshuawuyts/choo-handbook">
|
||||||
|
Handbook
|
||||||
|
</a>
|
||||||
|
<span>|</span>
|
||||||
|
Packages
|
||||||
|
<span>|</span>
|
||||||
|
<a href="https://github.com/yoshuawuyts/choo/blob/master/.github/CONTRIBUTING.md">
|
||||||
|
Contributing
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<sub>The little framework that could. Built with ❤︎ by
|
<sub>The little framework that could. Built with ❤︎ by
|
||||||
@@ -60,32 +72,18 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Table of Contents
|
<h2>Table of Contents</h2>
|
||||||
- [Features](#features)
|
<details>
|
||||||
- [Demos](#demos)
|
<summary>Table of Contents</summary>
|
||||||
- [Getting started](#getting-started)
|
<li><a href="#features">Features</a></li>
|
||||||
- [Concepts](#concepts)
|
<li><a href="#demos">Demos</a></li>
|
||||||
- [Models](#models)
|
<li><a href="#example">Example</a></li>
|
||||||
- [Actions](#actions)
|
<li><a href="#concepts">Concepts</a></li>
|
||||||
- [Effects](#effects)
|
<li><a href="#api">API</a></li>
|
||||||
- [Subscriptions](#subscriptions)
|
<li><a href="#faq">FAQ</a></li>
|
||||||
- [Router](#router)
|
<li><a href="#installation">Installation</a></li>
|
||||||
- [Views](#views)
|
<li><a href="#see-also">See Also</a></li>
|
||||||
- [Common actions](#common-actions)
|
</details>
|
||||||
- [HTTP](#http)
|
|
||||||
- [Server sent events](#server-sent-events-sse)
|
|
||||||
- [Keyboard](#keyboard)
|
|
||||||
- [Websockets](#websockets)
|
|
||||||
- [Forms](#forms)
|
|
||||||
- [Links](#links)
|
|
||||||
- [Rendering in Node](#rendering-in-node)
|
|
||||||
- [API](#api)
|
|
||||||
- [Errors](#errors)
|
|
||||||
- [FAQ](#faq)
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [See Also](#see-also)
|
|
||||||
- [License](#license)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- __minimal size:__ weighing `5kb`, `choo` is a tiny little framework
|
- __minimal size:__ weighing `5kb`, `choo` is a tiny little framework
|
||||||
@@ -111,67 +109,16 @@
|
|||||||
_note: If you've built something cool using `choo` or are using it in
|
_note: If you've built something cool using `choo` or are using it in
|
||||||
production, we'd love to hear from you!_
|
production, we'd love to hear from you!_
|
||||||
|
|
||||||
## Getting started
|
## Example
|
||||||
Let's create an input box that changes the content of a textbox in real time.
|
Let's create an input box that changes the content of a textbox in real time.
|
||||||
[Click here to see the final app](http://requirebin.com/?gist=e589473373b3100a6ace29f7bbee3186).
|
[Click here to see the app running](http://requirebin.com/?gist=e589473373b3100a6ace29f7bbee3186).
|
||||||
|
|
||||||
First we import `choo` and create a new instance:
|
|
||||||
```js
|
|
||||||
const choo = require('choo')
|
|
||||||
const html = require('choo/html')
|
|
||||||
const app = choo()
|
|
||||||
```
|
|
||||||
|
|
||||||
Then we define a model. We set an initial value of `state` and a `reducer` that
|
|
||||||
can be called to modify it:
|
|
||||||
```js
|
|
||||||
app.model({
|
|
||||||
state: { title: 'Set the title' },
|
|
||||||
reducers: {
|
|
||||||
update: (data, state) => ({ title: data.value })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Then we create a new view. It has an `h1` tag which displays the current title,
|
|
||||||
and an `<input>` field which sends the current value of the text box on every
|
|
||||||
input:
|
|
||||||
```js
|
|
||||||
const mainView = (params, state, send) => html`
|
|
||||||
<main>
|
|
||||||
<h1>${state.title}</h1>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
oninput=${(e) => send('update', { value: e.target.value })}>
|
|
||||||
</main>
|
|
||||||
`
|
|
||||||
```
|
|
||||||
|
|
||||||
_Note_: if an `id` property is defined on the outer-most element it will be
|
|
||||||
replaced.
|
|
||||||
|
|
||||||
We then bind the view to the `/` route on our application
|
|
||||||
```js
|
|
||||||
app.router((route) => [
|
|
||||||
route('/', mainView)
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
And then start the app and append it to the DOM. You can now run it and [see it
|
|
||||||
in action!](http://requirebin.com/?gist=e589473373b3100a6ace29f7bbee3186)
|
|
||||||
```js
|
|
||||||
const tree = app.start()
|
|
||||||
document.body.appendChild(tree)
|
|
||||||
```
|
|
||||||
|
|
||||||
And all together now:
|
|
||||||
```js
|
```js
|
||||||
const choo = require('choo')
|
const choo = require('choo')
|
||||||
const html = require('choo/html')
|
const html = require('choo/html')
|
||||||
const app = choo()
|
const app = choo()
|
||||||
|
|
||||||
app.model({
|
app.model({
|
||||||
state: { title: 'Set the title' },
|
state: { title: 'Not quite set yet' },
|
||||||
reducers: {
|
reducers: {
|
||||||
update: (data, state) => ({ title: data.value })
|
update: (data, state) => ({ title: data.value })
|
||||||
}
|
}
|
||||||
@@ -179,7 +126,7 @@ app.model({
|
|||||||
|
|
||||||
const mainView = (params, state, send) => html`
|
const mainView = (params, state, send) => html`
|
||||||
<main>
|
<main>
|
||||||
<h1>${state.title}</h1>
|
<h1>Title: ${state.title}</h1>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
oninput=${(e) => send('update', { value: e.target.value })}>
|
oninput=${(e) => send('update', { value: e.target.value })}>
|
||||||
@@ -194,6 +141,44 @@ const tree = app.start()
|
|||||||
document.body.appendChild(tree)
|
document.body.appendChild(tree)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
And to run it, save it as `client.js` and run with [budo][budo] and
|
||||||
|
[es2020][es2020]:
|
||||||
|
```sh
|
||||||
|
$ budo 'client.js' -p 8080 --open -- -t es2020
|
||||||
|
```
|
||||||
|
|
||||||
|
And to save to static files for deployment, open a new terminal and do:
|
||||||
|
```bash
|
||||||
|
$ mkdir -p 'dist/'
|
||||||
|
$ curl 'localhost:8080' > 'dist/index.html'
|
||||||
|
$ curl 'localhost:8080/bundle.js' > 'dist/bundle.js'
|
||||||
|
```
|
||||||
|
All just a couple of shell commands and `.js` files, no grandiose boilerplate
|
||||||
|
needed.
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
We believe programming should be fun and light, not stern and stressful. It's
|
||||||
|
cool to be cute; using serious words without explaining them doesn't make for
|
||||||
|
better results - if anything it scares people off. We don't want to be scary,
|
||||||
|
we want to be nice and fun, and then _casually_ be the best choice around.
|
||||||
|
_Real casually._
|
||||||
|
|
||||||
|
We believe frameworks should be disposable, and components recyclable. We don't
|
||||||
|
like the current state of web development where walled gardens jealously
|
||||||
|
compete with one another. We want you to be free, not shackled to a damp
|
||||||
|
dungeon wall. By making the DOM the lowest common denominator, switching from
|
||||||
|
one framework to another becomes frictionless. Components should run anywhere
|
||||||
|
that has a DOM, regardless of the framework. `choo` is modest in its design; we
|
||||||
|
don't believe it will be top of the class forever, so we've made it as easy to
|
||||||
|
toss out as it is to pick up.
|
||||||
|
|
||||||
|
We don't believe that bigger is better. Big APIs, big dependencies, large file
|
||||||
|
sizes - we see them as omens of impending userland complexity. We want
|
||||||
|
everyone on a team, no matter the size, to fully understand how an application
|
||||||
|
is laid out. And once an application is built, we want it to be small,
|
||||||
|
performant and easy to reason about. All of which makes for easy to debug code,
|
||||||
|
better results and super smiley faces.
|
||||||
|
|
||||||
## Concepts
|
## Concepts
|
||||||
`choo` cleanly structures internal data flow, so that all pieces of logic can
|
`choo` cleanly structures internal data flow, so that all pieces of logic can
|
||||||
be combined into a nice, cohesive machine. Internally all logic lives within
|
be combined into a nice, cohesive machine. Internally all logic lives within
|
||||||
@@ -238,7 +223,7 @@ application logic. This is the _unidirectional_ architecture of `choo`.
|
|||||||
- __views:__ take `state` and returns a new `DOM tree` that is rendered in the
|
- __views:__ take `state` and returns a new `DOM tree` that is rendered in the
|
||||||
browser
|
browser
|
||||||
|
|
||||||
## Models
|
### Models
|
||||||
`models` are objects that contain initial `state`, `subscriptions`, `effects`
|
`models` are objects that contain initial `state`, `subscriptions`, `effects`
|
||||||
and `reducers`. They're generally grouped around a theme (or domain, if you
|
and `reducers`. They're generally grouped around a theme (or domain, if you
|
||||||
like). To provide some sturdiness to your `models`, they can either be
|
like). To provide some sturdiness to your `models`, they can either be
|
||||||
@@ -273,7 +258,7 @@ and has access to the full application state. Try and keep the logic in these
|
|||||||
bulk of your logic will safely shielded, with only a few points touching every
|
bulk of your logic will safely shielded, with only a few points touching every
|
||||||
part of your application.
|
part of your application.
|
||||||
|
|
||||||
## Effects
|
### Effects
|
||||||
Side effects are done through `effects` declared in `app.model()`. Unlike
|
Side effects are done through `effects` declared in `app.model()`. Unlike
|
||||||
`reducers` they cannot modify the state by returning objects, but get a
|
`reducers` they cannot modify the state by returning objects, but get a
|
||||||
callback passed which is used to emit `actions` to handle results. Use effects
|
callback passed which is used to emit `actions` to handle results. Use effects
|
||||||
@@ -288,7 +273,7 @@ A typical `effect` flow looks like:
|
|||||||
4. When the async call is done, either a success or error action is emitted
|
4. When the async call is done, either a success or error action is emitted
|
||||||
5. A reducer catches the action and updates the state
|
5. A reducer catches the action and updates the state
|
||||||
|
|
||||||
## Subscriptions
|
### Subscriptions
|
||||||
Subscriptions are a way of receiving data from a source. For example when
|
Subscriptions are a way of receiving data from a source. For example when
|
||||||
listening for events from a server using `SSE` or `Websockets` for a
|
listening for events from a server using `SSE` or `Websockets` for a
|
||||||
chat app, or when catching keyboard input for a videogame.
|
chat app, or when catching keyboard input for a videogame.
|
||||||
@@ -307,7 +292,7 @@ app.model({
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Router
|
### Router
|
||||||
The `router` manages which `views` are rendered at any given time. It also
|
The `router` manages which `views` are rendered at any given time. It also
|
||||||
supports rendering a default `view` if no routes match.
|
supports rendering a default `view` if no routes match.
|
||||||
|
|
||||||
@@ -333,7 +318,7 @@ using `send('app:location', { location: href })`. This will not work from
|
|||||||
within namespaced `models`, and usage should preferably be kept to a minimum.
|
within namespaced `models`, and usage should preferably be kept to a minimum.
|
||||||
Changing views all over the place tends to lead to messiness.
|
Changing views all over the place tends to lead to messiness.
|
||||||
|
|
||||||
## Views
|
### Views
|
||||||
Views are pure functions that return a DOM tree for the router to render. They’re passed the current state, and any time the state changes they’re run again with the new state.
|
Views are pure functions that return a DOM tree for the router to render. They’re passed the current state, and any time the state changes they’re run again with the new state.
|
||||||
|
|
||||||
Views are also passed the `send` function, which they can use to dispatch actions that can update the state. For example, the DOM tree can have an `onclick` handler that dispatches an `add` action.
|
Views are also passed the `send` function, which they can use to dispatch actions that can update the state. For example, the DOM tree can have an `onclick` handler that dispatches an `add` action.
|
||||||
@@ -349,316 +334,75 @@ const view = (params, state, send) => {
|
|||||||
```
|
```
|
||||||
In this example, when the `Add` button is clicked, the view will dispatch an `add` action that the model’s `add` reducer will receive. [As seen above](#models), the reducer will add an item to the state’s `todos` array. The state change will cause this view to be run again with the new state, and the resulting DOM tree will be used to [efficiently patch the DOM](#does-choo-use-a-virtual-dom).
|
In this example, when the `Add` button is clicked, the view will dispatch an `add` action that the model’s `add` reducer will receive. [As seen above](#models), the reducer will add an item to the state’s `todos` array. The state change will cause this view to be run again with the new state, and the resulting DOM tree will be used to [efficiently patch the DOM](#does-choo-use-a-virtual-dom).
|
||||||
|
|
||||||
## Common Actions
|
|
||||||
### HTTP
|
|
||||||
`choo` ships with a built-in [`http` module](https://github.com/Raynos/xhr)
|
|
||||||
that weighs only `2.4kb`:
|
|
||||||
```js
|
|
||||||
const http = require('choo/http')
|
|
||||||
const choo = require('choo')
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
effects: {
|
|
||||||
'app:error': (state, event) => console.error(`error: ${event.payload}`),
|
|
||||||
'app:print': (state, event) => console.log(`http: ${event.payload}`),
|
|
||||||
'http:get_json': getJson,
|
|
||||||
'http:post_json': postJson,
|
|
||||||
'http:delete': httpDelete
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function getJson (state, data, send) {
|
|
||||||
http.get('/my-endpoint', { json: true }, function (err, res, body) {
|
|
||||||
if (err) return send('app:error', { payload: err.message })
|
|
||||||
if (res.statusCode !== 200 || !body) {
|
|
||||||
return send('app:error', { payload:'something went wrong' })
|
|
||||||
}
|
|
||||||
send('app:print', { payload: body })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function postJson (state, data, send) {
|
|
||||||
const body = { foo: 'bar' }
|
|
||||||
http.post('/my-endpoint', { json: body }, function (err, res, body) {
|
|
||||||
if (err) return send('app:error', { payload: err.message })
|
|
||||||
if (res.statusCode !== 200 || !body) {
|
|
||||||
return send('app:error', { payload:'something went wrong' })
|
|
||||||
}
|
|
||||||
send('app:print', { payload: body })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function httpDelete (state, data, send) {
|
|
||||||
const body = { foo: 'bar' }
|
|
||||||
http.del('/my-endpoint', { json: body }, function (err, res, body) {
|
|
||||||
if (err) return send('app:error', { payload: err.message })
|
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
return send('app:error', { payload:'something went wrong' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Note that `http` only runs in the browser to prevent accidental requests when
|
|
||||||
rendering in Node. For more details view the [`raynos/xhr`
|
|
||||||
documentation](https://github.com/Raynos/xhr).
|
|
||||||
|
|
||||||
### Server Sent Events (SSE)
|
|
||||||
[Server Sent Events (SSE)][sse] allow servers to push data to the browser.
|
|
||||||
They're the unidirectional cousin of `websockets` and compliment `HTTP`
|
|
||||||
brilliantly. To enable `SSE`, create a new `EventSource`, point it at a local
|
|
||||||
uri (generally `/sse`) and setup a `subscription`:
|
|
||||||
```js
|
|
||||||
const stream = new document.EventSource('/sse')
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
subscriptions: [
|
|
||||||
function (send) {
|
|
||||||
stream.onerror = (e) => send('app:error', { payload: JSON.stringify(e) })
|
|
||||||
stream.onmessage = (e) => send('app:print', { payload: e.data })
|
|
||||||
}
|
|
||||||
],
|
|
||||||
effects: {
|
|
||||||
'sse:close': () => stream.close(),
|
|
||||||
'app:error': (state, event) => console.error(`error: ${event.payload}`),
|
|
||||||
'app:print': (state, event) => console.log(`sse: ${event.payload}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
This code does not handle reconnects, server timeouts, exponential backoff and
|
|
||||||
queueing data. You might want to use a package from `npm` or [write your
|
|
||||||
own][sse-reconnect] if you're building something for production.
|
|
||||||
|
|
||||||
### Keyboard
|
|
||||||
Most browsers have [basic support for keyboard events][keyboard-support]. To
|
|
||||||
capture keyboard events, setup a `subscription`:
|
|
||||||
```js
|
|
||||||
app.model({
|
|
||||||
namespace: 'input',
|
|
||||||
subscriptions: [
|
|
||||||
function (send) {
|
|
||||||
document.addEventListener(
|
|
||||||
'keypress',
|
|
||||||
(e) => send('input:print', { payload: e.keyCode })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
],
|
|
||||||
effects: {
|
|
||||||
print: (state) => console.log(`pressed key: ${state.payload}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### WebSockets
|
|
||||||
[WebSockets][ws] allow for bidirectional communication between servers and
|
|
||||||
browsers:
|
|
||||||
```js
|
|
||||||
const socket = new document.WebSocket('ws://localhost:8081')
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
subscriptions: [
|
|
||||||
function (send) {
|
|
||||||
socket.onerror = (e) => send('app:error', { payload: JSON.stringify(e) })
|
|
||||||
socket.onmessage = (e) => send('app:print', { payload: e.data })
|
|
||||||
}
|
|
||||||
],
|
|
||||||
effects: {
|
|
||||||
'ws:close': () => socket.close(),
|
|
||||||
'ws:send': (state, event) => socket.send(JSON.stringify(event.payload)),
|
|
||||||
'app:error': (state, event) => console.error(`error: ${event.payload}`),
|
|
||||||
'app:print': (state, event) => console.log(`ws: ${event.payload}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
This code does not handle reconnects, server timeouts, exponential backoff and
|
|
||||||
queueing data. You might want to use a package from `npm` or [write your
|
|
||||||
own][ws-reconnect] if you're building something for production.
|
|
||||||
|
|
||||||
### Forms
|
|
||||||
Forms and lists are probably the most used concepts on any page. Together with
|
|
||||||
links they comprise most of what can be done on web pages.
|
|
||||||
```js
|
|
||||||
const choo = require('choo')
|
|
||||||
const http = require('choo/http')
|
|
||||||
const html = require('choo/html')
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
function view (params, state, send) {
|
|
||||||
return html`
|
|
||||||
<form onsubmit=${onSubmit}>
|
|
||||||
<fieldset>
|
|
||||||
<label>username</label>
|
|
||||||
<input type="text" name="username" autofocus>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<label>password</label>
|
|
||||||
<input type="password" name="password">
|
|
||||||
</fieldset>
|
|
||||||
<input type="submit" value="Submit">
|
|
||||||
</form>
|
|
||||||
`
|
|
||||||
|
|
||||||
function onSubmit (event) {
|
|
||||||
send('login', new FormData(event.target))
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
effects: {
|
|
||||||
login: (data, state, send) => {
|
|
||||||
http.post('/login', { body: data }, (err, res, body) => {
|
|
||||||
send('authorize', { payload: body })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.router((route) => [
|
|
||||||
route('/', view)
|
|
||||||
])
|
|
||||||
|
|
||||||
app.start()
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want a form element to be selected when it's loaded, add the
|
|
||||||
[`autofocus`][html-input] property.
|
|
||||||
```js
|
|
||||||
const view = html`
|
|
||||||
<form>
|
|
||||||
<input type="text" autofocus>
|
|
||||||
</form>
|
|
||||||
`
|
|
||||||
```
|
|
||||||
|
|
||||||
### Links
|
|
||||||
In HTML links are represented with the `<a href="/some-location">` tag. By
|
|
||||||
default `choo` enables a `subscription` for all `a` tags on a page. When a link
|
|
||||||
is clicked, the click event is caught, and the value of `href` is passed into
|
|
||||||
the router causing a state change. If you want to disable this behavior, set
|
|
||||||
`app.start({ href: false })`.
|
|
||||||
```js
|
|
||||||
const nav = html`
|
|
||||||
<a href="/">home</a>
|
|
||||||
<a href="/first-link">first link</a>
|
|
||||||
<a href="/second-link">second link</a>
|
|
||||||
`
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rendering in Node
|
|
||||||
Sometimes it's necessary to render code inside of Node; for serving hyper fast
|
|
||||||
first requests, testing or other purposes. Applications that are capable of
|
|
||||||
being rendered in both Node and the browser are called
|
|
||||||
_[isomorphic][isomorphic]_.
|
|
||||||
|
|
||||||
Rendering in Node is slightly different than in the browser. First off, to
|
|
||||||
maintain performance all calls to `subscriptions`, `effects`, and `reducers`
|
|
||||||
are disabled. That means you need to know what the state of your application is
|
|
||||||
going to be _before_ you render it - no cheating!
|
|
||||||
|
|
||||||
Secondly, the `send()` method inside `router` and `view` has been disabled. If
|
|
||||||
you call it your program will crash. Disabling all these things means that your
|
|
||||||
program will render [`O(n)`][big-o], which is super neat. Off to [10.000
|
|
||||||
QPS][qps] we go!
|
|
||||||
|
|
||||||
To render in Node call the `.toString()` method instead of `.start()`. The
|
|
||||||
first argument is the path that should be rendered, the second is the state:
|
|
||||||
```js
|
|
||||||
const http = require('http')
|
|
||||||
const client = require('./client') // path to client entry point
|
|
||||||
http.createServer(function (req, res) {
|
|
||||||
const html = client.toString('/', { message: 'hello server!' })
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
||||||
res.end(html)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
In order to make our `choo` app call `app.start()` in the browser and be
|
|
||||||
`require()`-able in Node, we check if [`module.parent`][module-parent] exists:
|
|
||||||
```js
|
|
||||||
const choo = require('choo')
|
|
||||||
const html = require('choo/html')
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
app.router((route) => [
|
|
||||||
route('/', (params, state, send) => html`
|
|
||||||
<h1>${state.message}</h1>
|
|
||||||
`)
|
|
||||||
])
|
|
||||||
|
|
||||||
if (module.parent) module.exports = app
|
|
||||||
else document.body.appendChild(app.start())
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Rehydration
|
|
||||||
Now that your application is successfully rendering in Node, the next step would
|
|
||||||
be to make it load a JavaScript bundle once has loaded the HTML. To do this we
|
|
||||||
will use a technique called _rehydration_.
|
|
||||||
|
|
||||||
_Rehydration_ is when you take the static, server-rendered version of your
|
|
||||||
application (static HTML, _dehydrated_ because it has no logic) and _rehydrate_
|
|
||||||
it by booting up the JS and attaching event handlers on the DOM to make it
|
|
||||||
dynamic again. It's like restoring flavor to cup noodles by adding hot water.
|
|
||||||
|
|
||||||
Because we're using something called `morphdom` under the hood, all we need is
|
|
||||||
point at an `id` at the root of the application. The syntax for this is
|
|
||||||
slightly different from what we've seen so far, because we're _updating_ a
|
|
||||||
dehydrated DOM nodes to make them dynamic, rather than a new DOM tree and
|
|
||||||
attaching it to the DOM.
|
|
||||||
```js
|
|
||||||
const choo = require('choo')
|
|
||||||
const html = require('choo/html')
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
app.router((route) => [
|
|
||||||
route('/', (params, state, send) => html`
|
|
||||||
<h1 id="app-root">${state.message}</h1>
|
|
||||||
`)
|
|
||||||
])
|
|
||||||
|
|
||||||
if (module.parent) module.exports = app
|
|
||||||
else app.start('#app-root'))
|
|
||||||
```
|
|
||||||
|
|
||||||
When the JS is booted on top of the dehydrated application, it will look for
|
|
||||||
the `#app-root` id and load on top of it. You can choose any name you like for
|
|
||||||
the id, but __make sure it's the same on every possible top level DOM node__,
|
|
||||||
or else things might break. Furthermore to ensure things go smoothly, try and
|
|
||||||
keep the initial state identical on both the server and the client.
|
|
||||||
|
|
||||||
And that's it! If you want to go down the route of mad performance, consider
|
|
||||||
make all first request static and caching them using something like [bl][bl],
|
|
||||||
[nginx][nginx], [varnish][varnish] or a global CDN.
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
### app = choo()
|
This section provides documentation on how each function in `choo` works. It's
|
||||||
Create a new `choo` app
|
intended to be a technical reference. If you're interested in learning choo for
|
||||||
|
the first time, consider reading through the [handbook][handbook] or
|
||||||
|
[concepts](#concepts) first :sparkles:
|
||||||
|
|
||||||
|
### app = choo(opts)
|
||||||
|
Initialize a new `choo` app. Takes an optional object of handlers. Handlers can
|
||||||
|
be:
|
||||||
|
- __onError(err, state, createSend):__ called when an `effect` or
|
||||||
|
`subscription` emit an error. If no handler is passed, the default handler
|
||||||
|
will `throw` on each error.
|
||||||
|
- __onAction(action, state, name, caller, createSend):__ called when an
|
||||||
|
`action` is fired.
|
||||||
|
- __onStateChange(action, state, prev, caller, createSend):__ called after a
|
||||||
|
reducer changes the `state`.
|
||||||
|
|
||||||
|
`createSend()` is a special function that allows the creation of a new named
|
||||||
|
`send()` function. The first argument should be a string which is the name, the
|
||||||
|
second argument is a boolean `callOnError` which can be set to `true` to call
|
||||||
|
the `onError` hook istead of a provided callback. It then returns a
|
||||||
|
`send(actionName, data?)` function.
|
||||||
|
|
||||||
|
Handlers should be used with care, as they're the most powerful interface into
|
||||||
|
the state. For application level code it's generally recommended to delegate to
|
||||||
|
actions inside models using the `send()` call, and only shape the actions
|
||||||
|
inside the handlers.
|
||||||
|
|
||||||
### app.model(obj)
|
### app.model(obj)
|
||||||
Create a new model. Models modify data and perform IO. Takes the following
|
Create a new model. Models modify data and perform IO. Takes the following
|
||||||
arguments:
|
arguments:
|
||||||
- __namespace:__ optional namespace that prefixes the keys in `state`,
|
- __namespace:__ namespace the model so that it cannot access any properties
|
||||||
`reducers` and `effects`. Also limits `actions` called by `send()` to
|
and handlers in other models
|
||||||
in-namespace only.
|
- __state:__ initial values of `state` inside the model
|
||||||
- __state:__ object. Key value store of initial values
|
- __reducers:__ synchronous operations that modify state. Triggered by
|
||||||
- __reducers:__ object. Syncronous functions that modify state. Each function
|
`actions`. Signature of `(actionData, state)`.
|
||||||
has a signature of `(data, state)`
|
- __effects:__ asynchronous operations that don't modify state directly.
|
||||||
- __effects:__ object. Asyncronous functions that perform IO. Each function has
|
Triggered by `actions`, can call `actions`. Signature of `(actionData, state,
|
||||||
a signature of `(data, state, send)` where `send` is a reference to
|
send, done)`
|
||||||
`app.send()`
|
- __subscriptions:__ asynchronous read-only operations that don't modify state
|
||||||
|
directly. Can call `actions`. Signature of `(state, send, done)`.
|
||||||
|
|
||||||
### app.router(params, state, send)
|
#### send()
|
||||||
Creates a new router. See
|
Send a new action to the models with optional data attached. Namespaced models
|
||||||
|
can be accessed by prefixing the name with the namespace separated with a `:`,
|
||||||
|
e.g. `namespace:name`.
|
||||||
|
|
||||||
|
#### done()
|
||||||
|
When an `effect` or `subscription` is done executing, or encounters an error,
|
||||||
|
it should call the final `done(err)` callback. If an `effect` was called by
|
||||||
|
another `effect` it will call the callback of the caller. When an error
|
||||||
|
propegates all the way to the top, the `onError` handler will be called,
|
||||||
|
registered in `barracks(handlers)`. If no callback is registered, errors will
|
||||||
|
`throw`.
|
||||||
|
|
||||||
|
### app.router((route) => [routes])
|
||||||
|
Creates a new router. Takes a function that exposes a single `route` function,
|
||||||
|
and that expects a tree of `routes` to be returned. See
|
||||||
[`sheet-router`](https://github.com/yoshuawuyts/sheet-router) for full
|
[`sheet-router`](https://github.com/yoshuawuyts/sheet-router) for full
|
||||||
documentation. Registered views have a signature of `(params, state, send)`,
|
documentation. Registered views have a signature of `(state, prev, send)`,
|
||||||
where `params` is URI partials.
|
where `state` is the current `state`, `prev` is the last state, `state.params`
|
||||||
|
is URI partials and `send()` can be called to trigger actions.
|
||||||
|
|
||||||
### html = app.toString(route, state)
|
### html = app.toString(route, state?)
|
||||||
Render the application to a string of HTML. Useful for rendering on the server.
|
Render the application to a string of HTML. Useful for rendering on the server.
|
||||||
First argument is a path that's passed to the router. Second argument is the
|
First argument is a path that's passed to the router. Second argument is an
|
||||||
state object. When calling `.toString()` instead of `.start()`, all calls to
|
optional state object. When calling `.toString()` instead of `.start()`, all
|
||||||
`send()` are disabled, and `subscriptions`, `effects` and `reducers` aren't
|
calls to `send()` are disabled, and `subscriptions`, `effects` and `reducers`
|
||||||
loaded. See [rendering in Node](#rendering-in-node) for an in-depth guide.
|
aren't loaded.
|
||||||
|
|
||||||
### tree = app.start(rootId?, opts)
|
### tree = app.start(rootId?, opts)
|
||||||
Start the application. Returns a tree of DOM nodes that can be mounted using
|
Start the application. Returns a tree of DOM nodes that can be mounted using
|
||||||
@@ -677,45 +421,39 @@ following values:
|
|||||||
changes (eg `localhost/#posts/123`). Enabling this option automatically
|
changes (eg `localhost/#posts/123`). Enabling this option automatically
|
||||||
disables `opts.history` and `opts.href`.
|
disables `opts.history` and `opts.href`.
|
||||||
|
|
||||||
### choo/html\`html\`
|
### view = choo/html\`html\`
|
||||||
Tagged template string HTML builder. See
|
Tagged template string HTML builder. Built on top of [yo-yo][bel], [bel][bel]
|
||||||
[`yo-yo`](https://github.com/maxogden/yo-yo) for full documentation. Views
|
and [hyperx][hyperx]. To register a view on the `router` it should be wrapped
|
||||||
should be passed to `app.router()`
|
in a function with the signature of `(state, prev, send)` where `state` is the
|
||||||
|
current `state`, `prev` is the last state, `state.params` is URI partials and
|
||||||
|
`send()` can be called to trigger actions.
|
||||||
|
|
||||||
## Errors
|
To create listeners for events, create interpolated attributes on elements.
|
||||||
### Could not find DOM node (#id) to update
|
|
||||||
This means that a re-render of the DOM was triggered before the first render
|
|
||||||
was done. This is usually the case when `send()` is called inside a
|
|
||||||
`subscription` before the DOM is done rendering. Instead try listening for a
|
|
||||||
`'DOMContentLoaded'` event:
|
|
||||||
```js
|
```js
|
||||||
document.addEventListener('DOMContentLoaded', (e) => send('init'))
|
const html = require('choo/html')
|
||||||
|
html`
|
||||||
|
<button onclick=${(e) => console.log(e)}>click for bananas</button>
|
||||||
|
`
|
||||||
```
|
```
|
||||||
|
Example listeners include: `onclick`, `onsubmit`, `oninput`, `onkeydown`,
|
||||||
|
`onkeyup`. When creating listeners always remember to call `e.preventDefault()`
|
||||||
|
on the event so it doesn't bubble up and do stuff like refreshing the full page
|
||||||
|
or the like.
|
||||||
|
|
||||||
### send() cannot be called on the server
|
To trigger lifecycle events on any part of a view, set the `onload=${(el) =>
|
||||||
This means a `send()` event was triggered in Node. In Node, `reducers`,
|
{}}` and `onunload=${() => {}}` attributes. These parameters are useful when
|
||||||
`effects` and `subscriptions` are disabled for performance reasons, so if
|
creating self-contained widgets that take care of their own state and lifecycle
|
||||||
`send()` was called to trigger an action it wouldn't work. Try finding where in
|
(e.g. a maps widget) or to trigger animations. Most elements shouldn't have a
|
||||||
the DOM tree `send()` is called, and disable it when called from within Node.
|
need for these hooks though.
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
### Why did you build this?
|
|
||||||
`choo` is nothing but a formalization of how I've been building my applications
|
|
||||||
for the past year. I originally used `virtual-dom` with `virtual-app` and
|
|
||||||
`wayfarer` where now it's `yo-yo` with `send-action` and `sheet-router`. The
|
|
||||||
main benefit of using `choo` over these technologies separately is that it
|
|
||||||
becomes easier for teams to pick up and gather around. The code base for `choo`
|
|
||||||
itself is super petite (`~200` LOC) and mostly acts to enforce structure around
|
|
||||||
some excellent npm packages. This is my take on modular frameworks; I hope
|
|
||||||
you'll find it pleasant.
|
|
||||||
|
|
||||||
### Why is it called choo?
|
### Why is it called choo?
|
||||||
Because I thought it sounded cute. All these programs talk about being
|
Because I thought it sounded cute. All these programs talk about being
|
||||||
"performant", "rigid", "robust" - I like programming to be light, fun and
|
_"performant"_, _"rigid"_, _"robust"_ - I like programming to be light, fun and
|
||||||
non-scary. `choo` embraces that.
|
non-scary. `choo` embraces that.
|
||||||
|
|
||||||
Also imagine telling some business people you chose to rewrite something
|
Also imagine telling some business people you chose to rewrite something
|
||||||
critical to the company using the `choo` framework.
|
critical to the company using `choo`.
|
||||||
:steam_locomotive::train::train::train:
|
:steam_locomotive::train::train::train:
|
||||||
|
|
||||||
### Why is it a framework, and not a library?
|
### Why is it a framework, and not a library?
|
||||||
@@ -723,7 +461,13 @@ I love small libraries that do one thing well, but when working in a team,
|
|||||||
having an undocumented combination of packages often isn't great. `choo()` is a
|
having an undocumented combination of packages often isn't great. `choo()` is a
|
||||||
small set of packages that work well together, wrapped in an an architectural
|
small set of packages that work well together, wrapped in an an architectural
|
||||||
pattern. This means you get all the benefits of small packages, but get to be
|
pattern. This means you get all the benefits of small packages, but get to be
|
||||||
productive right from the start.
|
productive right from the start without needing to plough through layers of
|
||||||
|
boilerplate.
|
||||||
|
|
||||||
|
### Is it called choo, choo.js or...?
|
||||||
|
It's called "choo", though we're fine if you call it "choo-choo" or
|
||||||
|
"chugga-chugga-choo-choo" too. The only time "choo.js" is tolerated is if /
|
||||||
|
when you shimmy like you're a locomotive.
|
||||||
|
|
||||||
### How does choo compare to X?
|
### How does choo compare to X?
|
||||||
Ah, so this is where I get to rant. `choo` (_chugga-chugga-chugga-choo-choo!_)
|
Ah, so this is where I get to rant. `choo` (_chugga-chugga-chugga-choo-choo!_)
|
||||||
@@ -768,9 +512,16 @@ give you my opinions directly. Ready? Here goes:
|
|||||||
provides framework lock in, and additionally doesn't have a clean enough
|
provides framework lock in, and additionally doesn't have a clean enough
|
||||||
architecture. I appreciate what it does, but don't think it's the answer.
|
architecture. I appreciate what it does, but don't think it's the answer.
|
||||||
|
|
||||||
|
### Why can't send() be called on the server?
|
||||||
|
In Node, `reducers`, `effects` and `subscriptions` are disabled for performance
|
||||||
|
reasons, so if `send()` was called to trigger an action it wouldn't work. Try
|
||||||
|
finding where in the DOM tree `send()` is called, and disable it when called
|
||||||
|
from within Node.
|
||||||
|
|
||||||
### Which packages was choo built on?
|
### Which packages was choo built on?
|
||||||
- __views:__ [`yo-yo`](https://github.com/maxogden/yo-yo)
|
- __views:__ [`yo-yo`](https://github.com/maxogden/yo-yo),
|
||||||
- __models:__ [`send-action`](https://github.com/sethvincent/send-action),
|
[`bel`](https://github.com/shama/bel)
|
||||||
|
- __models:__ [`barracks`](https://github.com/yoshuawuyts/barracks),
|
||||||
[`xtend`](https://github.com/raynos/xtend)
|
[`xtend`](https://github.com/raynos/xtend)
|
||||||
- __routes:__ [`sheet-router`](https://github.com/yoshuawuyts/sheet-router)
|
- __routes:__ [`sheet-router`](https://github.com/yoshuawuyts/sheet-router)
|
||||||
- __http:__ [`xhr`](https://github.com/Raynos/xhr)
|
- __http:__ [`xhr`](https://github.com/Raynos/xhr)
|
||||||
@@ -782,13 +533,6 @@ with DOM nodes][morphdom-bench], and it has the added benefit of working with
|
|||||||
_any_ library that produces valid DOM nodes. So to put a long answer short:
|
_any_ library that produces valid DOM nodes. So to put a long answer short:
|
||||||
we're using something even better.
|
we're using something even better.
|
||||||
|
|
||||||
### What packages do you recommend to pair with choo?
|
|
||||||
- [tachyons](https://github.com/tachyons-css/tachyons) - functional CSS for
|
|
||||||
humans
|
|
||||||
- [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for
|
|
||||||
browserify
|
|
||||||
- [pull-stream](https://github.com/pull-stream/pull-stream) - minimal streams
|
|
||||||
|
|
||||||
### How can I optimize choo?
|
### How can I optimize choo?
|
||||||
`choo` really shines when coupled with `browserify` transforms. They can do
|
`choo` really shines when coupled with `browserify` transforms. They can do
|
||||||
things like reduce file size, prune dependencies and clean up boilerplate code.
|
things like reduce file size, prune dependencies and clean up boilerplate code.
|
||||||
@@ -796,7 +540,8 @@ Consider running some of the following:
|
|||||||
- [unassertify](https://github.com/twada/unassertify) - remove `assert()`
|
- [unassertify](https://github.com/twada/unassertify) - remove `assert()`
|
||||||
statements which reduces file size. Use as a `--global` transform
|
statements which reduces file size. Use as a `--global` transform
|
||||||
- [es2020](https://github.com/yoshuawuyts/es2020) - backport `const`,
|
- [es2020](https://github.com/yoshuawuyts/es2020) - backport `const`,
|
||||||
`fat-arrows` and `template strings` to older browsers
|
`fat-arrows` and `template strings` to older browsers. Should be run as a
|
||||||
|
`--global` transform
|
||||||
- [yo-yoify](https://github.com/shama/yo-yoify) - replace the internal `hyperx`
|
- [yo-yoify](https://github.com/shama/yo-yoify) - replace the internal `hyperx`
|
||||||
dependency with `document.createElement` calls; greatly speeds up performance
|
dependency with `document.createElement` calls; greatly speeds up performance
|
||||||
too
|
too
|
||||||
@@ -850,20 +595,22 @@ Sure.
|
|||||||
$ npm install choo
|
$ npm install choo
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
|
||||||
Browser tests can be run with the right credentials via the `npm run test:browser`
|
|
||||||
command. This will be run automatically when `npm version` is executed.
|
|
||||||
|
|
||||||
You may skip the tests by providing `SKIP_TEST=true` when running the version
|
|
||||||
command.
|
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
- [choo-handbook](https://github.com/yoshuawuyts/choo-handbook) - the little
|
||||||
|
`choo` guide
|
||||||
- [budo](https://github.com/mattdesl/budo) - quick prototyping tool for
|
- [budo](https://github.com/mattdesl/budo) - quick prototyping tool for
|
||||||
`browserify`
|
`browserify`
|
||||||
- [stack.gl](http://stack.gl/) - open software ecosystem for WebGL
|
- [stack.gl](http://stack.gl/) - open software ecosystem for WebGL
|
||||||
- [yo-yo](https://github.com/maxogden/yo-yo) - tiny library for modular UI
|
- [yo-yo](https://github.com/maxogden/yo-yo) - tiny library for modular UI
|
||||||
- [bel](https://github.com/shama/bel) - composable DOM elements using template
|
- [bel](https://github.com/shama/bel) - composable DOM elements using template
|
||||||
strings
|
strings
|
||||||
|
- [tachyons](https://github.com/tachyons-css/tachyons) - functional CSS for
|
||||||
|
humans
|
||||||
|
- [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for
|
||||||
|
`browserify`
|
||||||
|
- [pull-stream](https://github.com/pull-stream/pull-stream) - minimal streams
|
||||||
|
- [es2020](https://github.com/yoshuawuyts/es2020) - because in hindsight we
|
||||||
|
don't need most of ES6
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[MIT](https://tldrlegal.com/license/mit-license)
|
[MIT](https://tldrlegal.com/license/mit-license)
|
||||||
@@ -887,3 +634,8 @@ command.
|
|||||||
[sheet-router]: https://github.com/yoshuawuyts/sheet-router
|
[sheet-router]: https://github.com/yoshuawuyts/sheet-router
|
||||||
[html-input]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
|
[html-input]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
|
||||||
[inu]: https://github.com/ahdinosaur/inu
|
[inu]: https://github.com/ahdinosaur/inu
|
||||||
|
[yo-yo]: https://github.com/maxogden/yo-yo
|
||||||
|
[bel]: https://github.com/shama/bel
|
||||||
|
[hyperx]: https://github.com/substack/hyperx
|
||||||
|
[budo]: https://github.com/mattdesl/budo
|
||||||
|
[es2020]: https://github.com/yoshuawuyts/es2020
|
||||||
|
|||||||
Reference in New Issue
Block a user