[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`
|
## `3.3.0`
|
||||||
Yay, `plugins` now support `wrappers` which is a segway onto HMR, time travel
|
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`
|
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.
|
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).
|
[Click here to see the app running](http://requirebin.com/?gist=229bceda0334cf30e3044d5f5c600960).
|
||||||
```js
|
```js
|
||||||
const choo = require('choo')
|
var html = require('choo/html')
|
||||||
const html = require('choo/html')
|
var choo = require('choo')
|
||||||
const app = choo()
|
var app = choo()
|
||||||
|
|
||||||
app.model({
|
app.model({
|
||||||
state: { title: 'Not quite set yet' },
|
state: { title: 'Not quite set yet' },
|
||||||
reducers: {
|
reducers: {
|
||||||
update: (data, state) => ({ title: data })
|
update: function (state, data) {
|
||||||
|
return { title: data }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const mainView = (state, prev, send) => html`
|
function mainView (state, prev, send) {
|
||||||
<main>
|
return html`
|
||||||
<h1>Title: ${state.title}</h1>
|
<main>
|
||||||
<input
|
<h1>Title: ${state.title}</h1>
|
||||||
type="text"
|
<input type="text" oninput=${update}>
|
||||||
oninput=${(e) => send('update', e.target.value)}/>
|
</main>
|
||||||
</main>
|
`
|
||||||
`
|
|
||||||
|
|
||||||
app.router((route) => [
|
function update (e) {
|
||||||
route('/', mainView)
|
send('update', e.target.value)
|
||||||
])
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tree = app.start()
|
app.router(['/', mainView])
|
||||||
|
|
||||||
|
var tree = app.start()
|
||||||
document.body.appendChild(tree)
|
document.body.appendChild(tree)
|
||||||
```
|
```
|
||||||
|
|
||||||
To run it, save it as `client.js` and run with [budo] and [es2020]. These tools
|
To run it, save it as `client.js` and run with [bankai][bankai]. `bankai` is
|
||||||
are convenient but any [browserify] based tool should do:
|
convenient but any [browserify][browserify] based tool should do:
|
||||||
```sh
|
```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
|
# compile to static files in `./dist/`
|
||||||
do:
|
$ bankai build index.js dist/
|
||||||
```bash
|
|
||||||
$ mkdir -p 'dist/'
|
# deploy to github pages using `tschaub/gh-pages`
|
||||||
$ curl 'localhost:8080' > 'dist/index.html'
|
$ gh-pages -d dist/
|
||||||
$ curl 'localhost:8080/client.js' > 'dist/client.js'
|
|
||||||
```
|
```
|
||||||
All using a couple of shell commands and `.js` files, no grandiose boilerplate
|
|
||||||
needed.
|
|
||||||
|
|
||||||
## Philosophy
|
## Philosophy
|
||||||
We believe programming should be fun and light, not stern and stressful. It's
|
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
|
## Concepts
|
||||||
`choo` cleanly structures internal data flow, so that all pieces of logic can
|
`choo` cleanly structures internal data flow, so that all pieces of logic can
|
||||||
be combined into a nice, cohesive machine. Internally all logic lives within
|
be combined into a nice, cohesive machine. Roughly speaking there are two parts
|
||||||
`models` that contain several properties. `subscriptions` are functions that
|
to `choo`: the views and the models. Models take care of state and logic, and
|
||||||
are called at startup and have `send()` passed in, so they act as read-only
|
views are responsible for displaying the interface and responding to user
|
||||||
sources of data. `effects` react to changes, perform an `action` and can then
|
interactions.
|
||||||
post the results. `reducers` take data, modify it, and update the internal
|
|
||||||
`state`.
|
|
||||||
|
|
||||||
Communication of data is done using something called `actions`. Each `action`
|
All of `choo`'s state is contained in a single object and whenever it changes
|
||||||
consists of a unique `actionName` and an optional payload of `data`, which can
|
the views receive a new version of the state which they can use to safely
|
||||||
be any value.
|
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
|
The logic in choo exist in three different kinds of actions, each with their
|
||||||
`views`. `views` take `state` and return [DOM] nodes which are then
|
own role: `effects`, `subscriptions` and `reducers`.
|
||||||
efficiently rendered on the screen.
|
|
||||||
|
- __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
|
```txt
|
||||||
┌─────────────────┐
|
┌─────────────────┐
|
||||||
│ Subscriptions ─┤ User ───┐
|
│ Subscriptions ─┤ User ───┐
|
||||||
└─ Effects ◀─────┤ ▼
|
└─ Effects ◀─────┤ ▼
|
||||||
┌─ Reducers ◀─────┴──Actions── DOM ◀┐
|
┌─ Reducers ◀─────┴─────────── DOM ◀┐
|
||||||
│ │
|
│ │
|
||||||
└▶ Router ─────State ───▶ Views ────┘
|
└▶ 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
|
||||||
`models` are objects that contain initial `state`, `subscriptions`, `effects`
|
`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
|
`state.todos.items`. Inside the namespaced model they're called by
|
||||||
`send('todos:add')` and `state.items`. An example namespaced model:
|
`send('todos:add')` and `state.items`. An example namespaced model:
|
||||||
```js
|
```js
|
||||||
const app = choo()
|
var app = choo()
|
||||||
app.model({
|
app.model({
|
||||||
namespace: 'todos',
|
namespace: 'todos',
|
||||||
state: { items: [] },
|
state: { items: [] },
|
||||||
reducers: {
|
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.
|
part of your application.
|
||||||
|
|
||||||
### Effects
|
### Effects
|
||||||
Side effects are done through `effects` declared in `app.model()`. Unlike
|
`effects` are similar to `reducers` except instead of modifying the state they
|
||||||
`reducers` they cannot modify the state by returning objects, but get a
|
cause side `effects` by interacting servers, databases, DOM APIs, etc. Often
|
||||||
callback passed which is used to emit `actions` to handle results. Use effects
|
they'll call a reducer when they're done to update the state. For instance, you
|
||||||
every time you don't need to modify the state object directly, but wish to
|
may have an effect called getUsers that fetches a list of users from a server
|
||||||
respond to an action.
|
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:
|
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:
|
||||||
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].
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const http = require('choo/http')
|
var choo = require('choo')
|
||||||
const choo = require('choo')
|
var http = require('xhr')
|
||||||
const app = choo()
|
var app = choo()
|
||||||
|
|
||||||
app.model({
|
app.model({
|
||||||
namespace: 'todos',
|
namespace: 'todos',
|
||||||
state: { items: [] },
|
state: { values: [] },
|
||||||
|
reducers: {
|
||||||
|
add: function (data, state) {
|
||||||
|
return { todos: data }
|
||||||
|
}
|
||||||
|
},
|
||||||
effects: {
|
effects: {
|
||||||
fetch: (data, state, send, done) => {
|
addAndSave: function (state, data, send, done) {
|
||||||
http('/todos', (err, res, body) => {
|
var opts = { body: data.payload, json: true }
|
||||||
send('todos:receive', body, done)
|
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: {
|
subscriptions: {
|
||||||
receive: (data, state) => {
|
'called-once-when-the-app-loads': function (send, done) {
|
||||||
return { items: data }
|
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
|
||||||
Subscriptions are a way of receiving data from a source. For example when
|
Subscriptions are a way of receiving data from a source. For example when
|
||||||
listening for events from a server using `SSE` or `Websockets` for a
|
listening for events from a server using `SSE` or `Websockets` for a
|
||||||
@@ -325,23 +317,29 @@ chat app, or when catching keyboard input for a videogame.
|
|||||||
|
|
||||||
An example subscription that logs `"dog?"` every second:
|
An example subscription that logs `"dog?"` every second:
|
||||||
```js
|
```js
|
||||||
const app = choo()
|
var choo = require('choo')
|
||||||
|
|
||||||
|
var app = choo()
|
||||||
app.model({
|
app.model({
|
||||||
namespace: 'app',
|
namespace: 'app',
|
||||||
subscriptions: [
|
effects: {
|
||||||
(send, done) => {
|
print: function (state, data) {
|
||||||
setInterval(() => {
|
console.log(data.payload)
|
||||||
send('app:print', { payload: 'dog?', myOtherValue: 1000 }, (err) => {
|
}
|
||||||
|
},
|
||||||
|
subscriptions: {
|
||||||
|
callDog: function (send, done) {
|
||||||
|
setInterval(function () {
|
||||||
|
var data = { payload: 'dog?', myOtherValue: 1000 }
|
||||||
|
send('app:print', data, function (err) {
|
||||||
if (err) return done(err)
|
if (err) return done(err)
|
||||||
})
|
})
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
],
|
|
||||||
effects: {
|
|
||||||
print: (data, state) => console.log(data.payload)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
If a `subscription` runs into an error, it can call `done(err)` to signal the
|
If a `subscription` runs into an error, it can call `done(err)` to signal the
|
||||||
error to the error hook.
|
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.
|
supports rendering a default `view` if no routes match.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const app = choo()
|
var app = choo()
|
||||||
app.router('/404', (route) => [
|
app.router({ default: '/404' }, [
|
||||||
route('/', require('./views/empty')),
|
[ '/', require('./views/empty') ],
|
||||||
route('/404', require('./views/error')),
|
[ '/404', require('./views/error') ],
|
||||||
route('/:mailbox', require('./views/mailbox'), [
|
[ '/:mailbox', require('./views/mailbox'), [
|
||||||
route('/:message', require('./views/email'))
|
[ '/: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.
|
minimum. Changing views all over the place tends to lead to messiness.
|
||||||
|
|
||||||
### Views
|
### Views
|
||||||
Views are pure functions that return a DOM tree for the router to render. They’re passed the current state, and any time the state changes they’re run again with the new state.
|
Views are pure functions that return a DOM tree for the router to render.
|
||||||
|
They’re passed the current state, and any time the state changes they’re run
|
||||||
|
again with the new state.
|
||||||
|
|
||||||
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
|
```js
|
||||||
const view = (state, prev, send) => {
|
function view (state, prev, send) {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<h1>Total todos: ${state.todos.length}</h1>
|
<h1>Total todos: ${state.todos.length}</h1>
|
||||||
<button onclick=${(e) => send('add', {title: 'demo'})}>
|
<button onclick=${addTodo}>Add</button>
|
||||||
Add
|
</div>
|
||||||
</button>
|
`
|
||||||
</div>`
|
|
||||||
|
function addTodo (e) {
|
||||||
|
send('add', { title: 'demo' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, when the `Add` button is clicked, the view will dispatch an
|
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
|
`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
|
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()`:
|
that contain `hook` and `wrap` functions and are passed to `app.use()`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const log = require('choo-log')
|
var log = require('choo-log')
|
||||||
const choo = require('choo')
|
var choo = require('choo')
|
||||||
const app = choo()
|
var app = choo()
|
||||||
|
|
||||||
app.use(log())
|
app.use(log())
|
||||||
|
|
||||||
const tree = app.start()
|
var tree = app.start()
|
||||||
document.body.appendChild(tree)
|
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:
|
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
|
```md
|
||||||
[](https://github.com/yoshuawuyts/choo)
|
[](https://github.com/yoshuawuyts/choo)
|
||||||
```
|
```
|
||||||
|
|
||||||
## API
|
## API
|
||||||
@@ -449,9 +455,9 @@ arguments:
|
|||||||
and handlers in other models
|
and handlers in other models
|
||||||
- __state:__ initial values of `state` inside the model
|
- __state:__ initial values of `state` inside the model
|
||||||
- __reducers:__ synchronous operations that modify state. Triggered by
|
- __reducers:__ synchronous operations that modify state. Triggered by
|
||||||
`actions`. Signature of `(data, state)`.
|
`actions`. Signature of `(state, data)`.
|
||||||
- __effects:__ asynchronous operations that don't modify state directly.
|
- __effects:__ asynchronous operations that don't modify state directly.
|
||||||
Triggered by `actions`, can call `actions`. Signature of `(data, state,
|
Triggered by `actions`, can call `actions`. Signature of `(state, data,
|
||||||
send, done)`
|
send, done)`
|
||||||
- __subscriptions:__ asynchronous read-only operations that don't modify state
|
- __subscriptions:__ asynchronous read-only operations that don't modify state
|
||||||
directly. Can call `actions`. Signature of `(send, done)`.
|
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
|
- __onError(err, state, createSend):__ called when an `effect` or
|
||||||
`subscription` emit an error. If no handler is passed, the default handler
|
`subscription` emit an error. If no handler is passed, the default handler
|
||||||
will `throw` on each error.
|
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.
|
`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`.
|
reducer changes the `state`.
|
||||||
- __wrapSubscriptions(fn):__ wraps a `subscription` to add custom behavior
|
- __wrapSubscriptions(fn):__ wraps a `subscription` to add custom behavior
|
||||||
- __wrapReducers(fn):__ wraps a `reducer` 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`
|
calls to `send()` are disabled, and `subscriptions`, `effects` and `reducers`
|
||||||
aren't loaded.
|
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
|
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
|
`document.body.appendChild()`. Opts can contain the following values:
|
||||||
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:
|
|
||||||
- __opts.history:__ default: `true`. Enable a `subscription` to the browser
|
- __opts.history:__ default: `true`. Enable a `subscription` to the browser
|
||||||
history API. e.g. updates the internal `location.href` state whenever the
|
history API. e.g. updates the internal `location.href` state whenever the
|
||||||
browsers "forward" and "backward" buttons are pressed.
|
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.
|
To create listeners for events, create interpolated attributes on elements.
|
||||||
```js
|
```js
|
||||||
const html = require('choo/html')
|
var html = require('choo/html')
|
||||||
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`,
|
Example listeners include: `onclick`, `onsubmit`, `oninput`, `onkeydown`,
|
||||||
`onkeyup`. A full list can be found [at the yo-yo
|
`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()`
|
- [unassertify](https://github.com/twada/unassertify) - remove `assert()`
|
||||||
statements which reduces file size. Use as a `--global` transform
|
statements which reduces file size. Use as a `--global` transform
|
||||||
- [es2020](https://github.com/yoshuawuyts/es2020) - backport `const`,
|
- [es2020](https://github.com/yoshuawuyts/es2020) - backport `const`,
|
||||||
`fat-arrows` and `template strings` to older browsers. Should be run as a
|
`arrow functions` and `template strings` to older browsers. Should be run as
|
||||||
`--global` transform
|
a `--global` transform
|
||||||
- [yo-yoify](https://github.com/shama/yo-yoify) - replace the internal `hyperx`
|
- [yo-yoify](https://github.com/shama/yo-yoify) - replace the internal `hyperx`
|
||||||
dependency with `document.createElement` calls; greatly speeds up performance
|
dependency with `document.createElement` calls; greatly speeds up performance
|
||||||
too
|
too
|
||||||
@@ -676,8 +683,8 @@ Consider running some of the following:
|
|||||||
### Choo + Internet Explorer & Safari
|
### Choo + Internet Explorer & Safari
|
||||||
Out of the box `choo` only supports runtimes which support:
|
Out of the box `choo` only supports runtimes which support:
|
||||||
* `const`
|
* `const`
|
||||||
* `fat-arrow` functions (e.g. `() => {}`)
|
* `arrow functions` (e.g. `() => {}`)
|
||||||
* `template-strings`
|
* `template strings`
|
||||||
|
|
||||||
This does not include Safari 9 or any version of IE. If support for these
|
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
|
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/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>
|
<a href="https://opencollective.com/choo/backer/29/website" target="_blank"><img src="https://opencollective.com/choo/backer/29/avatar.svg"></a>
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[MIT](https://tldrlegal.com/license/mit-license)
|
[MIT](https://tldrlegal.com/license/mit-license)
|
||||||
|
|
||||||
|
[bankai]: https://github.com/yoshuawuyts/bankai
|
||||||
[bel]: https://github.com/shama/bel
|
[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
|
[browserify]: https://github.com/substack/node-browserify
|
||||||
[budo]: https://github.com/mattdesl/budo
|
[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
|
[es2020]: https://github.com/yoshuawuyts/es2020
|
||||||
[handbook]: https://github.com/yoshuawuyts/choo-handbook
|
[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
|
[hyperx]: https://github.com/substack/hyperx
|
||||||
[inu]: https://github.com/ahdinosaur/inu
|
[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-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks
|
||||||
[morphdom]: https://github.com/patrick-steele-idem/morphdom
|
[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
|
[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
|
[yo-yo]: https://github.com/maxogden/yo-yo
|
||||||
|
|||||||
@@ -8,20 +8,20 @@ app.model({
|
|||||||
counter: 0
|
counter: 0
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
increment: (data, state) => ({ counter: state.counter + 1 }),
|
increment: (state, data) => ({ counter: state.counter + 1 }),
|
||||||
decrement: (data, state) => ({ counter: state.counter - 1 })
|
decrement: (state, data) => ({ counter: state.counter - 1 })
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
incrementAsync: function (data, state, send, done) {
|
incrementAsync: function (state, data, send, done) {
|
||||||
setTimeout(() => send('increment', done), 1000)
|
setTimeout(() => send('increment', done), 1000)
|
||||||
},
|
},
|
||||||
decrementAsync: function (data, state, send, done) {
|
decrementAsync: function (state, data, send, done) {
|
||||||
setTimeout(() => send('decrement', done), 1000)
|
setTimeout(() => send('decrement', done), 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const mainView = (state, prev, send) => {
|
function mainView (state, prev, send) {
|
||||||
const count = state.counter
|
const count = state.counter
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -36,9 +36,7 @@ const mainView = (state, prev, send) => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
app.router((route) => [
|
app.router([ '/', mainView ])
|
||||||
route('/', mainView)
|
|
||||||
])
|
|
||||||
|
|
||||||
const tree = app.start()
|
const tree = app.start()
|
||||||
document.body.appendChild(tree)
|
document.body.appendChild(tree)
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "client.js",
|
"main": "client.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "budo client.js -p 8080"
|
"start": "bankai start --entry=client -p 8080"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Juan Soto <juan@juansoto.me>",
|
"author": "Juan Soto <juan@juansoto.me>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"budo": "^8.3.0",
|
"bankai": "^3.2.0",
|
||||||
"plur": "^2.1.2"
|
"plur": "^2.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ const mainView = require('./views/main')
|
|||||||
|
|
||||||
const app = choo({
|
const app = choo({
|
||||||
onError: function (err, state, createSend) {
|
onError: function (err, state, createSend) {
|
||||||
|
console.trace()
|
||||||
console.groupCollapsed(`Error: ${err.message}`)
|
console.groupCollapsed(`Error: ${err.message}`)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
console.groupEnd()
|
console.groupEnd()
|
||||||
const send = createSend('onError: ')
|
const send = createSend('onError: ')
|
||||||
send('app:error', err)
|
send('app:error', err)
|
||||||
},
|
},
|
||||||
onAction: function (data, state, name, caller, createSend) {
|
onAction: function (state, data, name, caller, createSend) {
|
||||||
console.groupCollapsed(`Action: ${caller} -> ${name}`)
|
console.groupCollapsed(`Action: ${caller} -> ${name}`)
|
||||||
console.log(data)
|
console.log(data)
|
||||||
console.groupEnd()
|
console.groupEnd()
|
||||||
},
|
},
|
||||||
onStateChange: function (data, state, prev, createSend) {
|
onStateChange: function (state, data, prev, createSend) {
|
||||||
console.groupCollapsed('State')
|
console.groupCollapsed('State')
|
||||||
console.log(prev)
|
console.log(prev)
|
||||||
console.log(state)
|
console.log(state)
|
||||||
@@ -26,9 +27,7 @@ const app = choo({
|
|||||||
app.model(require('./models/error'))
|
app.model(require('./models/error'))
|
||||||
app.model(require('./models/api'))
|
app.model(require('./models/api'))
|
||||||
|
|
||||||
app.router((route) => [
|
app.router(['/', mainView])
|
||||||
route('/', mainView)
|
|
||||||
])
|
|
||||||
|
|
||||||
const tree = app.start()
|
const tree = app.start()
|
||||||
document.body.appendChild(tree)
|
document.body.appendChild(tree)
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ module.exports = {
|
|||||||
title: 'Button pushing machine 3000'
|
title: 'Button pushing machine 3000'
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
set: (data, state) => ({ 'title': data })
|
set: (state, data) => ({ 'title': data })
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
good: function (data, state, send, done) {
|
good: function (state, data, send, done) {
|
||||||
request('/good', 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
|
triggerTime: null
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setError: function (data, state) {
|
setError: function (state, data) {
|
||||||
return {
|
return {
|
||||||
errors: state.errors.concat(data.message),
|
errors: state.errors.concat(data.message),
|
||||||
errorTimeDone: data.errorTimeDone
|
errorTimeDone: data.errorTimeDone
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'delError': function (data, state) {
|
'delError': function (state, data) {
|
||||||
state.errors.shift()
|
state.errors.shift()
|
||||||
return { errors: state.errors }
|
return { errors: state.errors }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
const choo = require('../../')
|
const log = require('choo-log')
|
||||||
const sf = require('sheetify')
|
const sf = require('sheetify')
|
||||||
|
|
||||||
|
const choo = require('../../')
|
||||||
|
|
||||||
sf('css-wipe/dest/bundle')
|
sf('css-wipe/dest/bundle')
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
|
app.use(log())
|
||||||
|
|
||||||
app.model(require('./models/inbox'))
|
app.model(require('./models/inbox'))
|
||||||
app.model(require('./models/spam'))
|
app.model(require('./models/spam'))
|
||||||
app.model(require('./models/sent'))
|
app.model(require('./models/sent'))
|
||||||
|
|
||||||
app.router((route) => [
|
app.router([
|
||||||
route('/', require('./views/empty')),
|
['/', require('./views/empty')],
|
||||||
route('/:mailbox', require('./views/mailbox'), [
|
['/:mailbox', require('./views/mailbox'), [
|
||||||
route('/:message', require('./views/email'))
|
['/:message', require('./views/email')]
|
||||||
])
|
]]
|
||||||
])
|
])
|
||||||
|
|
||||||
const tree = app.start()
|
const tree = app.start()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"author": "Yoshua Wuyts <i@yoshuawuyts.com>",
|
"author": "Yoshua Wuyts <i@yoshuawuyts.com>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"choo-log": "^1.4.1",
|
||||||
"css-wipe": "^4.2.1",
|
"css-wipe": "^4.2.1",
|
||||||
"dateformat": "^1.0.12",
|
"dateformat": "^1.0.12",
|
||||||
"pathname-match": "^1.1.3",
|
"pathname-match": "^1.1.3",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const nav = require('../elements/nav')
|
|||||||
module.exports = function (state, prev, send) {
|
module.exports = function (state, prev, send) {
|
||||||
return html`
|
return html`
|
||||||
<main class="mw5 mw7-ns center cf">
|
<main class="mw5 mw7-ns center cf">
|
||||||
|
<button onclick=${goHome}>Home</button>
|
||||||
${pathname(state, prev, send)}
|
${pathname(state, prev, send)}
|
||||||
${nav(state, prev, send)}
|
${nav(state, prev, send)}
|
||||||
<section class="fl mt4 w-80 db">
|
<section class="fl mt4 w-80 db">
|
||||||
@@ -14,4 +15,8 @@ module.exports = function (state, prev, send) {
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
`
|
`
|
||||||
|
|
||||||
|
function goHome () {
|
||||||
|
send('location:set', { pathname: '/' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
const mount = require('../../mount')
|
||||||
const choo = require('../../')
|
const choo = require('../../')
|
||||||
|
|
||||||
const mainView = require('./views/main')
|
const mainView = require('./view')
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
|
|
||||||
@@ -12,12 +13,11 @@ app.model({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.router((route) => [
|
app.router(['/', mainView])
|
||||||
route('/', mainView)
|
|
||||||
])
|
|
||||||
|
|
||||||
if (module.parent) {
|
if (module.parent) {
|
||||||
module.exports = app
|
module.exports = app
|
||||||
} else {
|
} else {
|
||||||
app.start('#app-root')
|
const tree = app.start()
|
||||||
|
mount('#app-root', tree)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const html = require('../../../html')
|
const html = require('../../html')
|
||||||
|
|
||||||
module.exports = function (state, prev, send) {
|
module.exports = function (state, prev, send) {
|
||||||
const serverMessage = state.message.server
|
const serverMessage = state.message.server
|
||||||
+8
-10
@@ -3,14 +3,12 @@ const html = require('../../html')
|
|||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.model(createModel())
|
app.model(createModel())
|
||||||
app.router((route) => [
|
app.router(['/', mainView])
|
||||||
route('/', mainView)
|
|
||||||
])
|
|
||||||
|
|
||||||
const tree = app.start()
|
const tree = app.start()
|
||||||
document.body.appendChild(tree)
|
document.body.appendChild(tree)
|
||||||
|
|
||||||
function mainView (params, state, send) {
|
function mainView (state, prev, send) {
|
||||||
return html`
|
return html`
|
||||||
<div>${state.logger.msg}</div>
|
<div>${state.logger.msg}</div>
|
||||||
`
|
`
|
||||||
@@ -24,27 +22,27 @@ function createModel () {
|
|||||||
msg: ''
|
msg: ''
|
||||||
},
|
},
|
||||||
subscriptions: [
|
subscriptions: [
|
||||||
function (send) {
|
function (send, done) {
|
||||||
stream.onerror = (e) => {
|
stream.onerror = (e) => {
|
||||||
send('logger:error', { payload: JSON.stringify(e) })
|
send('logger:error', { payload: JSON.stringify(e) }, done)
|
||||||
}
|
}
|
||||||
stream.onmessage = (e) => {
|
stream.onmessage = (e) => {
|
||||||
const msg = JSON.parse(e.data).message
|
const msg = JSON.parse(e.data).message
|
||||||
send('logger:print', { payload: msg })
|
send('logger:print', { payload: msg }, done)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
reducers: {
|
reducers: {
|
||||||
'print': (data, state) => {
|
'print': (state, data) => {
|
||||||
return ({ msg: state.msg + ' ' + data.payload })
|
return ({ msg: state.msg + ' ' + data.payload })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
close: (data, state, send, done) => {
|
close: (state, data, send, done) => {
|
||||||
stream.close()
|
stream.close()
|
||||||
done()
|
done()
|
||||||
},
|
},
|
||||||
error: (data, state, send, done) => {
|
error: (state, data, send, done) => {
|
||||||
console.error(`error: ${data.payload}`)
|
console.error(`error: ${data.payload}`)
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ const app = choo()
|
|||||||
|
|
||||||
app.model(stopwatch)
|
app.model(stopwatch)
|
||||||
|
|
||||||
app.router((route) => [
|
app.router(['/', mainView])
|
||||||
route('/', mainView)
|
|
||||||
])
|
|
||||||
|
|
||||||
const tree = app.start()
|
const tree = app.start()
|
||||||
document.body.appendChild(tree)
|
document.body.appendChild(tree)
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ module.exports = {
|
|||||||
laps: []
|
laps: []
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
start: (data, state) => ({ start: true, startTime: Date.now() - state.elapsed }),
|
start: (state, data) => ({ start: true, startTime: Date.now() - state.elapsed }),
|
||||||
stop: (data, state) => ({ start: false }),
|
stop: (state, data) => ({ start: false }),
|
||||||
update: (data, state) => ({ elapsed: data }),
|
update: (state, data) => ({ elapsed: data }),
|
||||||
reset: (data, state) => ({ startTime: Date.now(), elapsed: 0, laps: [] }),
|
reset: (state, data) => ({ startTime: Date.now(), elapsed: 0, laps: [] }),
|
||||||
add: (data, state) => ({ laps: state.laps.concat(data) })
|
add: (state, data) => ({ laps: state.laps.concat(data) })
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
now: (data, state, send, done) => {
|
now: (state, data, send, done) => {
|
||||||
if (state.start) {
|
if (state.start) {
|
||||||
let elapsed = data - state.startTime
|
let elapsed = data - state.startTime
|
||||||
send('update', elapsed, done)
|
send('update', elapsed, done)
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "client.js",
|
"main": "client.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "budo client.js -p 8080 -- -t es2020"
|
"start": "bankai start --entry=client.js -p 8080"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "traducer <traducer21@gmail.com>",
|
"author": "traducer <traducer21@gmail.com>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"budo": "^8.3.0",
|
"bankai": "^3.2.0",
|
||||||
"raf": "^3.2.0"
|
"raf": "^3.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ app.model({
|
|||||||
title: 'my demo app'
|
title: 'my demo app'
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
update: (data, state) => ({ title: data.payload })
|
update: (state, data) => ({ title: data.payload })
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
update: (data, state, send, done) => {
|
update: (state, data, send, done) => {
|
||||||
document.title = data.payload
|
document.title = data.payload
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
@@ -31,9 +31,6 @@ const mainView = (state, prev, send) => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
app.router((route) => [
|
app.router(['/', mainView])
|
||||||
route('/', mainView)
|
|
||||||
])
|
|
||||||
|
|
||||||
const tree = app.start()
|
const tree = app.start()
|
||||||
document.body.appendChild(tree)
|
document.body.appendChild(tree)
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "client.js",
|
"main": "client.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "budo client.js -p 8080"
|
"start": "bankai start --entry=client.js -p 8080 --open"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Yoshua Wuyts <i@yoshuawuyts.com>",
|
"author": "Yoshua Wuyts <i@yoshuawuyts.com>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {},
|
||||||
"budo": "^8.3.0"
|
"devDependencies": {
|
||||||
|
"bankai": "^3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-38
@@ -1,43 +1,38 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Vanilla example</title>
|
<title>Vanilla example</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="../../dist/choo.min.js"></script>
|
<script src="../../dist/choo.min.js"></script>
|
||||||
<script src="../../dist/html.min.js"></script>
|
<script src="../../dist/html.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const app = choo()
|
const app = choo()
|
||||||
|
|
||||||
app.model({
|
app.model({
|
||||||
namespace: 'counter',
|
namespace: 'counter',
|
||||||
state: {
|
state: { count: 0 },
|
||||||
count: 0
|
reducers: {
|
||||||
},
|
increment: (state, data) => ({count: state.count + 1}),
|
||||||
reducers: {
|
decrement: (state, data) => ({count: state.count - 1})
|
||||||
increment: (data, state) => ({count: state.count + 1}),
|
}
|
||||||
decrement: (data, state) => ({count: state.count - 1})
|
})
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const mainView = (state, prev, send) => {
|
const mainView = (state, prev, send) => {
|
||||||
return html`
|
return html`
|
||||||
<main class="app">
|
<main class="app">
|
||||||
<h1>Counter example</h1>
|
<h1>Counter example</h1>
|
||||||
<div>
|
<div>
|
||||||
Count: ${state.counter.count}
|
Count: ${state.counter.count}
|
||||||
<button onclick=${(e) => send('counter:increment')}>+</button>
|
<button onclick=${(e) => send('counter:increment')}>+</button>
|
||||||
<button onclick=${(e) => send('counter:decrement')}>-</button>
|
<button onclick=${(e) => send('counter:decrement')}>-</button>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
app.router((route) => [
|
app.router(['/', mainView])
|
||||||
route('/', mainView)
|
const tree = app.start()
|
||||||
])
|
document.body.appendChild(tree)
|
||||||
|
</script>
|
||||||
const tree = app.start()
|
</body>
|
||||||
document.body.appendChild(tree)
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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 sheetRouter = require('sheet-router')
|
||||||
const document = require('global/document')
|
const onHref = require('sheet-router/href')
|
||||||
const onReady = require('document-ready')
|
const walk = require('sheet-router/walk')
|
||||||
const href = require('sheet-router/href')
|
const mutate = require('xtend/mutable')
|
||||||
const hash = require('sheet-router/hash')
|
|
||||||
const hashMatch = require('hash-match')
|
|
||||||
const barracks = require('barracks')
|
const barracks = require('barracks')
|
||||||
const nanoraf = require('nanoraf')
|
const nanoraf = require('nanoraf')
|
||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
@@ -20,7 +19,7 @@ function choo (opts) {
|
|||||||
|
|
||||||
const _store = start._store = barracks()
|
const _store = start._store = barracks()
|
||||||
var _router = start._router = null
|
var _router = start._router = null
|
||||||
var _defaultRoute = null
|
var _routerOpts = null
|
||||||
var _rootNode = null
|
var _rootNode = null
|
||||||
var _routes = null
|
var _routes = null
|
||||||
var _frame = null
|
var _frame = null
|
||||||
@@ -45,7 +44,7 @@ function choo (opts) {
|
|||||||
_store.start({ subscriptions: false, reducers: false, effects: false })
|
_store.start({ subscriptions: false, reducers: false, effects: false })
|
||||||
|
|
||||||
const state = _store.state({ state: serverState })
|
const state = _store.state({ state: serverState })
|
||||||
const router = createRouter(_defaultRoute, _routes, createSend)
|
const router = createRouter(_routerOpts, _routes, createSend)
|
||||||
const tree = router(route, state)
|
const tree = router(route, state)
|
||||||
return tree.outerHTML || tree.toString()
|
return tree.outerHTML || tree.toString()
|
||||||
|
|
||||||
@@ -58,38 +57,31 @@ function choo (opts) {
|
|||||||
|
|
||||||
// start the application
|
// start the application
|
||||||
// (str?, obj?) -> DOMNode
|
// (str?, obj?) -> DOMNode
|
||||||
function start (selector, startOpts) {
|
function start () {
|
||||||
if (!startOpts && typeof selector !== 'string') {
|
_store.model(createLocationModel(opts))
|
||||||
startOpts = selector
|
const createSend = _store.start(opts)
|
||||||
selector = null
|
_router = start._router = createRouter(_routerOpts, _routes, createSend)
|
||||||
}
|
|
||||||
startOpts = startOpts || {}
|
|
||||||
|
|
||||||
_store.model(appInit(startOpts))
|
|
||||||
const createSend = _store.start(startOpts)
|
|
||||||
_router = start._router = createRouter(_defaultRoute, _routes, createSend)
|
|
||||||
const state = _store.state({state: {}})
|
const state = _store.state({state: {}})
|
||||||
|
|
||||||
if (!selector) {
|
const tree = _router(state.location.href, state)
|
||||||
const tree = _router(state.location.pathname, state)
|
_rootNode = tree
|
||||||
_rootNode = tree
|
tree.done = done
|
||||||
return tree
|
|
||||||
} else {
|
return tree
|
||||||
onReady(function onReady () {
|
|
||||||
const oldTree = document.querySelector(selector)
|
// allow a 'mount' function to return the new node
|
||||||
assert.ok(oldTree, 'could not query selector: ' + selector)
|
// html -> null
|
||||||
const newTree = _router(state.location.pathname, state)
|
function done (newNode) {
|
||||||
_rootNode = yo.update(oldTree, newTree)
|
_rootNode = newNode
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the DOM after every state mutation
|
// update the DOM after every state mutation
|
||||||
// (obj, obj, obj, str, fn) -> null
|
// (obj, obj, obj, str, fn) -> null
|
||||||
function render (data, state, prev, name, createSend) {
|
function render (state, data, prev, name, createSend) {
|
||||||
if (!_frame) {
|
if (!_frame) {
|
||||||
_frame = nanoraf(function (state, prev) {
|
_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)
|
_rootNode = yo.update(_rootNode, newTree)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -99,7 +91,7 @@ function choo (opts) {
|
|||||||
// register all routes on the router
|
// register all routes on the router
|
||||||
// (str?, [fn|[fn]]) -> obj
|
// (str?, [fn|[fn]]) -> obj
|
||||||
function router (defaultRoute, routes) {
|
function router (defaultRoute, routes) {
|
||||||
_defaultRoute = defaultRoute
|
_routerOpts = defaultRoute
|
||||||
_routes = routes
|
_routes = routes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,70 +109,103 @@ function choo (opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create a new router with a custom `createRoute()` function
|
// create a new router with a custom `createRoute()` function
|
||||||
// (str?, obj, fn?) -> null
|
// (str?, obj) -> null
|
||||||
function createRouter (defaultRoute, routes, createSend) {
|
function createRouter (routerOpts, routes, createSend) {
|
||||||
var prev = { params: {} }
|
var prev = null
|
||||||
return sheetRouter(defaultRoute, routes, createRoute)
|
if (!routes) {
|
||||||
|
routes = routerOpts
|
||||||
|
routerOpts = {}
|
||||||
|
}
|
||||||
|
routerOpts = mutate({ thunk: 'match' }, routerOpts)
|
||||||
|
const router = sheetRouter(routerOpts, routes)
|
||||||
|
walk(router, wrap)
|
||||||
|
|
||||||
function createRoute (routeFn) {
|
return router
|
||||||
return function (route, inline, child) {
|
|
||||||
if (typeof inline === 'function') {
|
|
||||||
inline = wrap(inline, route)
|
|
||||||
}
|
|
||||||
return routeFn(route, inline, child)
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrap (child, route) {
|
function wrap (route, handler) {
|
||||||
const send = createSend('view: ' + route, true)
|
const send = createSend('view: ' + route, true)
|
||||||
return function chooWrap (params, state) {
|
return function chooWrap (params) {
|
||||||
|
return function (state) {
|
||||||
const nwPrev = prev
|
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)
|
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
|
// obj -> obj
|
||||||
function appInit (opts) {
|
function createLocationModel (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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
namespace: 'location',
|
namespace: 'location',
|
||||||
subscriptions: subs,
|
state: mutate(createLocation(), { params: {} }),
|
||||||
reducers: reducers,
|
subscriptions: createSubscriptions(opts),
|
||||||
state: state
|
effects: { set: setLocation, touch: touchLocation },
|
||||||
|
reducers: { update: updateLocation }
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a new subscription that modifies
|
// update the location on the state
|
||||||
// 'app:location' and push it to be loaded
|
// try and jump to an anchor on the page if it exists
|
||||||
// (fn, obj) -> null
|
// (obj, obj) -> obj
|
||||||
function pushLocationSub (cb, key, model) {
|
function updateLocation (state, data) {
|
||||||
model[key] = function (send, done) {
|
if (opts.history !== false && data.hash && data.hash !== state.hash) {
|
||||||
cb(function navigate (pathname) {
|
try {
|
||||||
send('location:setLocation', { location: pathname }, done)
|
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",
|
"name": "choo",
|
||||||
"version": "3.3.0",
|
"version": "4.0.0-7",
|
||||||
"description": "A 5kb framework for creating sturdy frontend applications",
|
"description": "A 5kb framework for creating sturdy frontend applications",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -30,19 +30,17 @@
|
|||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
"http.js",
|
"mount.js",
|
||||||
"html.js",
|
"html.js",
|
||||||
"dist/"
|
"dist/"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"barracks": "^8.3.1",
|
"barracks": "^9.0.0",
|
||||||
"document-ready": "~1.0.2",
|
"document-ready": "~1.0.2",
|
||||||
"global": "^4.3.0",
|
"global": "^4.3.0",
|
||||||
"hash-match": "^1.0.2",
|
|
||||||
"nanoraf": "^2.1.1",
|
"nanoraf": "^2.1.1",
|
||||||
"sheet-router": "^3.1.0",
|
"sheet-router": "^4.0.1",
|
||||||
"xhr": "^2.2.0",
|
|
||||||
"xtend": "^4.0.1",
|
"xtend": "^4.0.1",
|
||||||
"yo-yo": "^1.2.2"
|
"yo-yo": "^1.2.2"
|
||||||
},
|
},
|
||||||
@@ -68,6 +66,7 @@
|
|||||||
"server-router": "^3.0.0",
|
"server-router": "^3.0.0",
|
||||||
"sheetify": "^5.1.0",
|
"sheetify": "^5.1.0",
|
||||||
"standard": "^8.0.0",
|
"standard": "^8.0.0",
|
||||||
|
"standard-markdown": "^2.2.0",
|
||||||
"tachyons": "^4.0.0-beta.19",
|
"tachyons": "^4.0.0-beta.19",
|
||||||
"tape": "^4.5.1",
|
"tape": "^4.5.1",
|
||||||
"tape-istanbul": "~1.0.2",
|
"tape-istanbul": "~1.0.2",
|
||||||
|
|||||||
+5
-2
@@ -8,7 +8,8 @@ usage () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lint () {
|
lint () {
|
||||||
standard
|
standard > /dev/tty
|
||||||
|
standard-markdown > /dev/tty
|
||||||
}
|
}
|
||||||
|
|
||||||
test_electron () {
|
test_electron () {
|
||||||
@@ -52,7 +53,8 @@ test_browser_local () {
|
|||||||
check_deps () {
|
check_deps () {
|
||||||
dependency-check . --entry 'index.js'
|
dependency-check . --entry 'index.js'
|
||||||
dependency-check . --entry 'index.js' --extra --no-dev \
|
dependency-check . --entry 'index.js' --extra --no-dev \
|
||||||
-i xhr
|
-i document-ready \
|
||||||
|
-i global
|
||||||
}
|
}
|
||||||
|
|
||||||
# set CLI flags
|
# set CLI flags
|
||||||
@@ -70,6 +72,7 @@ case "$1" in
|
|||||||
b|browser) shift; test_browser "$@";;
|
b|browser) shift; test_browser "$@";;
|
||||||
l|browser-local) shift; test_browser_local "$@";;
|
l|browser-local) shift; test_browser_local "$@";;
|
||||||
s|server) shift; test_server "$@";;
|
s|server) shift; test_server "$@";;
|
||||||
|
lint) shift; lint "$@";;
|
||||||
c|cov) shift; test_cov "$@";;
|
c|cov) shift; test_cov "$@";;
|
||||||
d|deps) shift; check_deps "$@";;
|
d|deps) shift; check_deps "$@";;
|
||||||
esac
|
esac
|
||||||
|
|||||||
+12
-9
@@ -1,7 +1,8 @@
|
|||||||
const test = require('tape')
|
|
||||||
const append = require('append-child')
|
const append = require('append-child')
|
||||||
|
const test = require('tape')
|
||||||
|
|
||||||
const choo = require('../../')
|
const choo = require('../../')
|
||||||
const view = require('../../html')
|
const html = require('../../html')
|
||||||
|
|
||||||
test('state is immutable', function (t) {
|
test('state is immutable', function (t) {
|
||||||
t.plan(4)
|
t.plan(4)
|
||||||
@@ -16,16 +17,16 @@ test('state is immutable', function (t) {
|
|||||||
state: state,
|
state: state,
|
||||||
namespace: 'test',
|
namespace: 'test',
|
||||||
reducers: {
|
reducers: {
|
||||||
'no-reducer-mutate': (data, state) => {
|
'no-reducer-mutate': (state, data) => {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
'mutate-on-return': (data, state) => {
|
'mutate-on-return': (state, data) => {
|
||||||
delete data.type
|
delete data.type
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
'triggers-reducers': (data, state, send, done) => {
|
'triggers-reducers': (state, data, send, done) => {
|
||||||
send('test:mutate-on-return', {beep: 'barp'}, done)
|
send('test:mutate-on-return', {beep: 'barp'}, done)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,13 +47,15 @@ test('state is immutable', function (t) {
|
|||||||
(send) => send('test:triggers-reducers')
|
(send) => send('test:triggers-reducers')
|
||||||
]
|
]
|
||||||
|
|
||||||
app.router((route) => [
|
app.router([
|
||||||
route('/', function (state, prev, send) {
|
['/', function (state, prev, send) {
|
||||||
++loop
|
++loop
|
||||||
asserts[loop] && asserts[loop](state.test)
|
asserts[loop] && asserts[loop](state.test)
|
||||||
setTimeout(() => triggers[loop] && triggers[loop](send), 5)
|
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()
|
const tree = app.start()
|
||||||
|
|||||||
+12
-16
@@ -11,14 +11,12 @@ test('freeze (default)', function (t) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.router((route) => [
|
app.router(['/', function (state, prev, send) {
|
||||||
route('/', function (state, prev, send) {
|
state.foo = ''
|
||||||
state.foo = ''
|
t.equal(state.foo, 'bar', 'cannot modify property')
|
||||||
t.equal(state.foo, 'bar', 'cannot modify property')
|
state.bar = 'baz'
|
||||||
state.bar = 'baz'
|
t.equal(state.bar, undefined, 'cannot add property')
|
||||||
t.equal(state.bar, undefined, 'cannot add property')
|
}])
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
app.start()
|
app.start()
|
||||||
})
|
})
|
||||||
@@ -33,14 +31,12 @@ test('noFreeze', function (t) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.router((route) => [
|
app.router(['/', function (state, prev, send) {
|
||||||
route('/', function (state, prev, send) {
|
state.foo = ''
|
||||||
state.foo = ''
|
t.equal(state.foo, '', 'can modify property')
|
||||||
t.equal(state.foo, '', 'can modify property')
|
state.bar = 'baz'
|
||||||
state.bar = 'baz'
|
t.equal(state.bar, 'baz', 'can add property')
|
||||||
t.equal(state.bar, 'baz', 'can add property')
|
}])
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
app.start()
|
app.start()
|
||||||
})
|
})
|
||||||
|
|||||||
+13
-15
@@ -10,15 +10,15 @@ test('hooks', function (t) {
|
|||||||
onError: function (err) {
|
onError: function (err) {
|
||||||
t.equal(err.message, 'effect error', 'onError: receives 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
|
if (name === 'explodes') return
|
||||||
t.deepEqual(data, {foo: 'bar'}, 'onAction: action data')
|
t.deepEqual(data, {foo: 'bar'}, 'onAction: action data')
|
||||||
t.equal(state.clicks, 0, 'onAction: current state: 0 clicks')
|
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(caller, 'view: /', 'onAction: caller name')
|
||||||
t.equal(typeof createSend, 'function', 'onAction: createSend fn')
|
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(data, {foo: 'bar'}, 'onState: action data')
|
||||||
t.deepEqual(state.clicks, 1, 'onState: new state: 1 clicks')
|
t.deepEqual(state.clicks, 1, 'onState: new state: 1 clicks')
|
||||||
t.deepEqual(prev.clicks, 0, 'onState: prev state: 0 clicks')
|
t.deepEqual(prev.clicks, 0, 'onState: prev state: 0 clicks')
|
||||||
@@ -30,26 +30,24 @@ test('hooks', function (t) {
|
|||||||
clicks: 0
|
clicks: 0
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
click: (data, state) => ({clicks: state.clicks + 1})
|
click: (state, data) => ({clicks: state.clicks + 1})
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
explodes: (data, state, send, done) => {
|
explodes: (state, data, send, done) => {
|
||||||
setTimeout(() => done(new Error('effect error')), 5)
|
setTimeout(() => done(new Error('effect error')), 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
var sent = false
|
var sent = false
|
||||||
app.router((route) => [
|
app.router(['/', function (state, prev, send) {
|
||||||
route('/', function (state, prev, send) {
|
if (!sent) {
|
||||||
if (!sent) {
|
send('click', {foo: 'bar'})
|
||||||
send('click', {foo: 'bar'})
|
send('explodes')
|
||||||
send('explodes')
|
}
|
||||||
}
|
sent = true
|
||||||
sent = true
|
return view`<span></span>`
|
||||||
return view`<span></span>`
|
}])
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
const tree = app.start()
|
const tree = app.start()
|
||||||
t.on('end', append(tree))
|
t.on('end', append(tree))
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
const test = require('tape')
|
const test = require('tape')
|
||||||
const onReady = require('document-ready')
|
const onReady = require('document-ready')
|
||||||
const append = require('append-child')
|
const append = require('append-child')
|
||||||
|
const mount = require('../../mount')
|
||||||
const choo = require('../../')
|
const choo = require('../../')
|
||||||
const view = require('../../html')
|
const html = require('../../html')
|
||||||
|
|
||||||
test('rehydration', function (t) {
|
test('rehydration', function (t) {
|
||||||
t.plan(2)
|
t.plan(2)
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
|
|
||||||
app.router((route) => [
|
const node = html`
|
||||||
route('/', function (state, prev, send) {
|
<section id="app-root">
|
||||||
return view`<div id="app-root" onclick=${() => send('test')}>Hello world!</span>`
|
<div id="app-root">Hello squirrel!</span>
|
||||||
})
|
</section>
|
||||||
])
|
`
|
||||||
|
|
||||||
var node = document.createElement('div')
|
app.router(['/', function (state, prev, send) {
|
||||||
node.innerHTML = app.toString('/')
|
return html`
|
||||||
node = node.childNodes[0]
|
<section id="app-root">
|
||||||
t.on('end', append(node))
|
<div onclick=${() => send('test')}>Hello world!</span>
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
}])
|
||||||
|
|
||||||
app.start('#app-root')
|
append(node)
|
||||||
|
|
||||||
|
const tree = app.start()
|
||||||
|
mount('#app-root', tree)
|
||||||
|
|
||||||
onReady(function () {
|
onReady(function () {
|
||||||
t.equal(node.innerHTML, 'Hello world!', 'same content')
|
const newNode = document.querySelector('#app-root')
|
||||||
t.equal(typeof node.onclick, 'function', 'attaches dom listeners')
|
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) {
|
test('routing', function (t) {
|
||||||
t.test('history', function (t) {
|
t.test('history', function (t) {
|
||||||
t.plan(3)
|
t.plan(2)
|
||||||
|
|
||||||
const history = Event()
|
const history = Event()
|
||||||
const choo = proxyquire('../..', {
|
const choo = proxyquire('../..', {
|
||||||
@@ -20,23 +20,23 @@ test('routing', function (t) {
|
|||||||
user: null
|
user: null
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
set: (data, state) => ({user: data.id})
|
set: (state, data) => ({user: data.id})
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
open: function (data, state, send, done) {
|
open: function (state, data, send, done) {
|
||||||
t.deepEqual(data, {id: 1})
|
t.deepEqual(data, {id: 1})
|
||||||
send('set', {id: 1}, function (err) {
|
send('set', {id: 1}, function (err) {
|
||||||
if (err) return done(err)
|
if (err) return done(err)
|
||||||
history.broadcast('https://foo.com/users/1')
|
history.broadcast('/users/1')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.router('/users', (route) => [
|
app.router({ default: '/users' }, [
|
||||||
route('/users', parentView, [
|
['/users', parentView, [
|
||||||
route('/:user', childView)
|
['/:user', childView]
|
||||||
])
|
]]
|
||||||
])
|
])
|
||||||
|
|
||||||
const tree = app.start()
|
const tree = app.start()
|
||||||
@@ -59,74 +59,76 @@ test('routing', function (t) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('hash', function (t) {
|
// t.test('hash', function (t) {
|
||||||
t.plan(1)
|
// t.plan(1)
|
||||||
|
|
||||||
const hash = Event()
|
// resetLocation()
|
||||||
const choo = proxyquire('../..', {
|
// const hash = Event()
|
||||||
'sheet-router/hash': hash.listen
|
// const choo = proxyquire('../..', {
|
||||||
})
|
// 'sheet-router/hash': hash.listen
|
||||||
|
// })
|
||||||
|
|
||||||
const app = choo()
|
// const app = choo({hash: true})
|
||||||
|
|
||||||
app.model({
|
// app.model({
|
||||||
state: {
|
// state: {
|
||||||
user: null
|
// user: null
|
||||||
},
|
// },
|
||||||
reducers: {
|
// reducers: {
|
||||||
set: (data, state) => ({user: data.id})
|
// set: (state, data) => ({user: action.id})
|
||||||
},
|
// },
|
||||||
effects: {
|
// effects: {
|
||||||
open: function (data, state, send, done) {
|
// open: function (state, data, send, done) {
|
||||||
send('set', {id: 1}, function (err) {
|
// send('set', {id: 1}, function (err) {
|
||||||
if (err) return done(err)
|
// if (err) return done(err)
|
||||||
hash.broadcast('#users/1')
|
// hash.broadcast('#users/1')
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
app.router('/users', (route) => [
|
// app.router({ default: '/users' }, [
|
||||||
route('/users', parentView, [
|
// ['/users', parentView, [
|
||||||
route('/:user', childView)
|
// ['/:user', childView]
|
||||||
])
|
// ]]
|
||||||
])
|
// ])
|
||||||
|
|
||||||
const tree = app.start({hash: true})
|
// const tree = app.start()
|
||||||
t.on('end', append(tree))
|
// t.on('end', append(tree))
|
||||||
|
|
||||||
tree.onclick()
|
// tree.onclick()
|
||||||
|
|
||||||
function parentView (state, prev, send) {
|
// function parentView (state, prev, send) {
|
||||||
return view`
|
// return view`
|
||||||
<button onclick=${() => send('open', {id: 1})}>
|
// <button onclick=${() => send('open', {id: 1})}>
|
||||||
Open
|
// Open
|
||||||
</button>
|
// </button>
|
||||||
`
|
// `
|
||||||
}
|
// }
|
||||||
|
|
||||||
function childView (state, prev, send) {
|
// function childView (state, prev, send) {
|
||||||
t.equal(state.user, 1)
|
// t.equal(state.user, 1)
|
||||||
return view`<button>${state.user}</button>`
|
// return view`<p>${state.user}</p>`
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
t.test('disabling history', function (t) {
|
t.test('disabling history', function (t) {
|
||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
|
resetLocation()
|
||||||
const choo = proxyquire('../..', {
|
const choo = proxyquire('../..', {
|
||||||
'sheet-router/history': () => t.fail('history listener attached')
|
'sheet-router/history': () => t.fail('history listener attached')
|
||||||
})
|
})
|
||||||
|
|
||||||
const app = choo()
|
const app = choo({ history: false })
|
||||||
|
|
||||||
app.router('/', (route) => [
|
app.router('/', [
|
||||||
route('/', function () {
|
['/', function () {
|
||||||
t.pass('rendered')
|
t.pass('rendered')
|
||||||
})
|
}]
|
||||||
])
|
])
|
||||||
|
|
||||||
app.start({history: false})
|
app.start()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('disabling href', function (t) {
|
t.test('disabling href', function (t) {
|
||||||
@@ -136,15 +138,9 @@ test('routing', function (t) {
|
|||||||
'sheet-router/href': () => t.fail('href listener attached')
|
'sheet-router/href': () => t.fail('href listener attached')
|
||||||
})
|
})
|
||||||
|
|
||||||
const app = choo()
|
const app = choo({ href: false })
|
||||||
|
app.router(['/', () => t.pass('rendered')])
|
||||||
app.router('/', (route) => [
|
app.start()
|
||||||
route('/', function () {
|
|
||||||
t.pass('rendered')
|
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
app.start({href: false})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('viewless nesting', function (t) {
|
t.test('viewless nesting', function (t) {
|
||||||
@@ -153,12 +149,12 @@ test('routing', function (t) {
|
|||||||
const choo = require('../..')
|
const choo = require('../..')
|
||||||
const app = choo()
|
const app = choo()
|
||||||
|
|
||||||
app.router('/users/123', (route) => [
|
app.router({ default: '/users/123' }, [
|
||||||
route('/users', [
|
['/users', [
|
||||||
route('/:user', function (state) {
|
['/:user', function (state) {
|
||||||
t.deepEqual(state.params, {user: '123'})
|
t.deepEqual(state.params, {user: '123'})
|
||||||
})
|
}]
|
||||||
])
|
]]
|
||||||
])
|
])
|
||||||
|
|
||||||
app.start()
|
app.start()
|
||||||
@@ -181,3 +177,7 @@ test('routing', function (t) {
|
|||||||
app.start()
|
app.start()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function resetLocation () {
|
||||||
|
window.history.pushState({}, null, '/')
|
||||||
|
}
|
||||||
|
|||||||
+15
-35
@@ -8,9 +8,7 @@ test('server', function (t) {
|
|||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.router((route) => [
|
app.router(['/', () => view`<h1>Hello Tokyo!</h1>`])
|
||||||
route('/', () => view`<h1>Hello Tokyo!</h1>`)
|
|
||||||
])
|
|
||||||
|
|
||||||
const html = app.toString('/')
|
const html = app.toString('/')
|
||||||
const expected = '<h1>Hello Tokyo!</h1>'
|
const expected = '<h1>Hello Tokyo!</h1>'
|
||||||
@@ -21,9 +19,7 @@ test('server', function (t) {
|
|||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.router((route) => [
|
app.router(['/', () => minDocument.createElement('div')])
|
||||||
route('/', () => minDocument.createElement('div'))
|
|
||||||
])
|
|
||||||
|
|
||||||
const html = app.toString('/')
|
const html = app.toString('/')
|
||||||
const expected = '<div></div>'
|
const expected = '<div></div>'
|
||||||
@@ -34,11 +30,9 @@ test('server', function (t) {
|
|||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.router((route) => [
|
app.router(['/', (state, prev, send) => {
|
||||||
route('/', function (state, prev, send) {
|
return view`<h1>meow meow ${state.message}</h1>`
|
||||||
return view`<h1>meow meow ${state.message}</h1>`
|
}])
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
const html = app.toString('/', { message: 'nyan!' })
|
const html = app.toString('/', { message: 'nyan!' })
|
||||||
const expected = '<h1>meow meow nyan!</h1>'
|
const expected = '<h1>meow meow nyan!</h1>'
|
||||||
@@ -50,11 +44,9 @@ test('server', function (t) {
|
|||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.model({ state: { bin: 'baz', beep: 'boop' } })
|
app.model({ state: { bin: 'baz', beep: 'boop' } })
|
||||||
app.router((route) => [
|
app.router(['/', (state, prev, send) => {
|
||||||
route('/', function (state, prev, send) {
|
return view`<h1>${state.foo} ${state.bin} ${state.beep}</h1>`
|
||||||
return view`<h1>${state.foo} ${state.bin} ${state.beep}</h1>`
|
}])
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
const state = { foo: 'bar!', beep: 'beep' }
|
const state = { foo: 'bar!', beep: 'beep' }
|
||||||
const html = app.toString('/', state)
|
const html = app.toString('/', state)
|
||||||
@@ -70,13 +62,11 @@ test('server', function (t) {
|
|||||||
namespace: 'hello',
|
namespace: 'hello',
|
||||||
state: { bin: 'baz', beep: 'boop' }
|
state: { bin: 'baz', beep: 'boop' }
|
||||||
})
|
})
|
||||||
app.router((route) => [
|
app.router(['/', (state, prev, send) => {
|
||||||
route('/', function (state, prev, send) {
|
return view`
|
||||||
return view`
|
<h1>${state.hello.foo} ${state.hello.bin} ${state.hello.beep}</h1>
|
||||||
<h1>${state.hello.foo} ${state.hello.bin} ${state.hello.beep}</h1>
|
`
|
||||||
`
|
}])
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
hello: {
|
hello: {
|
||||||
@@ -93,12 +83,7 @@ test('server', function (t) {
|
|||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.router((route) => [
|
app.router(['/', (state, prev, send) => send('hey!')])
|
||||||
route('/', function (state, prev, send) {
|
|
||||||
send('hey!')
|
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
t.throws(app.toString.bind(null), /route must be a string/)
|
t.throws(app.toString.bind(null), /route must be a string/)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -106,12 +91,7 @@ test('server', function (t) {
|
|||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.router((route) => [
|
app.router(['/', (state, prev, send) => send('hey!')])
|
||||||
route('/', function (state, prev, send) {
|
|
||||||
send('hey!')
|
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
const msg = /send\(\) cannot be called/
|
const msg = /send\(\) cannot be called/
|
||||||
t.throws(app.toString.bind(null, '/', { message: 'nyan!' }), msg)
|
t.throws(app.toString.bind(null, '/', { message: 'nyan!' }), msg)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user