[major] choo v4 (#352)
* location: change url on location:setLocation - [ ] don't break hashing - [ ] allow not changing the url fixup! move fns around fixup! add search string * app.start: clean * walk: add * location: update arg calls * fixup! location: update * uri-wrap: fix thunking * router: update to latest sheet-router (#239) * router: update to latest sheet-router * tests: fix for latest version * tests: fix SSR * tests: fix history * tests: spruce up * docs: update example * examples: update * deps: bump sheet-router * 4.0.0-0 * chore(changelog): 4.0.0 (#211) * feat(api:) arg order (#268) * s/data, state/state, data/ * feat(api): swap arguments * fix(href): fix routing (#271) * feat(http): remove (#269) * feat(router): enable hash routing (#273) * deps: fix mount * 4.0.0-1 * feat(mount): copy {script,link} tags * 4.0.0-2 * fix(mount): forEach -> for Lol can't use forEach * fix(router): use state.location.href (#282) * fix(mount): use deep node clone * 4.0.0-3 * fix(deps): remove hash-match * 4.0.0-4 * fix(mount): return node * 4.0.0-5 * fix(router): check if a hash is a valid selector (#339) * 4.0.0-6 * fix(router): pass params on newstate (#343) * 4.0.0-7 * feat(docs): update for 4.0.0 (#320) * feat(docs): update for 4.0.0 * docs: update router example in readme (#337) * chore(changelog): update for v4 (#351)
This commit is contained in:
@@ -1,3 +1,32 @@
|
||||
## `4.0.0` The routing patch
|
||||
This patch changes the way we handle routes. It introduces query string
|
||||
support (!), and changes the router to use a lisp-like syntax. It also inverts
|
||||
the argument order of effects and reducers to be more intuitive. We also
|
||||
managed to sneak in some performance upgrades :sparkles: - We hope you enjoy
|
||||
it!
|
||||
|
||||
### changes
|
||||
- :exclamation: slim down server side rendering API |
|
||||
[issue](https://github.com/yoshuawuyts/choo/issues/191) |
|
||||
[pull-request](https://github.com/yoshuawuyts/choo/pull/203)
|
||||
- :exclamation: update router API to be lisp-like
|
||||
- :exclamation: swap `state` and `data` argument order |
|
||||
[issue](https://github.com/yoshuawuyts/choo/issues/179)
|
||||
- :exclamation: remove `choo/http`. Use [xhr](https://github.com/naugtur/xhr)
|
||||
instead | [pull-request](https://github.com/yoshuawuyts/choo/pull/269)
|
||||
- update `router` to use memoization |
|
||||
[issue](https://github.com/yoshuawuyts/sheet-router/issues/17) |
|
||||
[pull-request](https://github.com/yoshuawuyts/sheet-router/pull/34)
|
||||
- support inline anchor links |
|
||||
[issue](https://github.com/yoshuawuyts/choo/issues/65)
|
||||
- allow bypassing of link clicks in `sheet-router` |
|
||||
[issue](https://github.com/yoshuawuyts/sheet-router/issues/15) |
|
||||
[pull-request](https://github.com/yoshuawuyts/sheet-router/pull/27)
|
||||
- update router API to handle hashes by default
|
||||
- update router to provide out of the box support for Electron
|
||||
- update `location` state to expose `search` parameters (query strings) |
|
||||
[issue](https://github.com/yoshuawuyts/sheet-router/issues/31)
|
||||
|
||||
## `3.3.0`
|
||||
Yay, `plugins` now support `wrappers` which is a segway onto HMR, time travel
|
||||
and other cool plugins. These changes have come through in barracks `v8.3.0`
|
||||
|
||||
@@ -125,49 +125,50 @@ production, we'd love to hear from you!_
|
||||
Let's create an input box that changes the content of a textbox in real time.
|
||||
[Click here to see the app running](http://requirebin.com/?gist=229bceda0334cf30e3044d5f5c600960).
|
||||
```js
|
||||
const choo = require('choo')
|
||||
const html = require('choo/html')
|
||||
const app = choo()
|
||||
var html = require('choo/html')
|
||||
var choo = require('choo')
|
||||
var app = choo()
|
||||
|
||||
app.model({
|
||||
state: { title: 'Not quite set yet' },
|
||||
reducers: {
|
||||
update: (data, state) => ({ title: data })
|
||||
update: function (state, data) {
|
||||
return { title: data }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const mainView = (state, prev, send) => html`
|
||||
function mainView (state, prev, send) {
|
||||
return html`
|
||||
<main>
|
||||
<h1>Title: ${state.title}</h1>
|
||||
<input
|
||||
type="text"
|
||||
oninput=${(e) => send('update', e.target.value)}/>
|
||||
<input type="text" oninput=${update}>
|
||||
</main>
|
||||
`
|
||||
|
||||
app.router((route) => [
|
||||
route('/', mainView)
|
||||
])
|
||||
function update (e) {
|
||||
send('update', e.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
const tree = app.start()
|
||||
app.router(['/', mainView])
|
||||
|
||||
var tree = app.start()
|
||||
document.body.appendChild(tree)
|
||||
```
|
||||
|
||||
To run it, save it as `client.js` and run with [budo] and [es2020]. These tools
|
||||
are convenient but any [browserify] based tool should do:
|
||||
To run it, save it as `client.js` and run with [bankai][bankai]. `bankai` is
|
||||
convenient but any [browserify][browserify] based tool should do:
|
||||
```sh
|
||||
$ budo client.js -p 8080 --open -- -t es2020
|
||||
```
|
||||
# run and reload on port 8080
|
||||
$ bankai client.js -p 8080 --open
|
||||
|
||||
And to save the output to files so it can be deployed, open a new terminal and
|
||||
do:
|
||||
```bash
|
||||
$ mkdir -p 'dist/'
|
||||
$ curl 'localhost:8080' > 'dist/index.html'
|
||||
$ curl 'localhost:8080/client.js' > 'dist/client.js'
|
||||
# compile to static files in `./dist/`
|
||||
$ bankai build index.js dist/
|
||||
|
||||
# deploy to github pages using `tschaub/gh-pages`
|
||||
$ gh-pages -d dist/
|
||||
```
|
||||
All using 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
|
||||
@@ -194,47 +195,36 @@ better results and super smiley faces.
|
||||
|
||||
## Concepts
|
||||
`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
|
||||
`models` that contain several properties. `subscriptions` are functions that
|
||||
are called at startup and have `send()` passed in, so they act as read-only
|
||||
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
|
||||
`state`.
|
||||
be combined into a nice, cohesive machine. Roughly speaking there are two parts
|
||||
to `choo`: the views and the models. Models take care of state and logic, and
|
||||
views are responsible for displaying the interface and responding to user
|
||||
interactions.
|
||||
|
||||
Communication of data is done using something called `actions`. Each `action`
|
||||
consists of a unique `actionName` and an optional payload of `data`, which can
|
||||
be any value.
|
||||
All of `choo`'s state is contained in a single object and whenever it changes
|
||||
the views receive a new version of the state which they can use to safely
|
||||
render a complete new representation of the DOM. The DOM is efficiently updated
|
||||
using DOM diffing/patching.
|
||||
|
||||
When a `reducer` modifies `state`, the `router` is called, which in turn calls
|
||||
`views`. `views` take `state` and return [DOM] nodes which are then
|
||||
efficiently rendered on the screen.
|
||||
The logic in choo exist in three different kinds of actions, each with their
|
||||
own role: `effects`, `subscriptions` and `reducers`.
|
||||
|
||||
- __Effects__ makes an asynchronous operation and calls another action when
|
||||
it's done.
|
||||
|
||||
- __Subscriptions__ (called once when the DOM loads) listens for external input
|
||||
like keyboard or WebSocket events and then calls another action.
|
||||
|
||||
- __Reducers__ receives the current state and returns an updated version of the
|
||||
state which is then sent to the views.
|
||||
|
||||
In turn when the `views` are rendered, the `user` can interact with elements by
|
||||
clicking on them, triggering `actions` which then flow back into the
|
||||
application logic. This is the _unidirectional_ architecture of `choo`.
|
||||
```txt
|
||||
┌─────────────────┐
|
||||
│ Subscriptions ─┤ User ───┐
|
||||
└─ Effects ◀─────┤ ▼
|
||||
┌─ Reducers ◀─────┴──Actions── DOM ◀┐
|
||||
┌─ Reducers ◀─────┴─────────── DOM ◀┐
|
||||
│ │
|
||||
└▶ Router ─────State ───▶ Views ────┘
|
||||
```
|
||||
- __user:__ 🙆
|
||||
- __DOM:__ the [Document Object Model][DOM] is what is currently displayed in
|
||||
your browser
|
||||
- __actions:__ a named event with optional properties attached. Used to call
|
||||
`effects` and `reducers` that have been registered in `models`
|
||||
- __model:__ optionally namespaced object containing `subscriptions`,
|
||||
`effects`, `reducers` and initial `state`
|
||||
- __subscriptions:__ read-only data sources that emit `actions`
|
||||
- __effects:__ asynchronous functions that emit an `action` when done
|
||||
- __reducers:__ synchronous functions that modify `state`
|
||||
- __state:__ a single object that contains __all__ the values used in your
|
||||
application
|
||||
- __router:__ determines which `view` to render
|
||||
- __views:__ take `state` and returns a new `DOM tree` that is rendered in the
|
||||
browser
|
||||
|
||||
### Models
|
||||
`models` are objects that contain initial `state`, `subscriptions`, `effects`
|
||||
@@ -249,12 +239,14 @@ Outside the model they're called by `send('todos:add')` and
|
||||
`state.todos.items`. Inside the namespaced model they're called by
|
||||
`send('todos:add')` and `state.items`. An example namespaced model:
|
||||
```js
|
||||
const app = choo()
|
||||
var app = choo()
|
||||
app.model({
|
||||
namespace: 'todos',
|
||||
state: { items: [] },
|
||||
reducers: {
|
||||
add: (data, state) => ({ items: state.items.concat(data.payload) })
|
||||
add: function (state, data) {
|
||||
return { items: state.items.concat(data.payload) }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -272,52 +264,52 @@ bulk of your logic will be safely shielded, with only a few points touching ever
|
||||
part of your application.
|
||||
|
||||
### Effects
|
||||
Side effects are done through `effects` declared in `app.model()`. Unlike
|
||||
`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
|
||||
every time you don't need to modify the state object directly, but wish to
|
||||
respond to an action.
|
||||
`effects` are similar to `reducers` except instead of modifying the state they
|
||||
cause side `effects` by interacting servers, databases, DOM APIs, etc. Often
|
||||
they'll call a reducer when they're done to update the state. For instance, you
|
||||
may have an effect called getUsers that fetches a list of users from a server
|
||||
API using AJAX. Assuming the AJAX request completes successfully, the effect
|
||||
can pass off the list of users to a reducer called receiveUsers which simply
|
||||
updates the state with that list, separating the concerns of interacting with
|
||||
an API from updating the application's state.
|
||||
|
||||
A typical `effect` flow looks like:
|
||||
|
||||
1. An action is received
|
||||
2. An effect is triggered
|
||||
3. The effect performs an async call
|
||||
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
|
||||
|
||||
Examples of effects include: performing [xhr] requests (server requests),
|
||||
calling multiple `reducers`, persisting state to [localstorage].
|
||||
This is an example `effect` that is called once when the application loads and
|
||||
calls the `'todos:add'` `reducer` when it receives data from the server:
|
||||
|
||||
```js
|
||||
const http = require('choo/http')
|
||||
const choo = require('choo')
|
||||
const app = choo()
|
||||
var choo = require('choo')
|
||||
var http = require('xhr')
|
||||
var app = choo()
|
||||
|
||||
app.model({
|
||||
namespace: 'todos',
|
||||
state: { items: [] },
|
||||
state: { values: [] },
|
||||
reducers: {
|
||||
add: function (data, state) {
|
||||
return { todos: data }
|
||||
}
|
||||
},
|
||||
effects: {
|
||||
fetch: (data, state, send, done) => {
|
||||
http('/todos', (err, res, body) => {
|
||||
send('todos:receive', body, done)
|
||||
addAndSave: function (state, data, send, done) {
|
||||
var opts = { body: data.payload, json: true }
|
||||
http.post('/todo', opts, function (err, res, body) {
|
||||
if (err) return done(err)
|
||||
data.payload.id = body.id
|
||||
send('todos:add', data, function (err, value) {
|
||||
if (err) return done(err)
|
||||
done(null, value)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
reducers: {
|
||||
receive: (data, state) => {
|
||||
return { items: data }
|
||||
subscriptions: {
|
||||
'called-once-when-the-app-loads': function (send, done) {
|
||||
send('todos:addAndSave', done)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
When an `effect` is done executing, it should call the `done(err, res)`
|
||||
callback. This callback is 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 really powerful - without becoming
|
||||
complicated.
|
||||
|
||||
### Subscriptions
|
||||
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
|
||||
@@ -325,23 +317,29 @@ chat app, or when catching keyboard input for a videogame.
|
||||
|
||||
An example subscription that logs `"dog?"` every second:
|
||||
```js
|
||||
const app = choo()
|
||||
var choo = require('choo')
|
||||
|
||||
var app = choo()
|
||||
app.model({
|
||||
namespace: 'app',
|
||||
subscriptions: [
|
||||
(send, done) => {
|
||||
setInterval(() => {
|
||||
send('app:print', { payload: 'dog?', myOtherValue: 1000 }, (err) => {
|
||||
effects: {
|
||||
print: function (state, data) {
|
||||
console.log(data.payload)
|
||||
}
|
||||
},
|
||||
subscriptions: {
|
||||
callDog: function (send, done) {
|
||||
setInterval(function () {
|
||||
var data = { payload: 'dog?', myOtherValue: 1000 }
|
||||
send('app:print', data, function (err) {
|
||||
if (err) return done(err)
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
],
|
||||
effects: {
|
||||
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.
|
||||
|
||||
@@ -350,13 +348,13 @@ The `router` manages which `views` are rendered at any given time. It also
|
||||
supports rendering a default `view` if no routes match.
|
||||
|
||||
```js
|
||||
const app = choo()
|
||||
app.router('/404', (route) => [
|
||||
route('/', require('./views/empty')),
|
||||
route('/404', require('./views/error')),
|
||||
route('/:mailbox', require('./views/mailbox'), [
|
||||
route('/:message', require('./views/email'))
|
||||
])
|
||||
var app = choo()
|
||||
app.router({ default: '/404' }, [
|
||||
[ '/', require('./views/empty') ],
|
||||
[ '/404', require('./views/error') ],
|
||||
[ '/:mailbox', require('./views/mailbox'), [
|
||||
[ '/:message', require('./views/email') ]
|
||||
]]
|
||||
])
|
||||
```
|
||||
|
||||
@@ -372,21 +370,29 @@ from within namespaced `models`, and usage should preferably be kept to a
|
||||
minimum. Changing views all over the place tends to lead to messiness.
|
||||
|
||||
### 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.
|
||||
|
||||
```javascript
|
||||
const view = (state, prev, send) => {
|
||||
```js
|
||||
function view (state, prev, send) {
|
||||
return html`
|
||||
<div>
|
||||
<h1>Total todos: ${state.todos.length}</h1>
|
||||
<button onclick=${(e) => send('add', {title: 'demo'})}>
|
||||
Add
|
||||
</button>
|
||||
</div>`
|
||||
<button onclick=${addTodo}>Add</button>
|
||||
</div>
|
||||
`
|
||||
|
||||
function addTodo (e) {
|
||||
send('add', { title: 'demo' })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
@@ -401,13 +407,13 @@ somewhere. This is done through something called `plugins`. Plugins are objects
|
||||
that contain `hook` and `wrap` functions and are passed to `app.use()`:
|
||||
|
||||
```js
|
||||
const log = require('choo-log')
|
||||
const choo = require('choo')
|
||||
const app = choo()
|
||||
var log = require('choo-log')
|
||||
var choo = require('choo')
|
||||
var app = choo()
|
||||
|
||||
app.use(log())
|
||||
|
||||
const tree = app.start()
|
||||
var tree = app.start()
|
||||
document.body.appendChild(tree)
|
||||
```
|
||||
|
||||
@@ -427,9 +433,9 @@ and `wrappers` are available, head on over to [app.use()](#appusehooks).
|
||||
Using `choo` in a project? Show off which version you've used using a badge:
|
||||
|
||||
|
||||
[](https://github.com/yoshuawuyts/choo)
|
||||
[](https://github.com/yoshuawuyts/choo)
|
||||
```md
|
||||
[](https://github.com/yoshuawuyts/choo)
|
||||
[](https://github.com/yoshuawuyts/choo)
|
||||
```
|
||||
|
||||
## API
|
||||
@@ -449,9 +455,9 @@ arguments:
|
||||
and handlers in other models
|
||||
- __state:__ initial values of `state` inside the model
|
||||
- __reducers:__ synchronous operations that modify state. Triggered by
|
||||
`actions`. Signature of `(data, state)`.
|
||||
`actions`. Signature of `(state, data)`.
|
||||
- __effects:__ asynchronous operations that don't modify state directly.
|
||||
Triggered by `actions`, can call `actions`. Signature of `(data, state,
|
||||
Triggered by `actions`, can call `actions`. Signature of `(state, data,
|
||||
send, done)`
|
||||
- __subscriptions:__ asynchronous read-only operations that don't modify state
|
||||
directly. Can call `actions`. Signature of `(send, done)`.
|
||||
@@ -490,9 +496,9 @@ There are several `hooks` and `wrappers` that are picked up by `choo`:
|
||||
- __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(data, state, name, caller, createSend):__ called when an
|
||||
- __onAction(state, data, name, caller, createSend):__ called when an
|
||||
`action` is fired.
|
||||
- __onStateChange(data, state, prev, caller, createSend):__ called after a
|
||||
- __onStateChange(state, data, prev, caller, createSend):__ called after a
|
||||
reducer changes the `state`.
|
||||
- __wrapSubscriptions(fn):__ wraps a `subscription` to add custom behavior
|
||||
- __wrapReducers(fn):__ wraps a `reducer` to add custom behavior
|
||||
@@ -523,12 +529,9 @@ optional state object. When calling `.toString()` instead of `.start()`, all
|
||||
calls to `send()` are disabled, and `subscriptions`, `effects` and `reducers`
|
||||
aren't loaded.
|
||||
|
||||
### tree = app.start(rootId?, opts)
|
||||
### tree = app.start(opts)
|
||||
Start the application. Returns a tree of DOM nodes that can be mounted using
|
||||
`document.body.appendChild()`. If a valid `id` selector is passed in as the
|
||||
first argument, the tree will diff against the selected node rather than be
|
||||
returned. This is useful for [rehydration](https://github.com/yoshuawuyts/choo-handbook/blob/master/rendering-in-node.md#rehydration). Opts can contain the
|
||||
following values:
|
||||
`document.body.appendChild()`. Opts can contain the following values:
|
||||
- __opts.history:__ default: `true`. Enable a `subscription` to the browser
|
||||
history API. e.g. updates the internal `location.href` state whenever the
|
||||
browsers "forward" and "backward" buttons are pressed.
|
||||
@@ -549,10 +552,14 @@ current `state`, `prev` is the last state, `state.params` is URI partials and
|
||||
|
||||
To create listeners for events, create interpolated attributes on elements.
|
||||
```js
|
||||
const html = require('choo/html')
|
||||
var html = require('choo/html')
|
||||
html`
|
||||
<button onclick=${(e) => console.log(e)}>click for bananas</button>
|
||||
<button onclick=${log}>click for bananas</button>
|
||||
`
|
||||
|
||||
function log (e) {
|
||||
console.log(e)
|
||||
}
|
||||
```
|
||||
Example listeners include: `onclick`, `onsubmit`, `oninput`, `onkeydown`,
|
||||
`onkeyup`. A full list can be found [at the yo-yo
|
||||
@@ -660,8 +667,8 @@ Consider running some of the following:
|
||||
- [unassertify](https://github.com/twada/unassertify) - remove `assert()`
|
||||
statements which reduces file size. Use as a `--global` transform
|
||||
- [es2020](https://github.com/yoshuawuyts/es2020) - backport `const`,
|
||||
`fat-arrows` and `template strings` to older browsers. Should be run as a
|
||||
`--global` transform
|
||||
`arrow functions` 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`
|
||||
dependency with `document.createElement` calls; greatly speeds up performance
|
||||
too
|
||||
@@ -676,8 +683,8 @@ Consider running some of the following:
|
||||
### Choo + Internet Explorer & Safari
|
||||
Out of the box `choo` only supports runtimes which support:
|
||||
* `const`
|
||||
* `fat-arrow` functions (e.g. `() => {}`)
|
||||
* `template-strings`
|
||||
* `arrow functions` (e.g. `() => {}`)
|
||||
* `template strings`
|
||||
|
||||
This does not include Safari 9 or any version of IE. If support for these
|
||||
platforms is required you will have to provide some sort of transform that
|
||||
@@ -814,35 +821,18 @@ Become a backer, and buy us a coffee (or perhaps lunch?) every month or so.
|
||||
<a href="https://opencollective.com/choo/backer/28/website" target="_blank"><img src="https://opencollective.com/choo/backer/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/29/website" target="_blank"><img src="https://opencollective.com/choo/backer/29/avatar.svg"></a>
|
||||
|
||||
|
||||
## License
|
||||
[MIT](https://tldrlegal.com/license/mit-license)
|
||||
|
||||
[bankai]: https://github.com/yoshuawuyts/bankai
|
||||
[bel]: https://github.com/shama/bel
|
||||
[big-o]: https://rob-bell.net/2009/06/a-beginners-guide-to-big-o-notation/
|
||||
[bl]: https://github.com/rvagg/bl
|
||||
[browserify]: https://github.com/substack/node-browserify
|
||||
[budo]: https://github.com/mattdesl/budo
|
||||
[DOM]: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
|
||||
[dom]: https://en.wikipedia.org/wiki/Document_Object_Model
|
||||
[es2020]: https://github.com/yoshuawuyts/es2020
|
||||
[handbook]: https://github.com/yoshuawuyts/choo-handbook
|
||||
[html-input]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
|
||||
[hyperx]: https://github.com/substack/hyperx
|
||||
[inu]: https://github.com/ahdinosaur/inu
|
||||
[isomorphic]: https://en.wikipedia.org/wiki/Isomorphism
|
||||
[keyboard-support]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Browser_compatibility
|
||||
[localstorage]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
|
||||
[module-parent]: https://nodejs.org/dist/latest-v6.x/docs/api/modules.html#modules_module_parent
|
||||
[morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks
|
||||
[morphdom]: https://github.com/patrick-steele-idem/morphdom
|
||||
[nginx]: http://nginx.org/
|
||||
[qps]: https://en.wikipedia.org/wiki/Queries_per_second
|
||||
[sheet-router]: https://github.com/yoshuawuyts/sheet-router
|
||||
[sse-reconnect]: http://stackoverflow.com/questions/24564030/is-an-eventsource-sse-supposed-to-try-to-reconnect-indefinitely
|
||||
[sse]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||
[varnish]: https://varnish-cache.org
|
||||
[ws-reconnect]: http://stackoverflow.com/questions/13797262/how-to-reconnect-to-websocket-after-close-connection
|
||||
[ws]: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
|
||||
[xhr]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
|
||||
[yo-yo]: https://github.com/maxogden/yo-yo
|
||||
|
||||
@@ -8,20 +8,20 @@ app.model({
|
||||
counter: 0
|
||||
},
|
||||
reducers: {
|
||||
increment: (data, state) => ({ counter: state.counter + 1 }),
|
||||
decrement: (data, state) => ({ counter: state.counter - 1 })
|
||||
increment: (state, data) => ({ counter: state.counter + 1 }),
|
||||
decrement: (state, data) => ({ counter: state.counter - 1 })
|
||||
},
|
||||
effects: {
|
||||
incrementAsync: function (data, state, send, done) {
|
||||
incrementAsync: function (state, data, send, done) {
|
||||
setTimeout(() => send('increment', done), 1000)
|
||||
},
|
||||
decrementAsync: function (data, state, send, done) {
|
||||
decrementAsync: function (state, data, send, done) {
|
||||
setTimeout(() => send('decrement', done), 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const mainView = (state, prev, send) => {
|
||||
function mainView (state, prev, send) {
|
||||
const count = state.counter
|
||||
|
||||
return html`
|
||||
@@ -36,9 +36,7 @@ const mainView = (state, prev, send) => {
|
||||
`
|
||||
}
|
||||
|
||||
app.router((route) => [
|
||||
route('/', mainView)
|
||||
])
|
||||
app.router([ '/', mainView ])
|
||||
|
||||
const tree = app.start()
|
||||
document.body.appendChild(tree)
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"description": "",
|
||||
"main": "client.js",
|
||||
"scripts": {
|
||||
"start": "budo client.js -p 8080"
|
||||
"start": "bankai start --entry=client -p 8080"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Juan Soto <juan@juansoto.me>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"budo": "^8.3.0",
|
||||
"bankai": "^3.2.0",
|
||||
"plur": "^2.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,19 @@ const mainView = require('./views/main')
|
||||
|
||||
const app = choo({
|
||||
onError: function (err, state, createSend) {
|
||||
console.trace()
|
||||
console.groupCollapsed(`Error: ${err.message}`)
|
||||
console.error(err)
|
||||
console.groupEnd()
|
||||
const send = createSend('onError: ')
|
||||
send('app:error', err)
|
||||
},
|
||||
onAction: function (data, state, name, caller, createSend) {
|
||||
onAction: function (state, data, name, caller, createSend) {
|
||||
console.groupCollapsed(`Action: ${caller} -> ${name}`)
|
||||
console.log(data)
|
||||
console.groupEnd()
|
||||
},
|
||||
onStateChange: function (data, state, prev, createSend) {
|
||||
onStateChange: function (state, data, prev, createSend) {
|
||||
console.groupCollapsed('State')
|
||||
console.log(prev)
|
||||
console.log(state)
|
||||
@@ -26,9 +27,7 @@ const app = choo({
|
||||
app.model(require('./models/error'))
|
||||
app.model(require('./models/api'))
|
||||
|
||||
app.router((route) => [
|
||||
route('/', mainView)
|
||||
])
|
||||
app.router(['/', mainView])
|
||||
|
||||
const tree = app.start()
|
||||
document.body.appendChild(tree)
|
||||
|
||||
@@ -6,13 +6,13 @@ module.exports = {
|
||||
title: 'Button pushing machine 3000'
|
||||
},
|
||||
reducers: {
|
||||
set: (data, state) => ({ 'title': data })
|
||||
set: (state, data) => ({ 'title': data })
|
||||
},
|
||||
effects: {
|
||||
good: function (data, state, send, done) {
|
||||
good: function (state, data, send, done) {
|
||||
request('/good', send, done)
|
||||
},
|
||||
bad: (data, state, send, done) => request('/bad', send, done)
|
||||
bad: (state, data, send, done) => request('/bad', send, done)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ module.exports = {
|
||||
triggerTime: null
|
||||
},
|
||||
reducers: {
|
||||
setError: function (data, state) {
|
||||
setError: function (state, data) {
|
||||
return {
|
||||
errors: state.errors.concat(data.message),
|
||||
errorTimeDone: data.errorTimeDone
|
||||
}
|
||||
},
|
||||
'delError': function (data, state) {
|
||||
'delError': function (state, data) {
|
||||
state.errors.shift()
|
||||
return { errors: state.errors }
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
const choo = require('../../')
|
||||
const log = require('choo-log')
|
||||
const sf = require('sheetify')
|
||||
|
||||
const choo = require('../../')
|
||||
|
||||
sf('css-wipe/dest/bundle')
|
||||
|
||||
const app = choo()
|
||||
app.use(log())
|
||||
|
||||
app.model(require('./models/inbox'))
|
||||
app.model(require('./models/spam'))
|
||||
app.model(require('./models/sent'))
|
||||
|
||||
app.router((route) => [
|
||||
route('/', require('./views/empty')),
|
||||
route('/:mailbox', require('./views/mailbox'), [
|
||||
route('/:message', require('./views/email'))
|
||||
])
|
||||
app.router([
|
||||
['/', require('./views/empty')],
|
||||
['/:mailbox', require('./views/mailbox'), [
|
||||
['/:message', require('./views/email')]
|
||||
]]
|
||||
])
|
||||
|
||||
const tree = app.start()
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"author": "Yoshua Wuyts <i@yoshuawuyts.com>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"choo-log": "^1.4.1",
|
||||
"css-wipe": "^4.2.1",
|
||||
"dateformat": "^1.0.12",
|
||||
"pathname-match": "^1.1.3",
|
||||
|
||||
@@ -7,6 +7,7 @@ const nav = require('../elements/nav')
|
||||
module.exports = function (state, prev, send) {
|
||||
return html`
|
||||
<main class="mw5 mw7-ns center cf">
|
||||
<button onclick=${goHome}>Home</button>
|
||||
${pathname(state, prev, send)}
|
||||
${nav(state, prev, send)}
|
||||
<section class="fl mt4 w-80 db">
|
||||
@@ -14,4 +15,8 @@ module.exports = function (state, prev, send) {
|
||||
</section>
|
||||
</main>
|
||||
`
|
||||
|
||||
function goHome () {
|
||||
send('location:set', { pathname: '/' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const mount = require('../../mount')
|
||||
const choo = require('../../')
|
||||
|
||||
const mainView = require('./views/main')
|
||||
const mainView = require('./view')
|
||||
|
||||
const app = choo()
|
||||
|
||||
@@ -12,12 +13,11 @@ app.model({
|
||||
}
|
||||
})
|
||||
|
||||
app.router((route) => [
|
||||
route('/', mainView)
|
||||
])
|
||||
app.router(['/', mainView])
|
||||
|
||||
if (module.parent) {
|
||||
module.exports = app
|
||||
} else {
|
||||
app.start('#app-root')
|
||||
const tree = app.start()
|
||||
mount('#app-root', tree)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const assert = require('assert')
|
||||
const html = require('../../../html')
|
||||
const html = require('../../html')
|
||||
|
||||
module.exports = function (state, prev, send) {
|
||||
const serverMessage = state.message.server
|
||||
+8
-10
@@ -3,14 +3,12 @@ const html = require('../../html')
|
||||
|
||||
const app = choo()
|
||||
app.model(createModel())
|
||||
app.router((route) => [
|
||||
route('/', mainView)
|
||||
])
|
||||
app.router(['/', mainView])
|
||||
|
||||
const tree = app.start()
|
||||
document.body.appendChild(tree)
|
||||
|
||||
function mainView (params, state, send) {
|
||||
function mainView (state, prev, send) {
|
||||
return html`
|
||||
<div>${state.logger.msg}</div>
|
||||
`
|
||||
@@ -24,27 +22,27 @@ function createModel () {
|
||||
msg: ''
|
||||
},
|
||||
subscriptions: [
|
||||
function (send) {
|
||||
function (send, done) {
|
||||
stream.onerror = (e) => {
|
||||
send('logger:error', { payload: JSON.stringify(e) })
|
||||
send('logger:error', { payload: JSON.stringify(e) }, done)
|
||||
}
|
||||
stream.onmessage = (e) => {
|
||||
const msg = JSON.parse(e.data).message
|
||||
send('logger:print', { payload: msg })
|
||||
send('logger:print', { payload: msg }, done)
|
||||
}
|
||||
}
|
||||
],
|
||||
reducers: {
|
||||
'print': (data, state) => {
|
||||
'print': (state, data) => {
|
||||
return ({ msg: state.msg + ' ' + data.payload })
|
||||
}
|
||||
},
|
||||
effects: {
|
||||
close: (data, state, send, done) => {
|
||||
close: (state, data, send, done) => {
|
||||
stream.close()
|
||||
done()
|
||||
},
|
||||
error: (data, state, send, done) => {
|
||||
error: (state, data, send, done) => {
|
||||
console.error(`error: ${data.payload}`)
|
||||
done()
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@ const app = choo()
|
||||
|
||||
app.model(stopwatch)
|
||||
|
||||
app.router((route) => [
|
||||
route('/', mainView)
|
||||
])
|
||||
app.router(['/', mainView])
|
||||
|
||||
const tree = app.start()
|
||||
document.body.appendChild(tree)
|
||||
|
||||
@@ -8,14 +8,14 @@ module.exports = {
|
||||
laps: []
|
||||
},
|
||||
reducers: {
|
||||
start: (data, state) => ({ start: true, startTime: Date.now() - state.elapsed }),
|
||||
stop: (data, state) => ({ start: false }),
|
||||
update: (data, state) => ({ elapsed: data }),
|
||||
reset: (data, state) => ({ startTime: Date.now(), elapsed: 0, laps: [] }),
|
||||
add: (data, state) => ({ laps: state.laps.concat(data) })
|
||||
start: (state, data) => ({ start: true, startTime: Date.now() - state.elapsed }),
|
||||
stop: (state, data) => ({ start: false }),
|
||||
update: (state, data) => ({ elapsed: data }),
|
||||
reset: (state, data) => ({ startTime: Date.now(), elapsed: 0, laps: [] }),
|
||||
add: (state, data) => ({ laps: state.laps.concat(data) })
|
||||
},
|
||||
effects: {
|
||||
now: (data, state, send, done) => {
|
||||
now: (state, data, send, done) => {
|
||||
if (state.start) {
|
||||
let elapsed = data - state.startTime
|
||||
send('update', elapsed, done)
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"description": "",
|
||||
"main": "client.js",
|
||||
"scripts": {
|
||||
"start": "budo client.js -p 8080 -- -t es2020"
|
||||
"start": "bankai start --entry=client.js -p 8080"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "traducer <traducer21@gmail.com>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"budo": "^8.3.0",
|
||||
"bankai": "^3.2.0",
|
||||
"raf": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -8,10 +8,10 @@ app.model({
|
||||
title: 'my demo app'
|
||||
},
|
||||
reducers: {
|
||||
update: (data, state) => ({ title: data.payload })
|
||||
update: (state, data) => ({ title: data.payload })
|
||||
},
|
||||
effects: {
|
||||
update: (data, state, send, done) => {
|
||||
update: (state, data, send, done) => {
|
||||
document.title = data.payload
|
||||
done()
|
||||
}
|
||||
@@ -31,9 +31,6 @@ const mainView = (state, prev, send) => {
|
||||
`
|
||||
}
|
||||
|
||||
app.router((route) => [
|
||||
route('/', mainView)
|
||||
])
|
||||
|
||||
app.router(['/', mainView])
|
||||
const tree = app.start()
|
||||
document.body.appendChild(tree)
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
"description": "",
|
||||
"main": "client.js",
|
||||
"scripts": {
|
||||
"start": "budo client.js -p 8080"
|
||||
"start": "bankai start --entry=client.js -p 8080 --open"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Yoshua Wuyts <i@yoshuawuyts.com>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"budo": "^8.3.0"
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"bankai": "^3.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,10 @@
|
||||
|
||||
app.model({
|
||||
namespace: 'counter',
|
||||
state: {
|
||||
count: 0
|
||||
},
|
||||
state: { count: 0 },
|
||||
reducers: {
|
||||
increment: (data, state) => ({count: state.count + 1}),
|
||||
decrement: (data, state) => ({count: state.count - 1})
|
||||
increment: (state, data) => ({count: state.count + 1}),
|
||||
decrement: (state, data) => ({count: state.count - 1})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,10 +30,7 @@
|
||||
`
|
||||
}
|
||||
|
||||
app.router((route) => [
|
||||
route('/', mainView)
|
||||
])
|
||||
|
||||
app.router(['/', mainView])
|
||||
const tree = app.start()
|
||||
document.body.appendChild(tree)
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
const history = require('sheet-router/history')
|
||||
const createLocation = require('sheet-router/create-location')
|
||||
const onHistoryChange = require('sheet-router/history')
|
||||
const sheetRouter = require('sheet-router')
|
||||
const document = require('global/document')
|
||||
const onReady = require('document-ready')
|
||||
const href = require('sheet-router/href')
|
||||
const hash = require('sheet-router/hash')
|
||||
const hashMatch = require('hash-match')
|
||||
const onHref = require('sheet-router/href')
|
||||
const walk = require('sheet-router/walk')
|
||||
const mutate = require('xtend/mutable')
|
||||
const barracks = require('barracks')
|
||||
const nanoraf = require('nanoraf')
|
||||
const assert = require('assert')
|
||||
@@ -20,7 +19,7 @@ function choo (opts) {
|
||||
|
||||
const _store = start._store = barracks()
|
||||
var _router = start._router = null
|
||||
var _defaultRoute = null
|
||||
var _routerOpts = null
|
||||
var _rootNode = null
|
||||
var _routes = null
|
||||
var _frame = null
|
||||
@@ -45,7 +44,7 @@ function choo (opts) {
|
||||
_store.start({ subscriptions: false, reducers: false, effects: false })
|
||||
|
||||
const state = _store.state({ state: serverState })
|
||||
const router = createRouter(_defaultRoute, _routes, createSend)
|
||||
const router = createRouter(_routerOpts, _routes, createSend)
|
||||
const tree = router(route, state)
|
||||
return tree.outerHTML || tree.toString()
|
||||
|
||||
@@ -58,38 +57,31 @@ function choo (opts) {
|
||||
|
||||
// start the application
|
||||
// (str?, obj?) -> DOMNode
|
||||
function start (selector, startOpts) {
|
||||
if (!startOpts && typeof selector !== 'string') {
|
||||
startOpts = selector
|
||||
selector = null
|
||||
}
|
||||
startOpts = startOpts || {}
|
||||
|
||||
_store.model(appInit(startOpts))
|
||||
const createSend = _store.start(startOpts)
|
||||
_router = start._router = createRouter(_defaultRoute, _routes, createSend)
|
||||
function start () {
|
||||
_store.model(createLocationModel(opts))
|
||||
const createSend = _store.start(opts)
|
||||
_router = start._router = createRouter(_routerOpts, _routes, createSend)
|
||||
const state = _store.state({state: {}})
|
||||
|
||||
if (!selector) {
|
||||
const tree = _router(state.location.pathname, state)
|
||||
const tree = _router(state.location.href, state)
|
||||
_rootNode = tree
|
||||
tree.done = done
|
||||
|
||||
return tree
|
||||
} else {
|
||||
onReady(function onReady () {
|
||||
const oldTree = document.querySelector(selector)
|
||||
assert.ok(oldTree, 'could not query selector: ' + selector)
|
||||
const newTree = _router(state.location.pathname, state)
|
||||
_rootNode = yo.update(oldTree, newTree)
|
||||
})
|
||||
|
||||
// allow a 'mount' function to return the new node
|
||||
// html -> null
|
||||
function done (newNode) {
|
||||
_rootNode = newNode
|
||||
}
|
||||
}
|
||||
|
||||
// update the DOM after every state mutation
|
||||
// (obj, obj, obj, str, fn) -> null
|
||||
function render (data, state, prev, name, createSend) {
|
||||
function render (state, data, prev, name, createSend) {
|
||||
if (!_frame) {
|
||||
_frame = nanoraf(function (state, prev) {
|
||||
const newTree = _router(state.location.pathname, state, prev)
|
||||
const newTree = _router(state.location.href, state, prev)
|
||||
_rootNode = yo.update(_rootNode, newTree)
|
||||
})
|
||||
}
|
||||
@@ -99,7 +91,7 @@ function choo (opts) {
|
||||
// register all routes on the router
|
||||
// (str?, [fn|[fn]]) -> obj
|
||||
function router (defaultRoute, routes) {
|
||||
_defaultRoute = defaultRoute
|
||||
_routerOpts = defaultRoute
|
||||
_routes = routes
|
||||
}
|
||||
|
||||
@@ -117,70 +109,103 @@ function choo (opts) {
|
||||
}
|
||||
|
||||
// create a new router with a custom `createRoute()` function
|
||||
// (str?, obj, fn?) -> null
|
||||
function createRouter (defaultRoute, routes, createSend) {
|
||||
var prev = { params: {} }
|
||||
return sheetRouter(defaultRoute, routes, createRoute)
|
||||
|
||||
function createRoute (routeFn) {
|
||||
return function (route, inline, child) {
|
||||
if (typeof inline === 'function') {
|
||||
inline = wrap(inline, route)
|
||||
}
|
||||
return routeFn(route, inline, child)
|
||||
// (str?, obj) -> null
|
||||
function createRouter (routerOpts, routes, createSend) {
|
||||
var prev = null
|
||||
if (!routes) {
|
||||
routes = routerOpts
|
||||
routerOpts = {}
|
||||
}
|
||||
routerOpts = mutate({ thunk: 'match' }, routerOpts)
|
||||
const router = sheetRouter(routerOpts, routes)
|
||||
walk(router, wrap)
|
||||
|
||||
function wrap (child, route) {
|
||||
return router
|
||||
|
||||
function wrap (route, handler) {
|
||||
const send = createSend('view: ' + route, true)
|
||||
return function chooWrap (params, state) {
|
||||
return function chooWrap (params) {
|
||||
return function (state) {
|
||||
const nwPrev = prev
|
||||
const nwState = prev = xtend(state, { params: params })
|
||||
prev = state
|
||||
|
||||
// TODO(yw): find a way to wrap handlers so params shows up in state
|
||||
const nwState = xtend(state)
|
||||
nwState.location = xtend(nwState.location, { params: params })
|
||||
|
||||
if (opts.freeze !== false) Object.freeze(nwState)
|
||||
return child(nwState, nwPrev, send)
|
||||
return handler(nwState, nwPrev, send)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initial application state model
|
||||
// application location model
|
||||
// obj -> obj
|
||||
function appInit (opts) {
|
||||
const loc = document.location
|
||||
const state = { pathname: (opts.hash) ? hashMatch(loc.hash) : loc.href }
|
||||
const reducers = {
|
||||
setLocation: function setLocation (data, state) {
|
||||
return { pathname: data.location.replace(/#.*/, '') }
|
||||
}
|
||||
}
|
||||
// if hash routing explicitly enabled, subscribe to it
|
||||
const subs = {}
|
||||
if (opts.hash === true) {
|
||||
pushLocationSub(function (navigate) {
|
||||
hash(function (fragment) {
|
||||
navigate(hashMatch(fragment))
|
||||
})
|
||||
}, 'handleHash', subs)
|
||||
} else {
|
||||
if (opts.history !== false) pushLocationSub(history, 'handleHistory', subs)
|
||||
if (opts.href !== false) pushLocationSub(href, 'handleHref', subs)
|
||||
}
|
||||
|
||||
function createLocationModel (opts) {
|
||||
return {
|
||||
namespace: 'location',
|
||||
subscriptions: subs,
|
||||
reducers: reducers,
|
||||
state: state
|
||||
state: mutate(createLocation(), { params: {} }),
|
||||
subscriptions: createSubscriptions(opts),
|
||||
effects: { set: setLocation, touch: touchLocation },
|
||||
reducers: { update: updateLocation }
|
||||
}
|
||||
|
||||
// create a new subscription that modifies
|
||||
// 'app:location' and push it to be loaded
|
||||
// (fn, obj) -> null
|
||||
function pushLocationSub (cb, key, model) {
|
||||
model[key] = function (send, done) {
|
||||
cb(function navigate (pathname) {
|
||||
send('location:setLocation', { location: pathname }, done)
|
||||
// update the location on the state
|
||||
// try and jump to an anchor on the page if it exists
|
||||
// (obj, obj) -> obj
|
||||
function updateLocation (state, data) {
|
||||
if (opts.history !== false && data.hash && data.hash !== state.hash) {
|
||||
try {
|
||||
const el = document.querySelector(data.hash)
|
||||
if (el) el.scrollIntoView(true)
|
||||
} catch (e) {
|
||||
return data
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// update internal location only
|
||||
// (str, obj, fn, fn) -> null
|
||||
function touchLocation (state, data, send, done) {
|
||||
const newLocation = createLocation(state, data)
|
||||
send('location:update', newLocation, done)
|
||||
}
|
||||
|
||||
// set a new location e.g. "/foo/bar#baz?beep=boop"
|
||||
// (str, obj, fn, fn) -> null
|
||||
function setLocation (state, data, send, done) {
|
||||
const newLocation = createLocation(state, data)
|
||||
|
||||
// update url bar if it changed
|
||||
if (opts.history !== false && newLocation.href !== state.href) {
|
||||
window.history.pushState({}, null, newLocation.href)
|
||||
}
|
||||
|
||||
send('location:update', newLocation, done)
|
||||
}
|
||||
|
||||
function createSubscriptions (opts) {
|
||||
const subs = {}
|
||||
|
||||
if (opts.history !== false) {
|
||||
subs.handleHistory = function (send, done) {
|
||||
onHistoryChange(function navigate (href) {
|
||||
send('location:touch', href, done)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.href !== false) {
|
||||
subs.handleHref = function (send, done) {
|
||||
onHref(function navigate (location) {
|
||||
send('location:set', location, done)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return subs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// mount.js
|
||||
const documentReady = require('document-ready')
|
||||
const assert = require('assert')
|
||||
const yo = require('yo-yo')
|
||||
|
||||
module.exports = mount
|
||||
|
||||
// (str, html) -> null
|
||||
function mount (selector, newTree) {
|
||||
assert.equal(typeof selector, 'string', 'choo/mount: selector should be a string')
|
||||
assert.equal(typeof newTree, 'object', 'choo/mount: newTree should be an object')
|
||||
|
||||
const done = newTree.done
|
||||
|
||||
documentReady(function onReady () {
|
||||
const _rootNode = document.querySelector(selector)
|
||||
assert.ok(_rootNode, 'could not query selector: ' + selector)
|
||||
|
||||
// copy script tags from the old newTree to the new newTree so
|
||||
// we can pass a <body> element straight up
|
||||
if (_rootNode.nodeName === 'BODY') {
|
||||
const children = _rootNode.childNodes
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
if (children[i].nodeName === 'SCRIPT') {
|
||||
newTree.appendChild(children[i].cloneNode(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newNode = yo.update(_rootNode, newTree)
|
||||
assert.equal(newNode, _rootNode, 'choo/mount: The DOM node: \n' +
|
||||
newNode.outerHTML + '\n is not equal to \n' + newTree.outerHTML +
|
||||
'choo cannot begin diffing.' +
|
||||
' Make sure the same initial tree is rendered in the browser' +
|
||||
' as on the server. Check out the choo handbook for more information')
|
||||
|
||||
// pass the node we mounted on back into choo
|
||||
if (done) done(newNode)
|
||||
})
|
||||
}
|
||||
+5
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "choo",
|
||||
"version": "3.3.0",
|
||||
"version": "4.0.0-7",
|
||||
"description": "A 5kb framework for creating sturdy frontend applications",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -30,19 +30,17 @@
|
||||
],
|
||||
"files": [
|
||||
"index.js",
|
||||
"http.js",
|
||||
"mount.js",
|
||||
"html.js",
|
||||
"dist/"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"barracks": "^8.3.1",
|
||||
"barracks": "^9.0.0",
|
||||
"document-ready": "~1.0.2",
|
||||
"global": "^4.3.0",
|
||||
"hash-match": "^1.0.2",
|
||||
"nanoraf": "^2.1.1",
|
||||
"sheet-router": "^3.1.0",
|
||||
"xhr": "^2.2.0",
|
||||
"sheet-router": "^4.0.1",
|
||||
"xtend": "^4.0.1",
|
||||
"yo-yo": "^1.2.2"
|
||||
},
|
||||
@@ -68,6 +66,7 @@
|
||||
"server-router": "^3.0.0",
|
||||
"sheetify": "^5.1.0",
|
||||
"standard": "^8.0.0",
|
||||
"standard-markdown": "^2.2.0",
|
||||
"tachyons": "^4.0.0-beta.19",
|
||||
"tape": "^4.5.1",
|
||||
"tape-istanbul": "~1.0.2",
|
||||
|
||||
+5
-2
@@ -8,7 +8,8 @@ usage () {
|
||||
}
|
||||
|
||||
lint () {
|
||||
standard
|
||||
standard > /dev/tty
|
||||
standard-markdown > /dev/tty
|
||||
}
|
||||
|
||||
test_electron () {
|
||||
@@ -52,7 +53,8 @@ test_browser_local () {
|
||||
check_deps () {
|
||||
dependency-check . --entry 'index.js'
|
||||
dependency-check . --entry 'index.js' --extra --no-dev \
|
||||
-i xhr
|
||||
-i document-ready \
|
||||
-i global
|
||||
}
|
||||
|
||||
# set CLI flags
|
||||
@@ -70,6 +72,7 @@ case "$1" in
|
||||
b|browser) shift; test_browser "$@";;
|
||||
l|browser-local) shift; test_browser_local "$@";;
|
||||
s|server) shift; test_server "$@";;
|
||||
lint) shift; lint "$@";;
|
||||
c|cov) shift; test_cov "$@";;
|
||||
d|deps) shift; check_deps "$@";;
|
||||
esac
|
||||
|
||||
+12
-9
@@ -1,7 +1,8 @@
|
||||
const test = require('tape')
|
||||
const append = require('append-child')
|
||||
const test = require('tape')
|
||||
|
||||
const choo = require('../../')
|
||||
const view = require('../../html')
|
||||
const html = require('../../html')
|
||||
|
||||
test('state is immutable', function (t) {
|
||||
t.plan(4)
|
||||
@@ -16,16 +17,16 @@ test('state is immutable', function (t) {
|
||||
state: state,
|
||||
namespace: 'test',
|
||||
reducers: {
|
||||
'no-reducer-mutate': (data, state) => {
|
||||
'no-reducer-mutate': (state, data) => {
|
||||
return {}
|
||||
},
|
||||
'mutate-on-return': (data, state) => {
|
||||
'mutate-on-return': (state, data) => {
|
||||
delete data.type
|
||||
return data
|
||||
}
|
||||
},
|
||||
effects: {
|
||||
'triggers-reducers': (data, state, send, done) => {
|
||||
'triggers-reducers': (state, data, send, done) => {
|
||||
send('test:mutate-on-return', {beep: 'barp'}, done)
|
||||
}
|
||||
}
|
||||
@@ -46,13 +47,15 @@ test('state is immutable', function (t) {
|
||||
(send) => send('test:triggers-reducers')
|
||||
]
|
||||
|
||||
app.router((route) => [
|
||||
route('/', function (state, prev, send) {
|
||||
app.router([
|
||||
['/', function (state, prev, send) {
|
||||
++loop
|
||||
asserts[loop] && asserts[loop](state.test)
|
||||
setTimeout(() => triggers[loop] && triggers[loop](send), 5)
|
||||
return view`<div><span class="test">${state.foo}:${state.beep}</span></div>`
|
||||
})
|
||||
return html`
|
||||
<div><span class="test">${state.foo}:${state.beep}</span></div>
|
||||
`
|
||||
}]
|
||||
])
|
||||
|
||||
const tree = app.start()
|
||||
|
||||
@@ -11,14 +11,12 @@ test('freeze (default)', function (t) {
|
||||
}
|
||||
})
|
||||
|
||||
app.router((route) => [
|
||||
route('/', function (state, prev, send) {
|
||||
app.router(['/', function (state, prev, send) {
|
||||
state.foo = ''
|
||||
t.equal(state.foo, 'bar', 'cannot modify property')
|
||||
state.bar = 'baz'
|
||||
t.equal(state.bar, undefined, 'cannot add property')
|
||||
})
|
||||
])
|
||||
}])
|
||||
|
||||
app.start()
|
||||
})
|
||||
@@ -33,14 +31,12 @@ test('noFreeze', function (t) {
|
||||
}
|
||||
})
|
||||
|
||||
app.router((route) => [
|
||||
route('/', function (state, prev, send) {
|
||||
app.router(['/', function (state, prev, send) {
|
||||
state.foo = ''
|
||||
t.equal(state.foo, '', 'can modify property')
|
||||
state.bar = 'baz'
|
||||
t.equal(state.bar, 'baz', 'can add property')
|
||||
})
|
||||
])
|
||||
}])
|
||||
|
||||
app.start()
|
||||
})
|
||||
|
||||
@@ -10,15 +10,15 @@ test('hooks', function (t) {
|
||||
onError: function (err) {
|
||||
t.equal(err.message, 'effect error', 'onError: receives err')
|
||||
},
|
||||
onAction: function (data, state, name, caller, createSend) {
|
||||
onAction: function (state, data, name, caller, createSend) {
|
||||
if (name === 'explodes') return
|
||||
t.deepEqual(data, {foo: 'bar'}, 'onAction: action data')
|
||||
t.equal(state.clicks, 0, 'onAction: current state: 0 clicks')
|
||||
t.equal(name, 'click', 'onAction: action name')
|
||||
t.equal(name, 'click', 'onAction: data name')
|
||||
t.equal(caller, 'view: /', 'onAction: caller name')
|
||||
t.equal(typeof createSend, 'function', 'onAction: createSend fn')
|
||||
},
|
||||
onStateChange: function (data, state, prev, createSend) {
|
||||
onStateChange: function (state, data, prev, createSend) {
|
||||
t.deepEqual(data, {foo: 'bar'}, 'onState: action data')
|
||||
t.deepEqual(state.clicks, 1, 'onState: new state: 1 clicks')
|
||||
t.deepEqual(prev.clicks, 0, 'onState: prev state: 0 clicks')
|
||||
@@ -30,26 +30,24 @@ test('hooks', function (t) {
|
||||
clicks: 0
|
||||
},
|
||||
reducers: {
|
||||
click: (data, state) => ({clicks: state.clicks + 1})
|
||||
click: (state, data) => ({clicks: state.clicks + 1})
|
||||
},
|
||||
effects: {
|
||||
explodes: (data, state, send, done) => {
|
||||
explodes: (state, data, send, done) => {
|
||||
setTimeout(() => done(new Error('effect error')), 5)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var sent = false
|
||||
app.router((route) => [
|
||||
route('/', function (state, prev, send) {
|
||||
app.router(['/', function (state, prev, send) {
|
||||
if (!sent) {
|
||||
send('click', {foo: 'bar'})
|
||||
send('explodes')
|
||||
}
|
||||
sent = true
|
||||
return view`<span></span>`
|
||||
})
|
||||
])
|
||||
}])
|
||||
|
||||
const tree = app.start()
|
||||
t.on('end', append(tree))
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
const test = require('tape')
|
||||
const onReady = require('document-ready')
|
||||
const append = require('append-child')
|
||||
const mount = require('../../mount')
|
||||
const choo = require('../../')
|
||||
const view = require('../../html')
|
||||
const html = require('../../html')
|
||||
|
||||
test('rehydration', function (t) {
|
||||
t.plan(2)
|
||||
|
||||
const app = choo()
|
||||
|
||||
app.router((route) => [
|
||||
route('/', function (state, prev, send) {
|
||||
return view`<div id="app-root" onclick=${() => send('test')}>Hello world!</span>`
|
||||
})
|
||||
])
|
||||
const node = html`
|
||||
<section id="app-root">
|
||||
<div id="app-root">Hello squirrel!</span>
|
||||
</section>
|
||||
`
|
||||
|
||||
var node = document.createElement('div')
|
||||
node.innerHTML = app.toString('/')
|
||||
node = node.childNodes[0]
|
||||
t.on('end', append(node))
|
||||
app.router(['/', function (state, prev, send) {
|
||||
return html`
|
||||
<section id="app-root">
|
||||
<div onclick=${() => send('test')}>Hello world!</span>
|
||||
</section>
|
||||
`
|
||||
}])
|
||||
|
||||
app.start('#app-root')
|
||||
append(node)
|
||||
|
||||
const tree = app.start()
|
||||
mount('#app-root', tree)
|
||||
|
||||
onReady(function () {
|
||||
t.equal(node.innerHTML, 'Hello world!', 'same content')
|
||||
t.equal(typeof node.onclick, 'function', 'attaches dom listeners')
|
||||
const newNode = document.querySelector('#app-root')
|
||||
const el = newNode.children[0]
|
||||
t.equal(el.innerHTML, 'Hello world!', 'same as it ever was')
|
||||
t.equal(typeof el.onclick, 'function', 'attaches dom listeners')
|
||||
})
|
||||
})
|
||||
|
||||
+70
-70
@@ -6,7 +6,7 @@ const view = require('../../html')
|
||||
|
||||
test('routing', function (t) {
|
||||
t.test('history', function (t) {
|
||||
t.plan(3)
|
||||
t.plan(2)
|
||||
|
||||
const history = Event()
|
||||
const choo = proxyquire('../..', {
|
||||
@@ -20,23 +20,23 @@ test('routing', function (t) {
|
||||
user: null
|
||||
},
|
||||
reducers: {
|
||||
set: (data, state) => ({user: data.id})
|
||||
set: (state, data) => ({user: data.id})
|
||||
},
|
||||
effects: {
|
||||
open: function (data, state, send, done) {
|
||||
open: function (state, data, send, done) {
|
||||
t.deepEqual(data, {id: 1})
|
||||
send('set', {id: 1}, function (err) {
|
||||
if (err) return done(err)
|
||||
history.broadcast('https://foo.com/users/1')
|
||||
history.broadcast('/users/1')
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.router('/users', (route) => [
|
||||
route('/users', parentView, [
|
||||
route('/:user', childView)
|
||||
])
|
||||
app.router({ default: '/users' }, [
|
||||
['/users', parentView, [
|
||||
['/:user', childView]
|
||||
]]
|
||||
])
|
||||
|
||||
const tree = app.start()
|
||||
@@ -59,74 +59,76 @@ test('routing', function (t) {
|
||||
}
|
||||
})
|
||||
|
||||
t.test('hash', function (t) {
|
||||
t.plan(1)
|
||||
// t.test('hash', function (t) {
|
||||
// t.plan(1)
|
||||
|
||||
const hash = Event()
|
||||
const choo = proxyquire('../..', {
|
||||
'sheet-router/hash': hash.listen
|
||||
})
|
||||
// resetLocation()
|
||||
// const hash = Event()
|
||||
// const choo = proxyquire('../..', {
|
||||
// 'sheet-router/hash': hash.listen
|
||||
// })
|
||||
|
||||
const app = choo()
|
||||
// const app = choo({hash: true})
|
||||
|
||||
app.model({
|
||||
state: {
|
||||
user: null
|
||||
},
|
||||
reducers: {
|
||||
set: (data, state) => ({user: data.id})
|
||||
},
|
||||
effects: {
|
||||
open: function (data, state, send, done) {
|
||||
send('set', {id: 1}, function (err) {
|
||||
if (err) return done(err)
|
||||
hash.broadcast('#users/1')
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
// app.model({
|
||||
// state: {
|
||||
// user: null
|
||||
// },
|
||||
// reducers: {
|
||||
// set: (state, data) => ({user: action.id})
|
||||
// },
|
||||
// effects: {
|
||||
// open: function (state, data, send, done) {
|
||||
// send('set', {id: 1}, function (err) {
|
||||
// if (err) return done(err)
|
||||
// hash.broadcast('#users/1')
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
app.router('/users', (route) => [
|
||||
route('/users', parentView, [
|
||||
route('/:user', childView)
|
||||
])
|
||||
])
|
||||
// app.router({ default: '/users' }, [
|
||||
// ['/users', parentView, [
|
||||
// ['/:user', childView]
|
||||
// ]]
|
||||
// ])
|
||||
|
||||
const tree = app.start({hash: true})
|
||||
t.on('end', append(tree))
|
||||
// const tree = app.start()
|
||||
// t.on('end', append(tree))
|
||||
|
||||
tree.onclick()
|
||||
// tree.onclick()
|
||||
|
||||
function parentView (state, prev, send) {
|
||||
return view`
|
||||
<button onclick=${() => send('open', {id: 1})}>
|
||||
Open
|
||||
</button>
|
||||
`
|
||||
}
|
||||
// function parentView (state, prev, send) {
|
||||
// return view`
|
||||
// <button onclick=${() => send('open', {id: 1})}>
|
||||
// Open
|
||||
// </button>
|
||||
// `
|
||||
// }
|
||||
|
||||
function childView (state, prev, send) {
|
||||
t.equal(state.user, 1)
|
||||
return view`<button>${state.user}</button>`
|
||||
}
|
||||
})
|
||||
// function childView (state, prev, send) {
|
||||
// t.equal(state.user, 1)
|
||||
// return view`<p>${state.user}</p>`
|
||||
// }
|
||||
// })
|
||||
|
||||
t.test('disabling history', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
resetLocation()
|
||||
const choo = proxyquire('../..', {
|
||||
'sheet-router/history': () => t.fail('history listener attached')
|
||||
})
|
||||
|
||||
const app = choo()
|
||||
const app = choo({ history: false })
|
||||
|
||||
app.router('/', (route) => [
|
||||
route('/', function () {
|
||||
app.router('/', [
|
||||
['/', function () {
|
||||
t.pass('rendered')
|
||||
})
|
||||
}]
|
||||
])
|
||||
|
||||
app.start({history: false})
|
||||
app.start()
|
||||
})
|
||||
|
||||
t.test('disabling href', function (t) {
|
||||
@@ -136,15 +138,9 @@ test('routing', function (t) {
|
||||
'sheet-router/href': () => t.fail('href listener attached')
|
||||
})
|
||||
|
||||
const app = choo()
|
||||
|
||||
app.router('/', (route) => [
|
||||
route('/', function () {
|
||||
t.pass('rendered')
|
||||
})
|
||||
])
|
||||
|
||||
app.start({href: false})
|
||||
const app = choo({ href: false })
|
||||
app.router(['/', () => t.pass('rendered')])
|
||||
app.start()
|
||||
})
|
||||
|
||||
t.test('viewless nesting', function (t) {
|
||||
@@ -153,12 +149,12 @@ test('routing', function (t) {
|
||||
const choo = require('../..')
|
||||
const app = choo()
|
||||
|
||||
app.router('/users/123', (route) => [
|
||||
route('/users', [
|
||||
route('/:user', function (state) {
|
||||
app.router({ default: '/users/123' }, [
|
||||
['/users', [
|
||||
['/:user', function (state) {
|
||||
t.deepEqual(state.params, {user: '123'})
|
||||
})
|
||||
])
|
||||
}]
|
||||
]]
|
||||
])
|
||||
|
||||
app.start()
|
||||
@@ -181,3 +177,7 @@ test('routing', function (t) {
|
||||
app.start()
|
||||
})
|
||||
})
|
||||
|
||||
function resetLocation () {
|
||||
window.history.pushState({}, null, '/')
|
||||
}
|
||||
|
||||
+10
-30
@@ -8,9 +8,7 @@ test('server', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const app = choo()
|
||||
app.router((route) => [
|
||||
route('/', () => view`<h1>Hello Tokyo!</h1>`)
|
||||
])
|
||||
app.router(['/', () => view`<h1>Hello Tokyo!</h1>`])
|
||||
|
||||
const html = app.toString('/')
|
||||
const expected = '<h1>Hello Tokyo!</h1>'
|
||||
@@ -21,9 +19,7 @@ test('server', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const app = choo()
|
||||
app.router((route) => [
|
||||
route('/', () => minDocument.createElement('div'))
|
||||
])
|
||||
app.router(['/', () => minDocument.createElement('div')])
|
||||
|
||||
const html = app.toString('/')
|
||||
const expected = '<div></div>'
|
||||
@@ -34,11 +30,9 @@ test('server', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const app = choo()
|
||||
app.router((route) => [
|
||||
route('/', function (state, prev, send) {
|
||||
app.router(['/', (state, prev, send) => {
|
||||
return view`<h1>meow meow ${state.message}</h1>`
|
||||
})
|
||||
])
|
||||
}])
|
||||
|
||||
const html = app.toString('/', { message: 'nyan!' })
|
||||
const expected = '<h1>meow meow nyan!</h1>'
|
||||
@@ -50,11 +44,9 @@ test('server', function (t) {
|
||||
|
||||
const app = choo()
|
||||
app.model({ state: { bin: 'baz', beep: 'boop' } })
|
||||
app.router((route) => [
|
||||
route('/', function (state, prev, send) {
|
||||
app.router(['/', (state, prev, send) => {
|
||||
return view`<h1>${state.foo} ${state.bin} ${state.beep}</h1>`
|
||||
})
|
||||
])
|
||||
}])
|
||||
|
||||
const state = { foo: 'bar!', beep: 'beep' }
|
||||
const html = app.toString('/', state)
|
||||
@@ -70,13 +62,11 @@ test('server', function (t) {
|
||||
namespace: 'hello',
|
||||
state: { bin: 'baz', beep: 'boop' }
|
||||
})
|
||||
app.router((route) => [
|
||||
route('/', function (state, prev, send) {
|
||||
app.router(['/', (state, prev, send) => {
|
||||
return view`
|
||||
<h1>${state.hello.foo} ${state.hello.bin} ${state.hello.beep}</h1>
|
||||
`
|
||||
})
|
||||
])
|
||||
}])
|
||||
|
||||
const state = {
|
||||
hello: {
|
||||
@@ -93,12 +83,7 @@ test('server', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const app = choo()
|
||||
app.router((route) => [
|
||||
route('/', function (state, prev, send) {
|
||||
send('hey!')
|
||||
})
|
||||
])
|
||||
|
||||
app.router(['/', (state, prev, send) => send('hey!')])
|
||||
t.throws(app.toString.bind(null), /route must be a string/)
|
||||
})
|
||||
|
||||
@@ -106,12 +91,7 @@ test('server', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const app = choo()
|
||||
app.router((route) => [
|
||||
route('/', function (state, prev, send) {
|
||||
send('hey!')
|
||||
})
|
||||
])
|
||||
|
||||
app.router(['/', (state, prev, send) => send('hey!')])
|
||||
const msg = /send\(\) cannot be called/
|
||||
t.throws(app.toString.bind(null, '/', { message: 'nyan!' }), msg)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user