[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:
Yoshua Wuyts
2016-12-11 19:35:29 +01:00
committed by GitHub
parent 54b6000800
commit a7916ec3f9
30 changed files with 569 additions and 503 deletions
+29
View File
@@ -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`
+142 -152
View File
@@ -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. Theyre passed the current state, and any time the state changes theyre run again with the new state.
Views are pure functions that return a DOM tree for the router to render.
Theyre passed the current state, and any time the state changes theyre 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 models `add` reducer will receive. [As seen
above](#models), the reducer will add an item to the states `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:
[![built with choo v3](https://img.shields.io/badge/built%20with%20choo-v3-ffc3e4.svg?style=flat-square)](https://github.com/yoshuawuyts/choo)
[![built with choo v4](https://img.shields.io/badge/built%20with%20choo-v4-ffc3e4.svg?style=flat-square)](https://github.com/yoshuawuyts/choo)
```md
[![built with choo v3](https://img.shields.io/badge/built%20with%20choo-v3-ffc3e4.svg?style=flat-square)](https://github.com/yoshuawuyts/choo)
[![built with choo v4](https://img.shields.io/badge/built%20with%20choo-v4-ffc3e4.svg?style=flat-square)](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 &amp; 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
+6 -8
View File
@@ -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)
+2 -2
View File
@@ -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 -5
View File
@@ -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)
+3 -3
View File
@@ -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)
}
}
+2 -2
View File
@@ -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 }
}
+9 -6
View File
@@ -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()
+1
View File
@@ -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",
+5
View File
@@ -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: '/' })
}
}
+5 -5
View File
@@ -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
View File
@@ -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()
}
+1 -3
View File
@@ -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)
+6 -6
View File
@@ -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)
+2 -2
View File
@@ -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": {
+3 -6
View File
@@ -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 -3
View File
@@ -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"
}
}
+4 -9
View File
@@ -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
View File
@@ -1 +0,0 @@
module.exports = require('xhr')
+102 -77
View File
@@ -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
}
}
+40
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+4 -8
View File
@@ -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()
})
+7 -9
View File
@@ -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))
+22 -13
View File
@@ -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
View File
@@ -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
View File
@@ -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)
})