@@ -121,16 +121,16 @@ const app = choo()
|
|||||||
app.model({
|
app.model({
|
||||||
state: { title: 'Not quite set yet' },
|
state: { title: 'Not quite set yet' },
|
||||||
reducers: {
|
reducers: {
|
||||||
update: (data, state) => ({ title: data.value })
|
update: (data, state) => ({ title: data })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const mainView = (params, state, send) => html`
|
const mainView = (state, prev, send) => html`
|
||||||
<main>
|
<main>
|
||||||
<h1>Title: ${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', e.target.value)}>
|
||||||
</main>
|
</main>
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -142,19 +142,21 @@ 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
|
To run it, save it as `client.js` and run with [budo][budo] and
|
||||||
[es2020][es2020]:
|
[es2020][es2020]. These tools are convenient but any [browserify][browserify]
|
||||||
|
based tool should do:
|
||||||
```sh
|
```sh
|
||||||
$ budo 'client.js' -p 8080 --open -- -t es2020
|
$ budo 'client.js' -p 8080 --open -- -t es2020
|
||||||
```
|
```
|
||||||
|
|
||||||
And to save to static files for deployment, open a new terminal and do:
|
And to save the output to files so it can be deployed, open a new terminal and
|
||||||
|
do:
|
||||||
```bash
|
```bash
|
||||||
$ mkdir -p 'dist/'
|
$ mkdir -p 'dist/'
|
||||||
$ curl 'localhost:8080' > 'dist/index.html'
|
$ curl 'localhost:8080' > 'dist/index.html'
|
||||||
$ curl 'localhost:8080/bundle.js' > 'dist/bundle.js'
|
$ curl 'localhost:8080/bundle.js' > 'dist/bundle.js'
|
||||||
```
|
```
|
||||||
All just a couple of shell commands and `.js` files, no grandiose boilerplate
|
All using a couple of shell commands and `.js` files, no grandiose boilerplate
|
||||||
needed.
|
needed.
|
||||||
|
|
||||||
## Philosophy
|
## Philosophy
|
||||||
@@ -189,7 +191,7 @@ sources of data. `effects` react to changes, perform an `action` and can then
|
|||||||
post the results. `reducers` take data, modify it, and update the internal
|
post the results. `reducers` take data, modify it, and update the internal
|
||||||
`state`.
|
`state`.
|
||||||
|
|
||||||
Communication of data is done using objects called `actions`. Each `action`
|
Communication of data is done using something called `actions`. Each `action`
|
||||||
consists of a unique `actionName` and an optional payload of `data`, which can
|
consists of a unique `actionName` and an optional payload of `data`, which can
|
||||||
be any value.
|
be any value.
|
||||||
|
|
||||||
@@ -232,14 +234,14 @@ namespaced or not. Namespacing means that only state within the model can be
|
|||||||
accessed. Models can still trigger actions on other models, though it's
|
accessed. Models can still trigger actions on other models, though it's
|
||||||
recommended to keep that to a minimum.
|
recommended to keep that to a minimum.
|
||||||
|
|
||||||
So say we have a `todos` namespace, an `add` reducer and a `todos` model.
|
So say we have a `myTodos` namespace, an `add` reducer and a `todos` model.
|
||||||
Outside the model they're called by `send('todos:add')` and
|
Outside the model they're called by `send('todos:add')` and
|
||||||
`state.todos.todos`. Inside the namespaced model they're called by
|
`state.todos.todos`. Inside the namespaced model they're called by
|
||||||
`send('todos:add')` and `state.todos`. An example namespaced model:
|
`send('todos:add')` and `state.todos`. An example namespaced model:
|
||||||
```js
|
```js
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.model({
|
app.model({
|
||||||
namespace: 'todos',
|
namespace: 'myTodos',
|
||||||
state: { todos: [] },
|
state: { todos: [] },
|
||||||
reducers: {
|
reducers: {
|
||||||
add: (data, state) => ({ todos: state.todos.concat(data.payload) })
|
add: (data, state) => ({ todos: state.todos.concat(data.payload) })
|
||||||
@@ -274,6 +276,18 @@ 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
|
||||||
|
|
||||||
|
Examples of effects include: performing
|
||||||
|
[xhr](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) requests
|
||||||
|
(server requests), calling multiple `reducers`, persisting state to
|
||||||
|
[localstorage][localstorage].
|
||||||
|
|
||||||
|
When an `effect` is done executing, it should call the `done(err, res)`
|
||||||
|
callback. This callback used to communicate when an `effect` is done, handle
|
||||||
|
possible errors and send values back to the caller. You'll probably notice when
|
||||||
|
applications become more complex, that composing multiple namespaced models
|
||||||
|
using higher level effects becomes real powerful - without becoming
|
||||||
|
complicated.
|
||||||
|
|
||||||
### 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
|
||||||
@@ -285,13 +299,17 @@ const app = choo()
|
|||||||
app.model({
|
app.model({
|
||||||
namespace: 'app',
|
namespace: 'app',
|
||||||
subscriptions: [
|
subscriptions: [
|
||||||
(send) => setInterval(() => send('app:print', { payload: 'dog?' }), 1000)
|
(send, done) => {
|
||||||
|
setInterval(() => send('app:print', { payload: 'dog?' }), 1000)
|
||||||
|
}
|
||||||
],
|
],
|
||||||
effects: {
|
effects: {
|
||||||
print: (data, state) => console.log(data.payload)
|
print: (data, state) => console.log(data.payload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
If a `subscription` runs into an error, it can call `done(err)` to signal the
|
||||||
|
error to the error hook.
|
||||||
|
|
||||||
### 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
|
||||||
@@ -315,9 +333,9 @@ figure out how views relate to each other.
|
|||||||
Under the hood `choo` uses [sheet-router][sheet-router]. Internally the
|
Under the hood `choo` uses [sheet-router][sheet-router]. Internally the
|
||||||
currently rendered route is kept in `state.app.location`. If you want to modify
|
currently rendered route is kept in `state.app.location`. If you want to modify
|
||||||
the location programmatically the `reducer` for the location can be called
|
the location programmatically the `reducer` for the location can be called
|
||||||
using `send('app:location', { location: href })`. This will not work from
|
using `send('location:setLocation', { location: href })`. This will not work
|
||||||
within namespaced `models`, and usage should preferably be kept to a minimum.
|
from within namespaced `models`, and usage should preferably be kept to a
|
||||||
Changing views all over the place tends to lead to messiness.
|
minimum. 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.
|
||||||
@@ -325,11 +343,13 @@ Views are pure functions that return a DOM tree for the router to render. They
|
|||||||
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.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const view = (params, state, send) => {
|
const view = (state, prev, send) => {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<h1>Total todos: ${state.todos.length}</h1>
|
<h1>Total todos: ${state.todos.length}</h1>
|
||||||
<button onclick=${(e) => send('add', { payload: {title: 'demo'})}>Add</button>
|
<button onclick=${(e) => send('add', {title: 'demo'})}>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -370,33 +390,35 @@ arguments:
|
|||||||
and handlers in other models
|
and handlers in other models
|
||||||
- __state:__ initial values of `state` inside the model
|
- __state:__ initial values of `state` inside the model
|
||||||
- __reducers:__ synchronous operations that modify state. Triggered by
|
- __reducers:__ synchronous operations that modify state. Triggered by
|
||||||
`actions`. Signature of `(actionData, state)`.
|
`actions`. Signature of `(data, state)`.
|
||||||
- __effects:__ asynchronous operations that don't modify state directly.
|
- __effects:__ asynchronous operations that don't modify state directly.
|
||||||
Triggered by `actions`, can call `actions`. Signature of `(actionData, state,
|
Triggered by `actions`, can call `actions`. Signature of `(data, state,
|
||||||
send, done)`
|
send, done)`
|
||||||
- __subscriptions:__ asynchronous read-only operations that don't modify state
|
- __subscriptions:__ asynchronous read-only operations that don't modify state
|
||||||
directly. Can call `actions`. Signature of `(state, send, done)`.
|
directly. Can call `actions`. Signature of `(state, send, done)`.
|
||||||
|
|
||||||
#### send()
|
#### send(actionName, data?)
|
||||||
Send a new action to the models with optional data attached. Namespaced models
|
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 `:`,
|
can be accessed by prefixing the name with the namespace separated with a `:`,
|
||||||
e.g. `namespace:name`.
|
e.g. `namespace:name`.
|
||||||
|
|
||||||
#### done()
|
#### done(err?, res?)
|
||||||
When an `effect` or `subscription` is done executing, or encounters an error,
|
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
|
it should call the final `done(err, res)` callback. If an `effect` was called
|
||||||
another `effect` it will call the callback of the caller. When an error
|
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,
|
propegates all the way to the top, the `onError` handler will be called,
|
||||||
registered in `barracks(handlers)`. If no callback is registered, errors will
|
registered in `choo(handlers)`. If no callback is registered, errors will
|
||||||
`throw`.
|
`throw`.
|
||||||
|
|
||||||
### app.router((route) => [routes])
|
### app.router(defaultRoute?, (route) => [routes])
|
||||||
Creates a new router. Takes a function that exposes a single `route` function,
|
Creates a new router. Takes a function that exposes a single `route` function,
|
||||||
and that expects a tree of `routes` to be returned. See
|
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 `(state, prev, send)`,
|
documentation. Registered views have a signature of `(state, prev, send)`,
|
||||||
where `state` is the current `state`, `prev` is the last state, `state.params`
|
where `state` is the current `state`, `prev` is the last state, `state.params`
|
||||||
is URI partials and `send()` can be called to trigger actions.
|
is URI partials and `send()` can be called to trigger actions. If
|
||||||
|
`defaultRoute` is passed in, that will be called if no paths match. If no
|
||||||
|
`defaultRoute` is specified it will throw instead.
|
||||||
|
|
||||||
### 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.
|
||||||
@@ -412,8 +434,8 @@ first argument, the tree will diff against the selected node rather than be
|
|||||||
returned. This is useful for [rehydration](#rehydration). Opts can contain the
|
returned. This is useful for [rehydration](#rehydration). Opts can contain the
|
||||||
following values:
|
following values:
|
||||||
- __opts.history:__ default: `true`. Enable a `subscription` to the browser
|
- __opts.history:__ default: `true`. Enable a `subscription` to the browser
|
||||||
history API. e.g. updates the internal `state.location` state whenever the
|
history API. e.g. updates the internal `location.href` state whenever the
|
||||||
browser "forward" and "backward" buttons are pressed.
|
browsers "forward" and "backward" buttons are pressed.
|
||||||
- __opts.href:__ default: `true`. Handle all relative `<a
|
- __opts.href:__ default: `true`. Handle all relative `<a
|
||||||
href="<location>"></a>` clicks and update internal `state.location`
|
href="<location>"></a>` clicks and update internal `state.location`
|
||||||
accordingly.
|
accordingly.
|
||||||
@@ -422,8 +444,8 @@ 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`.
|
||||||
|
|
||||||
### view = choo/html\`html\`
|
### view = require('choo/html')\`html\`
|
||||||
Tagged template string HTML builder. Built on top of [yo-yo][bel], [bel][bel]
|
Tagged template string HTML builder. Built on top of [yo-yo][yo-yo], [bel][bel]
|
||||||
and [hyperx][hyperx]. To register a view on the `router` it should be wrapped
|
and [hyperx][hyperx]. To register a view on the `router` it should be wrapped
|
||||||
in a function with the signature of `(state, prev, send)` where `state` is the
|
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
|
current `state`, `prev` is the last state, `state.params` is URI partials and
|
||||||
@@ -437,12 +459,13 @@ html`
|
|||||||
`
|
`
|
||||||
```
|
```
|
||||||
Example listeners include: `onclick`, `onsubmit`, `oninput`, `onkeydown`,
|
Example listeners include: `onclick`, `onsubmit`, `oninput`, `onkeydown`,
|
||||||
`onkeyup`. When creating listeners always remember to call `e.preventDefault()`
|
`onkeyup`. A full list can be found [at the yo-yo
|
||||||
on the event so it doesn't bubble up and do stuff like refreshing the full page
|
repo](https://github.com/maxogden/yo-yo/blob/master/update-events.js). When
|
||||||
or the like.
|
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.
|
||||||
|
|
||||||
To trigger lifecycle events on any part of a view, set the `onload=${(el) =>
|
To trigger lifecycle events on any part of a view, set the `onload=${(el) =>
|
||||||
{}}` and `onunload=${() => {}}` attributes. These parameters are useful when
|
{}}` and `onunload=${() => {el}}` attributes. These parameters are useful when
|
||||||
creating self-contained widgets that take care of their own state and lifecycle
|
creating self-contained widgets that take care of their own state and lifecycle
|
||||||
(e.g. a maps widget) or to trigger animations. Most elements shouldn't have a
|
(e.g. a maps widget) or to trigger animations. Most elements shouldn't have a
|
||||||
need for these hooks though.
|
need for these hooks though.
|
||||||
@@ -640,3 +663,6 @@ $ npm install choo
|
|||||||
[hyperx]: https://github.com/substack/hyperx
|
[hyperx]: https://github.com/substack/hyperx
|
||||||
[budo]: https://github.com/mattdesl/budo
|
[budo]: https://github.com/mattdesl/budo
|
||||||
[es2020]: https://github.com/yoshuawuyts/es2020
|
[es2020]: https://github.com/yoshuawuyts/es2020
|
||||||
|
[browserify]: https://github.com/substack/browserify
|
||||||
|
[localstorage]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
|
||||||
|
[handbook]: https://github.com/yoshuawuyts/choo-handbook
|
||||||
|
|||||||
Reference in New Issue
Block a user