@@ -0,0 +1,10 @@
|
|||||||
|
## Thanks for contributing to Choo!
|
||||||
|
:steam_locomotive::train::train::train::train::train:
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
Browser tests can be run with the right credentials via the `npm run
|
||||||
|
test:browser` command. This will be run automatically when `npm version` is
|
||||||
|
executed.
|
||||||
|
|
||||||
|
You may skip the tests by providing `SKIP_TEST=true` when running the version
|
||||||
|
command.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
### Expected behavior
|
||||||
|
Write here.
|
||||||
|
|
||||||
|
### Actual behavior
|
||||||
|
Write here.
|
||||||
|
|
||||||
|
### Steps to reproduce behavior
|
||||||
|
Write here.
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
coverage/
|
coverage/
|
||||||
|
coverage.json
|
||||||
tmp/
|
tmp/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ node_js:
|
|||||||
- '6'
|
- '6'
|
||||||
sudo: false
|
sudo: false
|
||||||
language: node_js
|
language: node_js
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- xvfb
|
||||||
|
before_install:
|
||||||
|
- export DISPLAY=':99.0'
|
||||||
|
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||||
script: npm run test:cov
|
script: npm run test:cov
|
||||||
after_script: npm i -g codecov.io && cat ./coverage/lcov.info | codecov
|
after_script: npm i -g codecov.io && cat ./coverage/lcov.info | codecov
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -1,3 +1,85 @@
|
|||||||
|
## `3.0.0`
|
||||||
|
Woooh, happy third birthday `choo` - _thanks dad_. You're all grown up now;
|
||||||
|
look at how far you've come in the last month. You've grown... tinier? But yet
|
||||||
|
you do more? I love you `choo` - _shut up dad_.
|
||||||
|
|
||||||
|
### Notable changes
|
||||||
|
#### Who's the tiniest of them all?
|
||||||
|
`choo` is now `5kb` optimized! That's `2kb` less compared to v2. _Woah, how?_
|
||||||
|
We now support [yo-yoify](https://github.com/shama/yo-yoify) which optimizes
|
||||||
|
those lil template tags to `document.createElement()` calls. So not only is it
|
||||||
|
smaller, creating elements now has no overhead. Pretty nifty eh? Mad shoutout
|
||||||
|
to [Shama](http://twitter.com/shamakry) for building this!
|
||||||
|
|
||||||
|
#### Captain Hook(s)
|
||||||
|
V3 introduces `hooks` - powerful functions that are called at certain points in
|
||||||
|
the refresh cycle. Unlike functions in `models` these functions have unfiltered
|
||||||
|
access to all properties, call stacks and more. They're super useful when
|
||||||
|
building error handling, logging or persisting for stuff like `hot reloading`.
|
||||||
|
I quite like them, and I'm def keen to see what uses people will come up with!
|
||||||
|
|
||||||
|
#### Effect Composition :train::train::train::train:
|
||||||
|
`effects` are now composable by calling a `done(err, res)` callback when
|
||||||
|
they're done executing. This means that multiple namespaced effects can be
|
||||||
|
chained together to form some higher level behavior.
|
||||||
|
|
||||||
|
Think of cases like "logout" - multiple models must be cleared, perhaps tokens
|
||||||
|
invalidated on the server, all in a certain order. This requires multiple
|
||||||
|
models to work in tandem. - And now that's possible! :sparkles:
|
||||||
|
|
||||||
|
#### Pathfinders guide
|
||||||
|
We've started work on the [choo
|
||||||
|
handbook](https://github.com/yoshuawuyts/choo-handbook) - a lil manual to help
|
||||||
|
you get started, not only with choo, but with web development in general. It's
|
||||||
|
super modest still, only containing a single `choo` tutorial, but we'll be
|
||||||
|
expanding this over the coming months. If you want to contribute some docs,
|
||||||
|
there's [a whole section of
|
||||||
|
ideas](https://github.com/yoshuawuyts/choo-handbook/issues/10) on stuff that
|
||||||
|
might be neat to write. Any lil bits are welcome! Shout out to
|
||||||
|
[Tim](https://twitter.com/timwis) for making this happen :tada:
|
||||||
|
|
||||||
|
#### The Cycle of Life
|
||||||
|
`views` have gone through a bit of a change - they're now required using
|
||||||
|
`require('choo/html')` so they can be factored out of a project into standalone
|
||||||
|
[bel](https://github.com/shama/bel) components at any time. But additionally
|
||||||
|
these components have gained super powers through the adition of `onload` and
|
||||||
|
`onunload` hooks. Components can now react to being mounted or not, which makes
|
||||||
|
them ideal to implement standalone widgets. This behavior uses [html5
|
||||||
|
MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
|
||||||
|
under the hood, so it will work anywhere with a DOM! Again, this was all
|
||||||
|
[Shama](http://twitter.com/shamakry)'s hard work.
|
||||||
|
|
||||||
|
#### Test coverage
|
||||||
|
`choo` has gained a beaut blanket of tests, courtesy of
|
||||||
|
[Todd](https://twitter.com/whale_eat_squid) and
|
||||||
|
[Ben](https://twitter.com/bendrucker/). We've got server, browser and
|
||||||
|
_pretty-much-all-browsers-known-to-mankind_ style testing which should give us
|
||||||
|
a pretty good idea if stuff breaks. Neat!
|
||||||
|
|
||||||
|
#### Core dump
|
||||||
|
Internally we've moved the core of `choo` into a separate package -
|
||||||
|
[barracks](https://github.com/yoshuawuyts/barracks). `choo` is now mere glue
|
||||||
|
code around `barracks`, `yo-yo` and `sheet-router`. This is good news for folks
|
||||||
|
who like `choo`, but don't agree with all decisions. Go forth and build your
|
||||||
|
own lil framework!
|
||||||
|
|
||||||
|
### Changelog
|
||||||
|
- move `choo.view` out to `require('choo/html')` #71 | pr #103
|
||||||
|
- streamline view API #35 | pr #111
|
||||||
|
- higher order functions #34 | pr #104
|
||||||
|
- create lifecycle hooks #1 | feature addition in dependency covered by semver
|
||||||
|
- implement state hooks #15 | pr #104
|
||||||
|
- add yo-yoify #3 | pr #110
|
||||||
|
- rename "app" namespace #82 | pr #111
|
||||||
|
- enable browser testing | pr #86
|
||||||
|
- propagating actions creates infinite loop #114 | pr #104
|
||||||
|
- state is now immutable in `reducers` and `effects`
|
||||||
|
|
||||||
|
### Thanks
|
||||||
|
Huge thanks to everyone who's collaborated on this, provided feedback or
|
||||||
|
even mentioned it anywhere. It's been a hella lot of people, but seriously,
|
||||||
|
you're the best :steam_locomotive::train::train::train::train::train:
|
||||||
|
|
||||||
## `2.3.1`
|
## `2.3.1`
|
||||||
- [76](https://github.com/yoshuawuyts/choo/pull/76) - fix router arguments
|
- [76](https://github.com/yoshuawuyts/choo/pull/76) - fix router arguments
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<strong>Fun functional programming</strong>
|
<strong>Fun functional programming</strong>
|
||||||
</div>
|
</div>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
A <code>7kb</code> framework for creating sturdy frontend applications
|
A <code>5kb</code> framework for creating sturdy frontend applications
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
@@ -50,7 +50,19 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<div align="center">
|
||||||
|
<h3>
|
||||||
|
<a href="https://github.com/yoshuawuyts/choo-handbook">
|
||||||
|
Handbook
|
||||||
|
</a>
|
||||||
|
<span>|</span>
|
||||||
|
Packages
|
||||||
|
<span>|</span>
|
||||||
|
<a href="https://github.com/yoshuawuyts/choo/blob/master/.github/CONTRIBUTING.md">
|
||||||
|
Contributing
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<sub>The little framework that could. Built with ❤︎ by
|
<sub>The little framework that could. Built with ❤︎ by
|
||||||
@@ -60,35 +72,22 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Table of Contents
|
<h2>Table of Contents</h2>
|
||||||
- [Features](#features)
|
<details>
|
||||||
- [Demos](#demos)
|
<summary>Table of Contents</summary>
|
||||||
- [Getting started](#getting-started)
|
<li><a href="#features">Features</a></li>
|
||||||
- [Concepts](#concepts)
|
<li><a href="#demos">Demos</a></li>
|
||||||
- [Models](#models)
|
<li><a href="#example">Example</a></li>
|
||||||
- [Actions](#actions)
|
<li><a href="#philosophy">Philosophy</a></li>
|
||||||
- [Effects](#effects)
|
<li><a href="#concepts">Concepts</a></li>
|
||||||
- [Subscriptions](#subscriptions)
|
<li><a href="#api">API</a></li>
|
||||||
- [Router](#router)
|
<li><a href="#faq">FAQ</a></li>
|
||||||
- [Views](#views)
|
<li><a href="#installation">Installation</a></li>
|
||||||
- [Common actions](#common-actions)
|
<li><a href="#see-also">See Also</a></li>
|
||||||
- [HTTP](#http)
|
</details>
|
||||||
- [Server sent events](#server-sent-events-sse)
|
|
||||||
- [Keyboard](#keyboard)
|
|
||||||
- [Websockets](#websockets)
|
|
||||||
- [Forms](#forms)
|
|
||||||
- [Links](#links)
|
|
||||||
- [Rendering in Node](#rendering-in-node)
|
|
||||||
- [API](#api)
|
|
||||||
- [Errors](#errors)
|
|
||||||
- [FAQ](#faq)
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [See Also](#see-also)
|
|
||||||
- [License](#license)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- __minimal size:__ weighing `7kb`, `choo` is a tiny little framework
|
- __minimal size:__ weighing `5kb`, `choo` is a tiny little framework
|
||||||
- __single state:__ immutable single state helps reason about changes
|
- __single state:__ immutable single state helps reason about changes
|
||||||
- __small api:__ with only 6 methods, there's not a lot to learn
|
- __small api:__ with only 6 methods, there's not a lot to learn
|
||||||
- __minimal tooling:__ built for the cutting edge `browserify` compiler
|
- __minimal tooling:__ built for the cutting edge `browserify` compiler
|
||||||
@@ -111,76 +110,27 @@
|
|||||||
_note: If you've built something cool using `choo` or are using it in
|
_note: If you've built something cool using `choo` or are using it in
|
||||||
production, we'd love to hear from you!_
|
production, we'd love to hear from you!_
|
||||||
|
|
||||||
## Getting started
|
## Example
|
||||||
Let's create an input box that changes the content of a textbox in real time.
|
Let's create an input box that changes the content of a textbox in real time.
|
||||||
[Click here to see the final app](http://requirebin.com/?gist=e589473373b3100a6ace29f7bbee3186).
|
[Click here to see the app running](http://requirebin.com/?gist=e589473373b3100a6ace29f7bbee3186).
|
||||||
|
|
||||||
First we import `choo` and create a new instance:
|
|
||||||
```js
|
|
||||||
const choo = require('choo')
|
|
||||||
const app = choo()
|
|
||||||
```
|
|
||||||
|
|
||||||
Then we define a model. We set an initial value of `state` and a `reducer` that
|
|
||||||
can be called to modify it:
|
|
||||||
```js
|
|
||||||
app.model({
|
|
||||||
state: { title: 'Set the title' },
|
|
||||||
reducers: {
|
|
||||||
update: (action, state) => ({ title: action.value })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Then we create a new view. It has an `h1` tag which displays the current title,
|
|
||||||
and an `<input>` field which sends the current value of the text box on every
|
|
||||||
input:
|
|
||||||
```js
|
|
||||||
const mainView = (params, state, send) => choo.view`
|
|
||||||
<main>
|
|
||||||
<h1>${state.title}</h1>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
oninput=${(e) => send('update', { value: e.target.value })}>
|
|
||||||
</main>
|
|
||||||
`
|
|
||||||
```
|
|
||||||
|
|
||||||
_Note_: if an `id` property is defined on the outer-most element it will be
|
|
||||||
replaced.
|
|
||||||
|
|
||||||
We then bind the view to the `/` route on our application
|
|
||||||
```js
|
|
||||||
app.router((route) => [
|
|
||||||
route('/', mainView)
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
And then start the app and append it to the DOM. You can now run it and [see it
|
|
||||||
in action!](http://requirebin.com/?gist=e589473373b3100a6ace29f7bbee3186)
|
|
||||||
```js
|
|
||||||
const tree = app.start()
|
|
||||||
document.body.appendChild(tree)
|
|
||||||
```
|
|
||||||
|
|
||||||
And all together now:
|
|
||||||
```js
|
```js
|
||||||
const choo = require('choo')
|
const choo = require('choo')
|
||||||
|
const html = require('choo/html')
|
||||||
const app = choo()
|
const app = choo()
|
||||||
|
|
||||||
app.model({
|
app.model({
|
||||||
state: { title: 'Set the title' },
|
state: { title: 'Not quite set yet' },
|
||||||
reducers: {
|
reducers: {
|
||||||
update: (action, state) => ({ title: action.value })
|
update: (data, state) => ({ title: data })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const mainView = (params, state, send) => choo.view`
|
const mainView = (state, prev, send) => html`
|
||||||
<main>
|
<main>
|
||||||
<h1>${state.title}</h1>
|
<h1>Title: ${state.title}</h1>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
oninput=${(e) => send('update', { value: e.target.value })}>
|
oninput=${(e) => send('update', e.target.value)}>
|
||||||
</main>
|
</main>
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -192,6 +142,46 @@ const tree = app.start()
|
|||||||
document.body.appendChild(tree)
|
document.body.appendChild(tree)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To run it, save it as `client.js` and run with [budo][budo] and
|
||||||
|
[es2020][es2020]. These tools are convenient but any [browserify][browserify]
|
||||||
|
based tool should do:
|
||||||
|
```sh
|
||||||
|
$ budo 'client.js' -p 8080 --open -- -t es2020
|
||||||
|
```
|
||||||
|
|
||||||
|
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/bundle.js' > 'dist/bundle.js'
|
||||||
|
```
|
||||||
|
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
|
||||||
|
cool to be cute; using serious words without explaining them doesn't make for
|
||||||
|
better results - if anything it scares people off. We don't want to be scary,
|
||||||
|
we want to be nice and fun, and then _casually_ be the best choice around.
|
||||||
|
_Real casually._
|
||||||
|
|
||||||
|
We believe frameworks should be disposable, and components recyclable. We don't
|
||||||
|
like the current state of web development where walled gardens jealously
|
||||||
|
compete with one another. We want you to be free, not shackled to a damp
|
||||||
|
dungeon wall. By making the DOM the lowest common denominator, switching from
|
||||||
|
one framework to another becomes frictionless. Components should run anywhere
|
||||||
|
that has a DOM, regardless of the framework. `choo` is modest in its design; we
|
||||||
|
don't believe it will be top of the class forever, so we've made it as easy to
|
||||||
|
toss out as it is to pick up.
|
||||||
|
|
||||||
|
We don't believe that bigger is better. Big APIs, big dependencies, large file
|
||||||
|
sizes - we see them as omens of impending userland complexity. We want
|
||||||
|
everyone on a team, no matter the size, to fully understand how an application
|
||||||
|
is laid out. And once an application is built, we want it to be small,
|
||||||
|
performant and easy to reason about. All of which makes for easy to debug code,
|
||||||
|
better results and super smiley faces.
|
||||||
|
|
||||||
## Concepts
|
## Concepts
|
||||||
`choo` cleanly structures internal data flow, so that all pieces of logic can
|
`choo` cleanly structures internal data flow, so that all pieces of logic can
|
||||||
be combined into a nice, cohesive machine. Internally all logic lives within
|
be combined into a nice, cohesive machine. Internally all logic lives within
|
||||||
@@ -201,9 +191,9 @@ sources of data. `effects` react to changes, perform an `action` and can then
|
|||||||
post the results. `reducers` take data, modify it, and update the internal
|
post the results. `reducers` take data, modify it, and update the internal
|
||||||
`state`.
|
`state`.
|
||||||
|
|
||||||
Communication of data is done using objects called `actions`. Each `action` has
|
Communication of data is done using something called `actions`. Each `action`
|
||||||
any number of properties for data, and a unique `type` that can trigger
|
consists of a unique `actionName` and an optional payload of `data`, which can
|
||||||
properties on the models.
|
be any value.
|
||||||
|
|
||||||
When a `reducer` modifies `state`, the `router` is called, which in turn calls
|
When a `reducer` modifies `state`, the `router` is called, which in turn calls
|
||||||
`views`. `views` take `state` and return [DOM][dom] nodes which are then
|
`views`. `views` take `state` and return [DOM][dom] nodes which are then
|
||||||
@@ -236,7 +226,7 @@ application logic. This is the _unidirectional_ architecture of `choo`.
|
|||||||
- __views:__ take `state` and returns a new `DOM tree` that is rendered in the
|
- __views:__ take `state` and returns a new `DOM tree` that is rendered in the
|
||||||
browser
|
browser
|
||||||
|
|
||||||
## Models
|
### Models
|
||||||
`models` are objects that contain initial `state`, `subscriptions`, `effects`
|
`models` are objects that contain initial `state`, `subscriptions`, `effects`
|
||||||
and `reducers`. They're generally grouped around a theme (or domain, if you
|
and `reducers`. They're generally grouped around a theme (or domain, if you
|
||||||
like). To provide some sturdiness to your `models`, they can either be
|
like). To provide some sturdiness to your `models`, they can either be
|
||||||
@@ -244,17 +234,17 @@ namespaced or not. Namespacing means that only state within the model can be
|
|||||||
accessed. Models can still trigger actions on other models, though it's
|
accessed. Models can still trigger actions on other models, though it's
|
||||||
recommended to keep that to a minimum.
|
recommended to keep that to a minimum.
|
||||||
|
|
||||||
So say we have a `todos` namespace, an `add` reducer and a `todos` model.
|
So say we have a `myTodos` namespace, an `add` reducer and a `todos` model.
|
||||||
Outside the model they're called by `send('todos:add')` and
|
Outside the model they're called by `send('todos:add')` and
|
||||||
`state.todos.todos`. Inside the namespaced model they're called by
|
`state.todos.todos`. Inside the namespaced model they're called by
|
||||||
`send('todos:add')` and `state.todos`. An example namespaced model:
|
`send('todos:add')` and `state.todos`. An example namespaced model:
|
||||||
```js
|
```js
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.model({
|
app.model({
|
||||||
namespace: 'todos',
|
namespace: 'myTodos',
|
||||||
state: { todos: [] },
|
state: { todos: [] },
|
||||||
reducers: {
|
reducers: {
|
||||||
add: (action, state) => ({ todos: state.todos.concat(action.payload) })
|
add: (data, state) => ({ todos: state.todos.concat(data.payload) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
@@ -271,7 +261,7 @@ and has access to the full application state. Try and keep the logic in these
|
|||||||
bulk of your logic will safely shielded, with only a few points touching every
|
bulk of your logic will safely shielded, with only a few points touching every
|
||||||
part of your application.
|
part of your application.
|
||||||
|
|
||||||
## Effects
|
### Effects
|
||||||
Side effects are done through `effects` declared in `app.model()`. Unlike
|
Side effects are done through `effects` declared in `app.model()`. Unlike
|
||||||
`reducers` they cannot modify the state by returning objects, but get a
|
`reducers` they cannot modify the state by returning objects, but get a
|
||||||
callback passed which is used to emit `actions` to handle results. Use effects
|
callback passed which is used to emit `actions` to handle results. Use effects
|
||||||
@@ -286,7 +276,19 @@ A typical `effect` flow looks like:
|
|||||||
4. When the async call is done, either a success or error action is emitted
|
4. When the async call is done, either a success or error action is emitted
|
||||||
5. A reducer catches the action and updates the state
|
5. A reducer catches the action and updates the state
|
||||||
|
|
||||||
## Subscriptions
|
Examples of effects include: performing
|
||||||
|
[xhr](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) requests
|
||||||
|
(server requests), calling multiple `reducers`, persisting state to
|
||||||
|
[localstorage][localstorage].
|
||||||
|
|
||||||
|
When an `effect` is done executing, it should call the `done(err, res)`
|
||||||
|
callback. This callback used to communicate when an `effect` is done, handle
|
||||||
|
possible errors and send values back to the caller. You'll probably notice when
|
||||||
|
applications become more complex, that composing multiple namespaced models
|
||||||
|
using higher level effects becomes real powerful - without becoming
|
||||||
|
complicated.
|
||||||
|
|
||||||
|
### Subscriptions
|
||||||
Subscriptions are a way of receiving data from a source. For example when
|
Subscriptions are a way of receiving data from a source. For example when
|
||||||
listening for events from a server using `SSE` or `Websockets` for a
|
listening for events from a server using `SSE` or `Websockets` for a
|
||||||
chat app, or when catching keyboard input for a videogame.
|
chat app, or when catching keyboard input for a videogame.
|
||||||
@@ -297,15 +299,19 @@ const app = choo()
|
|||||||
app.model({
|
app.model({
|
||||||
namespace: 'app',
|
namespace: 'app',
|
||||||
subscriptions: [
|
subscriptions: [
|
||||||
(send) => setInterval(() => send('app:print', { payload: 'dog?' }), 1000)
|
(send, done) => {
|
||||||
|
setInterval(() => send('app:print', { payload: 'dog?' }), 1000)
|
||||||
|
}
|
||||||
],
|
],
|
||||||
effects: {
|
effects: {
|
||||||
print: (action, state) => console.log(action.payload)
|
print: (data, state) => console.log(data.payload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
If a `subscription` runs into an error, it can call `done(err)` to signal the
|
||||||
|
error to the error hook.
|
||||||
|
|
||||||
## Router
|
### Router
|
||||||
The `router` manages which `views` are rendered at any given time. It also
|
The `router` manages which `views` are rendered at any given time. It also
|
||||||
supports rendering a default `view` if no routes match.
|
supports rendering a default `view` if no routes match.
|
||||||
|
|
||||||
@@ -327,338 +333,99 @@ figure out how views relate to each other.
|
|||||||
Under the hood `choo` uses [sheet-router][sheet-router]. Internally the
|
Under the hood `choo` uses [sheet-router][sheet-router]. Internally the
|
||||||
currently rendered route is kept in `state.app.location`. If you want to modify
|
currently rendered route is kept in `state.app.location`. If you want to modify
|
||||||
the location programmatically the `reducer` for the location can be called
|
the location programmatically the `reducer` for the location can be called
|
||||||
using `send('app:location', { location: href })`. This will not work from
|
using `send('location:setLocation', { location: href })`. This will not work
|
||||||
within namespaced `models`, and usage should preferably be kept to a minimum.
|
from within namespaced `models`, and usage should preferably be kept to a
|
||||||
Changing views all over the place tends to lead to messiness.
|
minimum. Changing views all over the place tends to lead to messiness.
|
||||||
|
|
||||||
## Views
|
### Views
|
||||||
Views are pure functions that return a DOM tree for the router to render. They’re passed the current state, and any time the state changes they’re run again with the new state.
|
Views are pure functions that return a DOM tree for the router to render. They’re passed the current state, and any time the state changes they’re run again with the new state.
|
||||||
|
|
||||||
Views are also passed the `send` function, which they can use to dispatch actions that can update the state. For example, the DOM tree can have an `onclick` handler that dispatches an `add` action.
|
Views are also passed the `send` function, which they can use to dispatch actions that can update the state. For example, the DOM tree can have an `onclick` handler that dispatches an `add` action.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const view = (params, state, send) => {
|
const view = (state, prev, send) => {
|
||||||
return choo.view`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<h1>Total todos: ${state.todos.length}</h1>
|
<h1>Total todos: ${state.todos.length}</h1>
|
||||||
<button onclick=${(e) => send('add', { payload: {title: 'demo'})}>Add</button>
|
<button onclick=${(e) => send('add', {title: 'demo'})}>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
In this example, when the `Add` button is clicked, the view will dispatch an `add` action that the model’s `add` reducer will receive. [As seen above](#models), the reducer will add an item to the state’s `todos` array. The state change will cause this view to be run again with the new state, and the resulting DOM tree will be used to [efficiently patch the DOM](#does-choo-use-a-virtual-dom).
|
In this example, when the `Add` button is clicked, the view will dispatch an `add` action that the model’s `add` reducer will receive. [As seen above](#models), the reducer will add an item to the state’s `todos` array. The state change will cause this view to be run again with the new state, and the resulting DOM tree will be used to [efficiently patch the DOM](#does-choo-use-a-virtual-dom).
|
||||||
|
|
||||||
## Common Actions
|
|
||||||
### HTTP
|
|
||||||
`choo` ships with a built-in [`http` module](https://github.com/Raynos/xhr)
|
|
||||||
that weighs only `2.4kb`:
|
|
||||||
```js
|
|
||||||
const http = require('choo/http')
|
|
||||||
const choo = require('choo')
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
effects: {
|
|
||||||
'app:error': (state, event) => console.error(`error: ${event.payload}`),
|
|
||||||
'app:print': (state, event) => console.log(`http: ${event.payload}`),
|
|
||||||
'http:get_json': getJson,
|
|
||||||
'http:post_json': postJson,
|
|
||||||
'http:delete': httpDelete
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function getJson (state, action, send) {
|
|
||||||
http.get('/my-endpoint', { json: true }, function (err, res, body) {
|
|
||||||
if (err) return send('app:error', { payload: err.message })
|
|
||||||
if (res.statusCode !== 200 || !body) {
|
|
||||||
return send('app:error', { payload:'something went wrong' })
|
|
||||||
}
|
|
||||||
send('app:print', { payload: body })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function postJson (state, action, send) {
|
|
||||||
const body = { foo: 'bar' }
|
|
||||||
http.post('/my-endpoint', { json: body }, function (err, res, body) {
|
|
||||||
if (err) return send('app:error', { payload: err.message })
|
|
||||||
if (res.statusCode !== 200 || !body) {
|
|
||||||
return send('app:error', { payload:'something went wrong' })
|
|
||||||
}
|
|
||||||
send('app:print', { payload: body })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function httpDelete (state, action, send) {
|
|
||||||
const body = { foo: 'bar' }
|
|
||||||
http.del('/my-endpoint', { json: body }, function (err, res, body) {
|
|
||||||
if (err) return send('app:error', { payload: err.message })
|
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
return send('app:error', { payload:'something went wrong' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Note that `http` only runs in the browser to prevent accidental requests when
|
|
||||||
rendering in Node. For more details view the [`raynos/xhr`
|
|
||||||
documentation](https://github.com/Raynos/xhr).
|
|
||||||
|
|
||||||
### Server Sent Events (SSE)
|
|
||||||
[Server Sent Events (SSE)][sse] allow servers to push data to the browser.
|
|
||||||
They're the unidirectional cousin of `websockets` and compliment `HTTP`
|
|
||||||
brilliantly. To enable `SSE`, create a new `EventSource`, point it at a local
|
|
||||||
uri (generally `/sse`) and setup a `subscription`:
|
|
||||||
```js
|
|
||||||
const stream = new document.EventSource('/sse')
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
subscriptions: [
|
|
||||||
function (send) {
|
|
||||||
stream.onerror = (e) => send('app:error', { payload: JSON.stringify(e) })
|
|
||||||
stream.onmessage = (e) => send('app:print', { payload: e.data })
|
|
||||||
}
|
|
||||||
],
|
|
||||||
effects: {
|
|
||||||
'sse:close': () => stream.close(),
|
|
||||||
'app:error': (state, event) => console.error(`error: ${event.payload}`),
|
|
||||||
'app:print': (state, event) => console.log(`sse: ${event.payload}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
This code does not handle reconnects, server timeouts, exponential backoff and
|
|
||||||
queueing data. You might want to use a package from `npm` or [write your
|
|
||||||
own][sse-reconnect] if you're building something for production.
|
|
||||||
|
|
||||||
### Keyboard
|
|
||||||
Most browsers have [basic support for keyboard events][keyboard-support]. To
|
|
||||||
capture keyboard events, setup a `subscription`:
|
|
||||||
```js
|
|
||||||
app.model({
|
|
||||||
namespace: 'input',
|
|
||||||
subscriptions: [
|
|
||||||
function (send) {
|
|
||||||
document.addEventListener(
|
|
||||||
'keypress',
|
|
||||||
(e) => send('input:print', { payload: e.keyCode })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
],
|
|
||||||
effects: {
|
|
||||||
print: (state) => console.log(`pressed key: ${state.payload}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### WebSockets
|
|
||||||
[WebSockets][ws] allow for bidirectional communication between servers and
|
|
||||||
browsers:
|
|
||||||
```js
|
|
||||||
const socket = new document.WebSocket('ws://localhost:8081')
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
subscriptions: [
|
|
||||||
function (send) {
|
|
||||||
socket.onerror = (e) => send('app:error', { payload: JSON.stringify(e) })
|
|
||||||
socket.onmessage = (e) => send('app:print', { payload: e.data })
|
|
||||||
}
|
|
||||||
],
|
|
||||||
effects: {
|
|
||||||
'ws:close': () => socket.close(),
|
|
||||||
'ws:send': (state, event) => socket.send(JSON.stringify(event.payload)),
|
|
||||||
'app:error': (state, event) => console.error(`error: ${event.payload}`),
|
|
||||||
'app:print': (state, event) => console.log(`ws: ${event.payload}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
This code does not handle reconnects, server timeouts, exponential backoff and
|
|
||||||
queueing data. You might want to use a package from `npm` or [write your
|
|
||||||
own][ws-reconnect] if you're building something for production.
|
|
||||||
|
|
||||||
### Forms
|
|
||||||
Forms and lists are probably the most used concepts on any page. Together with
|
|
||||||
links they comprise most of what can be done on web pages.
|
|
||||||
```js
|
|
||||||
const choo = require('choo')
|
|
||||||
const http = require('choo/http')
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
function view (params, state, send) {
|
|
||||||
return choo.view`
|
|
||||||
<form onsubmit=${onSubmit}>
|
|
||||||
<fieldset>
|
|
||||||
<label>username</label>
|
|
||||||
<input type="text" name="username" autofocus>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<label>password</label>
|
|
||||||
<input type="password" name="password">
|
|
||||||
</fieldset>
|
|
||||||
<input type="submit" value="Submit">
|
|
||||||
</form>
|
|
||||||
`
|
|
||||||
|
|
||||||
function onSubmit (event) {
|
|
||||||
send('login', { data: new FormData(event.target) })
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
effects: {
|
|
||||||
login: (action, state, send) => {
|
|
||||||
http.post('/login', { body: action.data }, (err, res, body) => {
|
|
||||||
send('authorize', { payload: body })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.router((route) => [
|
|
||||||
route('/', view)
|
|
||||||
])
|
|
||||||
|
|
||||||
app.start()
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want a form element to be selected when it's loaded, add the
|
|
||||||
[`autofocus`][html-input] property.
|
|
||||||
```js
|
|
||||||
const view = choo.view`
|
|
||||||
<form>
|
|
||||||
<input type="text" autofocus>
|
|
||||||
</form>
|
|
||||||
`
|
|
||||||
```
|
|
||||||
|
|
||||||
### Links
|
|
||||||
In HTML links are represented with the `<a href="/some-location">` tag. By
|
|
||||||
default `choo` enables a `subscription` for all `a` tags on a page. When a link
|
|
||||||
is clicked, the click event is caught, and the value of `href` is passed into
|
|
||||||
the router causing a state change. If you want to disable this behavior, set
|
|
||||||
`app.start({ href: false })`.
|
|
||||||
```js
|
|
||||||
const nav = choo.view`
|
|
||||||
<a href="/">home</a>
|
|
||||||
<a href="/first-link">first link</a>
|
|
||||||
<a href="/second-link">second link</a>
|
|
||||||
`
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rendering in Node
|
|
||||||
Sometimes it's necessary to render code inside of Node; for serving hyper fast
|
|
||||||
first requests, testing or other purposes. Applications that are capable of
|
|
||||||
being rendered in both Node and the browser are called
|
|
||||||
_[isomorphic][isomorphic]_.
|
|
||||||
|
|
||||||
Rendering in Node is slightly different than in the browser. First off, to
|
|
||||||
maintain performance all calls to `subscriptions`, `effects`, and `reducers`
|
|
||||||
are disabled. That means you need to know what the state of your application is
|
|
||||||
going to be _before_ you render it - no cheating!
|
|
||||||
|
|
||||||
Secondly, the `send()` method inside `router` and `view` has been disabled. If
|
|
||||||
you call it your program will crash. Disabling all these things means that your
|
|
||||||
program will render [`O(n)`][big-o], which is super neat. Off to [10.000
|
|
||||||
QPS][qps] we go!
|
|
||||||
|
|
||||||
To render in Node call the `.toString()` method instead of `.start()`. The
|
|
||||||
first argument is the path that should be rendered, the second is the state:
|
|
||||||
```js
|
|
||||||
const http = require('http')
|
|
||||||
const client = require('./client') // path to client entry point
|
|
||||||
http.createServer(function (req, res) {
|
|
||||||
const html = client.toString('/', { message: 'hello server!' })
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
||||||
res.end(html)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
In order to make our `choo` app call `app.start()` in the browser and be
|
|
||||||
`require()`-able in Node, we check if [`module.parent`][module-parent] exists:
|
|
||||||
```js
|
|
||||||
const choo = require('choo')
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
app.router((route) => [
|
|
||||||
route('/', (params, state, send) => choo.view`
|
|
||||||
<h1>${state.message}</h1>
|
|
||||||
`)
|
|
||||||
])
|
|
||||||
|
|
||||||
if (module.parent) module.exports = app
|
|
||||||
else document.body.appendChild(app.start())
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Rehydration
|
|
||||||
Now that your application is successfully rendering in Node, the next step would
|
|
||||||
be to make it load a JavaScript bundle once has loaded the HTML. To do this we
|
|
||||||
will use a technique called _rehydration_.
|
|
||||||
|
|
||||||
_Rehydration_ is when you take the static, server-rendered version of your
|
|
||||||
application (static HTML, _dehydrated_ because it has no logic) and _rehydrate_
|
|
||||||
it by booting up the JS and attaching event handlers on the DOM to make it
|
|
||||||
dynamic again. It's like restoring flavor to cup noodles by adding hot water.
|
|
||||||
|
|
||||||
Because we're using something called `morphdom` under the hood, all we need is
|
|
||||||
point at an `id` at the root of the application. The syntax for this is
|
|
||||||
slightly different from what we've seen so far, because we're _updating_ a
|
|
||||||
dehydrated DOM nodes to make them dynamic, rather than a new DOM tree and
|
|
||||||
attaching it to the DOM.
|
|
||||||
```js
|
|
||||||
const choo = require('choo')
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
app.router((route) => [
|
|
||||||
route('/', (params, state, send) => choo.view`
|
|
||||||
<h1 id="app-root">${state.message}</h1>
|
|
||||||
`)
|
|
||||||
])
|
|
||||||
|
|
||||||
if (module.parent) module.exports = app
|
|
||||||
else app.start('#app-root'))
|
|
||||||
```
|
|
||||||
|
|
||||||
When the JS is booted on top of the dehydrated application, it will look for
|
|
||||||
the `#app-root` id and load on top of it. You can choose any name you like for
|
|
||||||
the id, but __make sure it's the same on every possible top level DOM node__,
|
|
||||||
or else things might break. Furthermore to ensure things go smoothly, try and
|
|
||||||
keep the initial state identical on both the server and the client.
|
|
||||||
|
|
||||||
And that's it! If you want to go down the route of mad performance, consider
|
|
||||||
make all first request static and caching them using something like [bl][bl],
|
|
||||||
[nginx][nginx], [varnish][varnish] or a global CDN.
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
### app = choo()
|
This section provides documentation on how each function in `choo` works. It's
|
||||||
Create a new `choo` app
|
intended to be a technical reference. If you're interested in learning choo for
|
||||||
|
the first time, consider reading through the [handbook][handbook] or
|
||||||
|
[concepts](#concepts) first :sparkles:
|
||||||
|
|
||||||
|
### app = choo(opts)
|
||||||
|
Initialize a new `choo` app. Takes an optional object of handlers. Handlers can
|
||||||
|
be:
|
||||||
|
- __onError(err, state, createSend):__ called when an `effect` or
|
||||||
|
`subscription` emit an error. If no handler is passed, the default handler
|
||||||
|
will `throw` on each error.
|
||||||
|
- __onAction(action, state, name, caller, createSend):__ called when an
|
||||||
|
`action` is fired.
|
||||||
|
- __onStateChange(action, state, prev, caller, createSend):__ called after a
|
||||||
|
reducer changes the `state`.
|
||||||
|
|
||||||
|
`createSend()` is a special function that allows the creation of a new named
|
||||||
|
`send()` function. The first argument should be a string which is the name, the
|
||||||
|
second argument is a boolean `callOnError` which can be set to `true` to call
|
||||||
|
the `onError` hook istead of a provided callback. It then returns a
|
||||||
|
`send(actionName, data?)` function.
|
||||||
|
|
||||||
|
Handlers should be used with care, as they're the most powerful interface into
|
||||||
|
the state. For application level code it's generally recommended to delegate to
|
||||||
|
actions inside models using the `send()` call, and only shape the actions
|
||||||
|
inside the handlers.
|
||||||
|
|
||||||
### app.model(obj)
|
### app.model(obj)
|
||||||
Create a new model. Models modify data and perform IO. Takes the following
|
Create a new model. Models modify data and perform IO. Takes the following
|
||||||
arguments:
|
arguments:
|
||||||
- __namespace:__ optional namespace that prefixes the keys in `state`,
|
- __namespace:__ namespace the model so that it cannot access any properties
|
||||||
`reducers` and `effects`. Also limits `actions` called by `send()` to
|
and handlers in other models
|
||||||
in-namespace only.
|
- __state:__ initial values of `state` inside the model
|
||||||
- __state:__ object. Key value store of initial values
|
- __reducers:__ synchronous operations that modify state. Triggered by
|
||||||
- __reducers:__ object. Syncronous functions that modify state. Each function
|
`actions`. Signature of `(data, state)`.
|
||||||
has a signature of `(action, state)`
|
- __effects:__ asynchronous operations that don't modify state directly.
|
||||||
- __effects:__ object. Asyncronous functions that perform IO. Each function has
|
Triggered by `actions`, can call `actions`. Signature of `(data, state,
|
||||||
a signature of `(action, state, send)` where `send` is a reference to
|
send, done)`
|
||||||
`app.send()`
|
- __subscriptions:__ asynchronous read-only operations that don't modify state
|
||||||
|
directly. Can call `actions`. Signature of `(state, send, done)`.
|
||||||
|
|
||||||
### choo.view\`html\`
|
#### send(actionName, data?)
|
||||||
Tagged template string HTML builder. See
|
Send a new action to the models with optional data attached. Namespaced models
|
||||||
[`yo-yo`](https://github.com/maxogden/yo-yo) for full documentation. Views
|
can be accessed by prefixing the name with the namespace separated with a `:`,
|
||||||
should be passed to `app.router()`
|
e.g. `namespace:name`.
|
||||||
|
|
||||||
### app.router(params, state, send)
|
#### done(err?, res?)
|
||||||
Creates a new router. See
|
When an `effect` or `subscription` is done executing, or encounters an error,
|
||||||
|
it should call the final `done(err, res)` callback. If an `effect` was called
|
||||||
|
by another `effect` it will call the callback of the caller. When an error
|
||||||
|
propegates all the way to the top, the `onError` handler will be called,
|
||||||
|
registered in `choo(handlers)`. If no callback is registered, errors will
|
||||||
|
`throw`.
|
||||||
|
|
||||||
|
### app.router(defaultRoute?, (route) => [routes])
|
||||||
|
Creates a new router. Takes a function that exposes a single `route` function,
|
||||||
|
and that expects a tree of `routes` to be returned. See
|
||||||
[`sheet-router`](https://github.com/yoshuawuyts/sheet-router) for full
|
[`sheet-router`](https://github.com/yoshuawuyts/sheet-router) for full
|
||||||
documentation. Registered views have a signature of `(params, state, send)`,
|
documentation. Registered views have a signature of `(state, prev, send)`,
|
||||||
where `params` is URI partials.
|
where `state` is the current `state`, `prev` is the last state, `state.params`
|
||||||
|
is URI partials and `send()` can be called to trigger actions. If
|
||||||
|
`defaultRoute` is passed in, that will be called if no paths match. If no
|
||||||
|
`defaultRoute` is specified it will throw instead.
|
||||||
|
|
||||||
### html = app.toString(route, state)
|
### html = app.toString(route, state?)
|
||||||
Render the application to a string of HTML. Useful for rendering on the server.
|
Render the application to a string of HTML. Useful for rendering on the server.
|
||||||
First argument is a path that's passed to the router. Second argument is the
|
First argument is a path that's passed to the router. Second argument is an
|
||||||
state object. When calling `.toString()` instead of `.start()`, all calls to
|
optional state object. When calling `.toString()` instead of `.start()`, all
|
||||||
`send()` are disabled, and `subscriptions`, `effects` and `reducers` aren't
|
calls to `send()` are disabled, and `subscriptions`, `effects` and `reducers`
|
||||||
loaded. See [rendering in Node](#rendering-in-node) for an in-depth guide.
|
aren't loaded.
|
||||||
|
|
||||||
### tree = app.start(rootId?, opts)
|
### tree = app.start(rootId?, opts)
|
||||||
Start the application. Returns a tree of DOM nodes that can be mounted using
|
Start the application. Returns a tree of DOM nodes that can be mounted using
|
||||||
@@ -667,8 +434,8 @@ first argument, the tree will diff against the selected node rather than be
|
|||||||
returned. This is useful for [rehydration](#rehydration). Opts can contain the
|
returned. This is useful for [rehydration](#rehydration). Opts can contain the
|
||||||
following values:
|
following values:
|
||||||
- __opts.history:__ default: `true`. Enable a `subscription` to the browser
|
- __opts.history:__ default: `true`. Enable a `subscription` to the browser
|
||||||
history API. e.g. updates the internal `state.location` state whenever the
|
history API. e.g. updates the internal `location.href` state whenever the
|
||||||
browser "forward" and "backward" buttons are pressed.
|
browsers "forward" and "backward" buttons are pressed.
|
||||||
- __opts.href:__ default: `true`. Handle all relative `<a
|
- __opts.href:__ default: `true`. Handle all relative `<a
|
||||||
href="<location>"></a>` clicks and update internal `state.location`
|
href="<location>"></a>` clicks and update internal `state.location`
|
||||||
accordingly.
|
accordingly.
|
||||||
@@ -677,40 +444,40 @@ following values:
|
|||||||
changes (eg `localhost/#posts/123`). Enabling this option automatically
|
changes (eg `localhost/#posts/123`). Enabling this option automatically
|
||||||
disables `opts.history` and `opts.href`.
|
disables `opts.history` and `opts.href`.
|
||||||
|
|
||||||
## Errors
|
### view = require('choo/html')\`html\`
|
||||||
### Could not find DOM node (#id) to update
|
Tagged template string HTML builder. Built on top of [yo-yo][yo-yo], [bel][bel]
|
||||||
This means that a re-render of the DOM was triggered before the first render
|
and [hyperx][hyperx]. To register a view on the `router` it should be wrapped
|
||||||
was done. This is usually the case when `send()` is called inside a
|
in a function with the signature of `(state, prev, send)` where `state` is the
|
||||||
`subscription` before the DOM is done rendering. Instead try listening for a
|
current `state`, `prev` is the last state, `state.params` is URI partials and
|
||||||
`'DOMContentLoaded'` event:
|
`send()` can be called to trigger actions.
|
||||||
```js
|
|
||||||
document.addEventListener('DOMContentLoaded', (e) => send('init'))
|
|
||||||
```
|
|
||||||
|
|
||||||
### send() cannot be called on the server
|
To create listeners for events, create interpolated attributes on elements.
|
||||||
This means a `send()` event was triggered in Node. In Node, `reducers`,
|
```js
|
||||||
`effects` and `subscriptions` are disabled for performance reasons, so if
|
const html = require('choo/html')
|
||||||
`send()` was called to trigger an action it wouldn't work. Try finding where in
|
html`
|
||||||
the DOM tree `send()` is called, and disable it when called from within Node.
|
<button onclick=${(e) => console.log(e)}>click for bananas</button>
|
||||||
|
`
|
||||||
|
```
|
||||||
|
Example listeners include: `onclick`, `onsubmit`, `oninput`, `onkeydown`,
|
||||||
|
`onkeyup`. A full list can be found [at the yo-yo
|
||||||
|
repo](https://github.com/maxogden/yo-yo/blob/master/update-events.js). When
|
||||||
|
creating listeners always remember to call `e.preventDefault()` on the event so
|
||||||
|
it doesn't bubble up and do stuff like refreshing the full page or the like.
|
||||||
|
|
||||||
|
To trigger lifecycle events on any part of a view, set the `onload=${(el) =>
|
||||||
|
{}}` and `onunload=${() => {el}}` attributes. These parameters are useful when
|
||||||
|
creating self-contained widgets that take care of their own state and lifecycle
|
||||||
|
(e.g. a maps widget) or to trigger animations. Most elements shouldn't have a
|
||||||
|
need for these hooks though.
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
### Why did you build this?
|
|
||||||
`choo` is nothing but a formalization of how I've been building my applications
|
|
||||||
for the past year. I originally used `virtual-dom` with `virtual-app` and
|
|
||||||
`wayfarer` where now it's `yo-yo` with `send-action` and `sheet-router`. The
|
|
||||||
main benefit of using `choo` over these technologies separately is that it
|
|
||||||
becomes easier for teams to pick up and gather around. The code base for `choo`
|
|
||||||
itself is super petite (`~200` LOC) and mostly acts to enforce structure around
|
|
||||||
some excellent npm packages. This is my take on modular frameworks; I hope
|
|
||||||
you'll find it pleasant.
|
|
||||||
|
|
||||||
### Why is it called choo?
|
### Why is it called choo?
|
||||||
Because I thought it sounded cute. All these programs talk about being
|
Because I thought it sounded cute. All these programs talk about being
|
||||||
"performant", "rigid", "robust" - I like programming to be light, fun and
|
_"performant"_, _"rigid"_, _"robust"_ - I like programming to be light, fun and
|
||||||
non-scary. `choo` embraces that.
|
non-scary. `choo` embraces that.
|
||||||
|
|
||||||
Also imagine telling some business people you chose to rewrite something
|
Also imagine telling some business people you chose to rewrite something
|
||||||
critical to the company using the `choo` framework.
|
critical to the company using `choo`.
|
||||||
:steam_locomotive::train::train::train:
|
:steam_locomotive::train::train::train:
|
||||||
|
|
||||||
### Why is it a framework, and not a library?
|
### Why is it a framework, and not a library?
|
||||||
@@ -718,7 +485,13 @@ I love small libraries that do one thing well, but when working in a team,
|
|||||||
having an undocumented combination of packages often isn't great. `choo()` is a
|
having an undocumented combination of packages often isn't great. `choo()` is a
|
||||||
small set of packages that work well together, wrapped in an an architectural
|
small set of packages that work well together, wrapped in an an architectural
|
||||||
pattern. This means you get all the benefits of small packages, but get to be
|
pattern. This means you get all the benefits of small packages, but get to be
|
||||||
productive right from the start.
|
productive right from the start without needing to plough through layers of
|
||||||
|
boilerplate.
|
||||||
|
|
||||||
|
### Is it called choo, choo.js or...?
|
||||||
|
It's called "choo", though we're fine if you call it "choo-choo" or
|
||||||
|
"chugga-chugga-choo-choo" too. The only time "choo.js" is tolerated is if /
|
||||||
|
when you shimmy like you're a locomotive.
|
||||||
|
|
||||||
### How does choo compare to X?
|
### How does choo compare to X?
|
||||||
Ah, so this is where I get to rant. `choo` (_chugga-chugga-chugga-choo-choo!_)
|
Ah, so this is where I get to rant. `choo` (_chugga-chugga-chugga-choo-choo!_)
|
||||||
@@ -763,9 +536,16 @@ give you my opinions directly. Ready? Here goes:
|
|||||||
provides framework lock in, and additionally doesn't have a clean enough
|
provides framework lock in, and additionally doesn't have a clean enough
|
||||||
architecture. I appreciate what it does, but don't think it's the answer.
|
architecture. I appreciate what it does, but don't think it's the answer.
|
||||||
|
|
||||||
|
### Why can't send() be called on the server?
|
||||||
|
In Node, `reducers`, `effects` and `subscriptions` are disabled for performance
|
||||||
|
reasons, so if `send()` was called to trigger an action it wouldn't work. Try
|
||||||
|
finding where in the DOM tree `send()` is called, and disable it when called
|
||||||
|
from within Node.
|
||||||
|
|
||||||
### Which packages was choo built on?
|
### Which packages was choo built on?
|
||||||
- __views:__ [`yo-yo`](https://github.com/maxogden/yo-yo)
|
- __views:__ [`yo-yo`](https://github.com/maxogden/yo-yo),
|
||||||
- __models:__ [`send-action`](https://github.com/sethvincent/send-action),
|
[`bel`](https://github.com/shama/bel)
|
||||||
|
- __models:__ [`barracks`](https://github.com/yoshuawuyts/barracks),
|
||||||
[`xtend`](https://github.com/raynos/xtend)
|
[`xtend`](https://github.com/raynos/xtend)
|
||||||
- __routes:__ [`sheet-router`](https://github.com/yoshuawuyts/sheet-router)
|
- __routes:__ [`sheet-router`](https://github.com/yoshuawuyts/sheet-router)
|
||||||
- __http:__ [`xhr`](https://github.com/Raynos/xhr)
|
- __http:__ [`xhr`](https://github.com/Raynos/xhr)
|
||||||
@@ -777,13 +557,6 @@ with DOM nodes][morphdom-bench], and it has the added benefit of working with
|
|||||||
_any_ library that produces valid DOM nodes. So to put a long answer short:
|
_any_ library that produces valid DOM nodes. So to put a long answer short:
|
||||||
we're using something even better.
|
we're using something even better.
|
||||||
|
|
||||||
### What packages do you recommend to pair with choo?
|
|
||||||
- [tachyons](https://github.com/tachyons-css/tachyons) - functional CSS for
|
|
||||||
humans
|
|
||||||
- [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for
|
|
||||||
browserify
|
|
||||||
- [pull-stream](https://github.com/pull-stream/pull-stream) - minimal streams
|
|
||||||
|
|
||||||
### How can I optimize choo?
|
### How can I optimize choo?
|
||||||
`choo` really shines when coupled with `browserify` transforms. They can do
|
`choo` really shines when coupled with `browserify` transforms. They can do
|
||||||
things like reduce file size, prune dependencies and clean up boilerplate code.
|
things like reduce file size, prune dependencies and clean up boilerplate code.
|
||||||
@@ -791,7 +564,11 @@ Consider running some of the following:
|
|||||||
- [unassertify](https://github.com/twada/unassertify) - remove `assert()`
|
- [unassertify](https://github.com/twada/unassertify) - remove `assert()`
|
||||||
statements which reduces file size. Use as a `--global` transform
|
statements which reduces file size. Use as a `--global` transform
|
||||||
- [es2020](https://github.com/yoshuawuyts/es2020) - backport `const`,
|
- [es2020](https://github.com/yoshuawuyts/es2020) - backport `const`,
|
||||||
`fat-arrows` and `template strings` to older browsers
|
`fat-arrows` and `template strings` to older browsers. Should be run as a
|
||||||
|
`--global` transform
|
||||||
|
- [yo-yoify](https://github.com/shama/yo-yoify) - replace the internal `hyperx`
|
||||||
|
dependency with `document.createElement` calls; greatly speeds up performance
|
||||||
|
too
|
||||||
- [uglifyify](https://github.com/hughsk/uglifyify) - minify your code using
|
- [uglifyify](https://github.com/hughsk/uglifyify) - minify your code using
|
||||||
UglifyJS2. Use as a `--global` transform
|
UglifyJS2. Use as a `--global` transform
|
||||||
- [bulkify](https://www.npmjs.com/package/bulkify) - transform inline
|
- [bulkify](https://www.npmjs.com/package/bulkify) - transform inline
|
||||||
@@ -816,6 +593,7 @@ Generally for production builds you'll want to run:
|
|||||||
```sh
|
```sh
|
||||||
$ NODE_ENV=production browserify \
|
$ NODE_ENV=production browserify \
|
||||||
-t envify \
|
-t envify \
|
||||||
|
-g yo-yoify \
|
||||||
-g unassertify \
|
-g unassertify \
|
||||||
-g es2020 \
|
-g es2020 \
|
||||||
-g uglifyify \
|
-g uglifyify \
|
||||||
@@ -841,20 +619,22 @@ Sure.
|
|||||||
$ npm install choo
|
$ npm install choo
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
|
||||||
Browser tests can be run with the right credentials via the `npm run test:browser`
|
|
||||||
command. This will be run automatically when `npm version` is executed.
|
|
||||||
|
|
||||||
You may skip the tests by providing `SKIP_TEST=true` when running the version
|
|
||||||
command.
|
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
- [choo-handbook](https://github.com/yoshuawuyts/choo-handbook) - the little
|
||||||
|
`choo` guide
|
||||||
- [budo](https://github.com/mattdesl/budo) - quick prototyping tool for
|
- [budo](https://github.com/mattdesl/budo) - quick prototyping tool for
|
||||||
`browserify`
|
`browserify`
|
||||||
- [stack.gl](http://stack.gl/) - open software ecosystem for WebGL
|
- [stack.gl](http://stack.gl/) - open software ecosystem for WebGL
|
||||||
- [yo-yo](https://github.com/maxogden/yo-yo) - tiny library for modular UI
|
- [yo-yo](https://github.com/maxogden/yo-yo) - tiny library for modular UI
|
||||||
- [bel](https://github.com/shama/bel) - composable DOM elements using template
|
- [bel](https://github.com/shama/bel) - composable DOM elements using template
|
||||||
strings
|
strings
|
||||||
|
- [tachyons](https://github.com/tachyons-css/tachyons) - functional CSS for
|
||||||
|
humans
|
||||||
|
- [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for
|
||||||
|
`browserify`
|
||||||
|
- [pull-stream](https://github.com/pull-stream/pull-stream) - minimal streams
|
||||||
|
- [es2020](https://github.com/yoshuawuyts/es2020) - because in hindsight we
|
||||||
|
don't need most of ES6
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[MIT](https://tldrlegal.com/license/mit-license)
|
[MIT](https://tldrlegal.com/license/mit-license)
|
||||||
@@ -878,3 +658,11 @@ command.
|
|||||||
[sheet-router]: https://github.com/yoshuawuyts/sheet-router
|
[sheet-router]: https://github.com/yoshuawuyts/sheet-router
|
||||||
[html-input]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
|
[html-input]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
|
||||||
[inu]: https://github.com/ahdinosaur/inu
|
[inu]: https://github.com/ahdinosaur/inu
|
||||||
|
[yo-yo]: https://github.com/maxogden/yo-yo
|
||||||
|
[bel]: https://github.com/shama/bel
|
||||||
|
[hyperx]: https://github.com/substack/hyperx
|
||||||
|
[budo]: https://github.com/mattdesl/budo
|
||||||
|
[es2020]: https://github.com/yoshuawuyts/es2020
|
||||||
|
[browserify]: https://github.com/substack/browserify
|
||||||
|
[localstorage]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
|
||||||
|
[handbook]: https://github.com/yoshuawuyts/choo-handbook
|
||||||
|
|||||||
+20
-1
@@ -2,7 +2,26 @@ const choo = require('../../')
|
|||||||
|
|
||||||
const mainView = require('./views/main')
|
const mainView = require('./views/main')
|
||||||
|
|
||||||
const app = choo()
|
const app = choo({
|
||||||
|
onError: function (err, state, createSend) {
|
||||||
|
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) {
|
||||||
|
console.groupCollapsed(`Action: ${caller} -> ${name}`)
|
||||||
|
console.log(data)
|
||||||
|
console.groupEnd()
|
||||||
|
},
|
||||||
|
onStateChange: function (data, state, prev, createSend) {
|
||||||
|
console.groupCollapsed('State')
|
||||||
|
console.log(prev)
|
||||||
|
console.log(state)
|
||||||
|
console.groupEnd()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
app.model(require('./models/error'))
|
app.model(require('./models/error'))
|
||||||
app.model(require('./models/api'))
|
app.model(require('./models/api'))
|
||||||
|
|||||||
+10
-10
@@ -6,26 +6,26 @@ module.exports = {
|
|||||||
title: 'Button pushing machine 3000'
|
title: 'Button pushing machine 3000'
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
set: (action, state) => ({ 'title': action.payload })
|
set: (data, state) => ({ 'title': data })
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
good: (action, state, send) => request('/good', send),
|
good: function (data, state, send, done) {
|
||||||
bad: (action, state, send) => request('/bad', send)
|
request('/good', send, done)
|
||||||
|
},
|
||||||
|
bad: (data, state, send, done) => request('/bad', send, done)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function request (uri, send, state) {
|
function request (uri, send, done) {
|
||||||
http(uri, { json: true }, function (err, res, body) {
|
http(uri, { json: true }, function (err, res, body) {
|
||||||
if (err) return send('app:error', { payload: 'HTTP error' })
|
if (err) return done(new Error('HTTP error'))
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
const message = (body && body.message)
|
const message = (body && body.message)
|
||||||
? body.message
|
? body.message
|
||||||
: 'unknown server error'
|
: 'unknown server error'
|
||||||
return send('app:error', { payload: message })
|
return done(new Error(message))
|
||||||
}
|
}
|
||||||
if (!body) {
|
if (!body) return done(new Error('fatal: no body received'))
|
||||||
return send('app:error', { payload: 'fatal: no body received' })
|
send('api:set', body.message || body.title, done)
|
||||||
}
|
|
||||||
send('api:set', { payload: body.message || body.title })
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,34 +10,43 @@ const ERROR_TIMEOUT = 1000
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
namespace: 'app',
|
namespace: 'app',
|
||||||
state: {
|
state: {
|
||||||
error: [],
|
errors: [],
|
||||||
errorTimeDone: null,
|
errorTimeDone: 0,
|
||||||
triggerTime: null
|
triggerTime: null
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
error: function (action, state) {
|
setError: function (data, state) {
|
||||||
const now = Date.now()
|
|
||||||
const timeDone = state.errorTimeDone
|
|
||||||
const newTimestamp = (timeDone && timeDone >= now)
|
|
||||||
? timeDone + ERROR_TIMEOUT
|
|
||||||
: now + ERROR_TIMEOUT
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error: state.error.concat(action.payload),
|
errors: state.errors.concat(data.message),
|
||||||
errorTimeDone: newTimestamp
|
errorTimeDone: data.errorTimeDone
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'error:delete': function (action, state) {
|
'delError': function (data, state) {
|
||||||
state.error.shift()
|
state.errors.shift()
|
||||||
return { error: state.error }
|
return { errors: state.errors }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
error: function (action, state, send) {
|
error: function (err, state, send, done) {
|
||||||
const timeout = state.errorTimeDone - Date.now()
|
const timeDone = state.errorTimeDone
|
||||||
setTimeout(function () {
|
const now = Date.now()
|
||||||
send('app:error:delete')
|
|
||||||
}, timeout)
|
const timeStamp = (timeDone && timeDone >= now)
|
||||||
|
? timeDone + ERROR_TIMEOUT
|
||||||
|
: now + ERROR_TIMEOUT
|
||||||
|
|
||||||
|
const timeout = timeStamp - now
|
||||||
|
|
||||||
|
const errAction = {
|
||||||
|
message: err.message,
|
||||||
|
errorTimeDone: timeStamp
|
||||||
|
}
|
||||||
|
send('app:setError', errAction, function (err) {
|
||||||
|
if (err) return done(err)
|
||||||
|
setTimeout(function () {
|
||||||
|
send('app:delError', done)
|
||||||
|
}, timeout)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (state, prev, send) {
|
||||||
const error = state.app.error[0]
|
const error = state.app.errors[0]
|
||||||
const title = state.api.title
|
const title = state.api.title
|
||||||
return choo.view`
|
return html`
|
||||||
<section>
|
<section>
|
||||||
<h1>${title}</h1>
|
<h1>${title}</h1>
|
||||||
<h2>Latest error: ${error}</h2>
|
<h2>Latest error: ${error}</h2>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
const dateformat = require('dateformat')
|
const dateformat = require('dateformat')
|
||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (state, prev, send) {
|
||||||
|
const params = state.params
|
||||||
const mailbox = params.mailbox
|
const mailbox = params.mailbox
|
||||||
const messages = state[mailbox].messages
|
const messages = state[mailbox].messages
|
||||||
return choo.view`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<div class="db cf w-100">
|
<div class="db cf w-100">
|
||||||
<div class="fl mb3 w-25 mt0 b">Date</th>
|
<div class="fl mb3 w-25 mt0 b">Date</th>
|
||||||
@@ -20,7 +21,7 @@ module.exports = function (params, state, send) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createMessage (message, mailbox) {
|
function createMessage (message, mailbox) {
|
||||||
return choo.view`
|
return html`
|
||||||
<div class="db cf w-100">
|
<div class="db cf w-100">
|
||||||
<a href="${'/' + mailbox + '/' + message.id}">
|
<a href="${'/' + mailbox + '/' + message.id}">
|
||||||
<div class="fl mb3 w-25 f6 link">
|
<div class="fl mb3 w-25 f6 link">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (state, prev, send) {
|
||||||
|
const params = state.params
|
||||||
const mailbox = params.mailbox
|
const mailbox = params.mailbox
|
||||||
const message = params.message
|
const message = params.message
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ module.exports = function (params, state, send) {
|
|||||||
return String(msg.id) === message
|
return String(msg.id) === message
|
||||||
})[0]
|
})[0]
|
||||||
|
|
||||||
return choo.view`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
${email ? createEmail(email) : 'error: no email found'}
|
${email ? createEmail(email) : 'error: no email found'}
|
||||||
</div
|
</div
|
||||||
@@ -16,7 +17,7 @@ module.exports = function (params, state, send) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createEmail (message) {
|
function createEmail (message) {
|
||||||
return choo.view`
|
return html`
|
||||||
<div class="mail">
|
<div class="mail">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>From</dt>
|
<dt>From</dt>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (state, prev, send) {
|
||||||
return choo.view`
|
return html`
|
||||||
<section>
|
<section>
|
||||||
<p>Select a mailbox</p>
|
<p>Select a mailbox</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
const dateformat = require('dateformat')
|
const dateformat = require('dateformat')
|
||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
module.exports = function () {
|
module.exports = function () {
|
||||||
return function (params, state, send) {
|
return function (state, prev, send) {
|
||||||
|
const params = state.params
|
||||||
const mailbox = params.mailbox
|
const mailbox = params.mailbox
|
||||||
const message = params.message
|
const message = params.message
|
||||||
const messages = state[mailbox].messages
|
const messages = state[mailbox].messages
|
||||||
@@ -12,7 +13,7 @@ module.exports = function () {
|
|||||||
return String(msg.id) === message
|
return String(msg.id) === message
|
||||||
})[0]
|
})[0]
|
||||||
|
|
||||||
return choo.view`
|
return html`
|
||||||
<section class="fl mt4 w-80 db">
|
<section class="fl mt4 w-80 db">
|
||||||
<div>
|
<div>
|
||||||
${createHeader()}
|
${createHeader()}
|
||||||
@@ -26,7 +27,7 @@ module.exports = function () {
|
|||||||
</section>
|
</section>
|
||||||
`
|
`
|
||||||
} else {
|
} else {
|
||||||
return choo.view`
|
return html`
|
||||||
<section class="fl mt4 w-80 db">
|
<section class="fl mt4 w-80 db">
|
||||||
${createHeader()}
|
${createHeader()}
|
||||||
${messages.map(function (msg) {
|
${messages.map(function (msg) {
|
||||||
@@ -39,7 +40,7 @@ module.exports = function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createHeader () {
|
function createHeader () {
|
||||||
return choo.view`
|
return html`
|
||||||
<div class="db cf w-100">
|
<div class="db cf w-100">
|
||||||
<div class="fl mb3 w-25 mt0 b">Date</th>
|
<div class="fl mb3 w-25 mt0 b">Date</th>
|
||||||
<div class="fl mb3 w-25 mt0 b">Subject</th>
|
<div class="fl mb3 w-25 mt0 b">Subject</th>
|
||||||
@@ -50,7 +51,7 @@ function createHeader () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createMessage (message, mailbox) {
|
function createMessage (message, mailbox) {
|
||||||
return choo.view`
|
return html`
|
||||||
<div class="db cf w-100">
|
<div class="db cf w-100">
|
||||||
<a href="${'/' + mailbox + '/' + message.id}">
|
<a href="${'/' + mailbox + '/' + message.id}">
|
||||||
<div class="fl mb3 w-25 f6 link">
|
<div class="fl mb3 w-25 f6 link">
|
||||||
@@ -65,7 +66,7 @@ function createMessage (message, mailbox) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createEmail (message) {
|
function createEmail (message) {
|
||||||
return choo.view`
|
return html`
|
||||||
<div class="mail">
|
<div class="mail">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>From</dt>
|
<dt>From</dt>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
const mailboxes = [ 'inbox', 'spam', 'sent' ]
|
const mailboxes = [ 'inbox', 'spam', 'sent' ]
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (state, prev, send) {
|
||||||
return choo.view`
|
return html`
|
||||||
<aside class="fl mt4 w-20 db">
|
<aside class="fl mt4 w-20 db">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@@ -19,7 +19,7 @@ module.exports = function (params, state, send) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createLi (mailbox, messages) {
|
function createLi (mailbox, messages) {
|
||||||
return choo.view`
|
return html`
|
||||||
<li class="mt4 f6">
|
<li class="mt4 f6">
|
||||||
<a href="/${mailbox}">
|
<a href="/${mailbox}">
|
||||||
${mailbox.charAt(0).toUpperCase() + mailbox.slice(1)}
|
${mailbox.charAt(0).toUpperCase() + mailbox.slice(1)}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const pathname = require('pathname-match')
|
const pathname = require('pathname-match')
|
||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (state, prev, send) {
|
||||||
const location = state.app.location
|
const location = state.location.pathname
|
||||||
return choo.view`
|
return html`
|
||||||
<span class="fl mt4 w-100 f4 b">
|
<span class="fl mt4 w-100 f4 b">
|
||||||
URL: ${pathname(location) || '/'}
|
URL: ${pathname(location) || '/'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -6,16 +6,18 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "NODE_ENV=development node server.js"
|
"start": "NODE_ENV=development node server.js"
|
||||||
},
|
},
|
||||||
"browserify": {
|
|
||||||
"transform": [
|
|
||||||
"sheetify/transform"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"author": "Yoshua Wuyts <i@yoshuawuyts.com>",
|
"author": "Yoshua Wuyts <i@yoshuawuyts.com>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"css-wipe": "^4.2.1",
|
"css-wipe": "^4.2.1",
|
||||||
"dateformat": "^1.0.12",
|
"dateformat": "^1.0.12",
|
||||||
"tachyons": "^4.0.0-beta.33"
|
"tachyons": "^4.0.0-beta.33"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"bankai": "^2.0.5",
|
||||||
|
"browserify": "^13.0.1",
|
||||||
|
"insert-css": "^0.2.0",
|
||||||
|
"server-router": "^2.1.0",
|
||||||
|
"sheetify": "^5.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
const emailList = require('../elements/email-list')
|
const emailList = require('../elements/email-list')
|
||||||
const pathname = require('../elements/pathname')
|
const pathname = require('../elements/pathname')
|
||||||
const email = require('../elements/email')
|
const email = require('../elements/email')
|
||||||
const nav = require('../elements/nav')
|
const nav = require('../elements/nav')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (state, prev, send) {
|
||||||
return choo.view`
|
return html`
|
||||||
<main class="mw5 mw7-ns center cf">
|
<main class="mw5 mw7-ns center cf">
|
||||||
${pathname(params, state, send)}
|
${pathname(state, prev, send)}
|
||||||
${nav(params, state, send)}
|
${nav(state, prev, send)}
|
||||||
<section class="fl mt4 w-80 db">
|
<section class="fl mt4 w-80 db">
|
||||||
${emailList(params, state, send)}
|
${emailList(state, prev, send)}
|
||||||
${email(params, state, send)}
|
${email(state, prev, send)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
const empty = require('../elements/empty-mailbox')
|
const empty = require('../elements/empty-mailbox')
|
||||||
const pathname = require('../elements/pathname')
|
const pathname = require('../elements/pathname')
|
||||||
const nav = require('../elements/nav')
|
const nav = require('../elements/nav')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (state, prev, send) {
|
||||||
return choo.view`
|
return html`
|
||||||
<main class="mw5 mw7-ns center cf">
|
<main class="mw5 mw7-ns center cf">
|
||||||
${pathname(params, state, send)}
|
${pathname(state, prev, send)}
|
||||||
${nav(params, state, send)}
|
${nav(state, prev, send)}
|
||||||
${empty(params, state, send)}
|
${empty(state, prev, send)}
|
||||||
</main>
|
</main>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
const emailList = require('../elements/email-list')
|
const emailList = require('../elements/email-list')
|
||||||
const pathname = require('../elements/pathname')
|
const pathname = require('../elements/pathname')
|
||||||
const nav = require('../elements/nav')
|
const nav = require('../elements/nav')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (state, prev, send) {
|
||||||
return choo.view`
|
return html`
|
||||||
<main class="mw5 mw7-ns center cf">
|
<main class="mw5 mw7-ns center cf">
|
||||||
${pathname(params, state, send)}
|
${pathname(state, prev, send)}
|
||||||
${nav(params, state, send)}
|
${nav(state, prev, send)}
|
||||||
<section class="fl mt4 w-80 db">
|
<section class="fl mt4 w-80 db">
|
||||||
${emailList(params, state, send)}
|
${emailList(state, prev, send)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (state, prev, send) {
|
||||||
const serverMessage = state.message.server
|
const serverMessage = state.message.server
|
||||||
const clientMessage = state.message.client
|
const clientMessage = state.message.client
|
||||||
|
|
||||||
assert.equal(typeof serverMessage, 'string', 'server should be a string')
|
assert.equal(typeof serverMessage, 'string', 'server should be a string')
|
||||||
assert.equal(typeof clientMessage, 'string', 'client should be a string')
|
assert.equal(typeof clientMessage, 'string', 'client should be a string')
|
||||||
|
|
||||||
return choo.view`
|
return html`
|
||||||
<section id="app-root">
|
<section id="app-root">
|
||||||
<h1>server message: ${serverMessage}</h1>
|
<h1>server message: ${serverMessage}</h1>
|
||||||
<h1>client message: ${clientMessage}</h1>
|
<h1>client message: ${clientMessage}</h1>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const choo = require('../../')
|
const choo = require('../../')
|
||||||
|
const html = require('../../html')
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.model(createModel())
|
app.model(createModel())
|
||||||
@@ -10,7 +11,7 @@ const tree = app.start()
|
|||||||
document.body.appendChild(tree)
|
document.body.appendChild(tree)
|
||||||
|
|
||||||
function mainView (params, state, send) {
|
function mainView (params, state, send) {
|
||||||
return choo.view`
|
return html`
|
||||||
<div>${state.logger.msg}</div>
|
<div>${state.logger.msg}</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
@@ -34,13 +35,13 @@ function createModel () {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
reducers: {
|
reducers: {
|
||||||
'print': (action, state) => {
|
'print': (data, state) => {
|
||||||
return ({ msg: state.msg + ' ' + action.payload })
|
return ({ msg: state.msg + ' ' + data.payload })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
close: () => stream.close(),
|
close: () => stream.close(),
|
||||||
error: (action, state) => console.error(`error: ${action.payload}`)
|
error: (data, state) => console.error(`error: ${data.payload}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const choo = require('../../../')
|
const html = require('../../../html')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (params, state, send) {
|
||||||
const error = state.app.error[0]
|
const error = state.app.error[0]
|
||||||
const title = state.api.title
|
const title = state.api.title
|
||||||
return choo.view`
|
return html`
|
||||||
<section>
|
<section>
|
||||||
<h1>${title}</h1>
|
<h1>${title}</h1>
|
||||||
<h2>Latest error: ${error}</h2>
|
<h2>Latest error: ${error}</h2>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const choo = require('../../')
|
const choo = require('../../')
|
||||||
|
const html = require('../../html')
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.model({
|
app.model({
|
||||||
@@ -7,15 +8,15 @@ app.model({
|
|||||||
title: 'my demo app'
|
title: 'my demo app'
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
update: (action, state) => ({ title: action.payload })
|
update: (data, state) => ({ title: data.payload })
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
update: (action, state, send) => (document.title = action.payload)
|
update: (data, state, send) => (document.title = data.payload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const mainView = (params, state, send) => {
|
const mainView = (state, prev, send) => {
|
||||||
return choo.view`
|
return html`
|
||||||
<main class="app">
|
<main class="app">
|
||||||
<h1>${state.input.title}</h1>
|
<h1>${state.input.title}</h1>
|
||||||
<label>Set the title</label>
|
<label>Set the title</label>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "client.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "budo client.js -p 8080"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Yoshua Wuyts <i@yoshuawuyts.com>",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"budo": "^8.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,27 @@
|
|||||||
const history = require('sheet-router/history')
|
const history = require('sheet-router/history')
|
||||||
const sheetRouter = require('sheet-router')
|
const sheetRouter = require('sheet-router')
|
||||||
const document = require('global/document')
|
const document = require('global/document')
|
||||||
|
const onReady = require('document-ready')
|
||||||
const href = require('sheet-router/href')
|
const href = require('sheet-router/href')
|
||||||
const hash = require('sheet-router/hash')
|
const hash = require('sheet-router/hash')
|
||||||
const hashMatch = require('hash-match')
|
const hashMatch = require('hash-match')
|
||||||
const sendAction = require('send-action')
|
const barracks = require('barracks')
|
||||||
const mutate = require('xtend/mutable')
|
|
||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const xtend = require('xtend')
|
const xtend = require('xtend')
|
||||||
const yo = require('yo-yo')
|
const yo = require('yo-yo')
|
||||||
|
|
||||||
choo.view = yo
|
|
||||||
module.exports = choo
|
module.exports = choo
|
||||||
|
|
||||||
// framework for creating sturdy web applications
|
// framework for creating sturdy web applications
|
||||||
// null -> fn
|
// null -> fn
|
||||||
function choo () {
|
function choo (opts) {
|
||||||
const _models = []
|
opts = opts || {}
|
||||||
var _router = null
|
|
||||||
|
const _store = start._store = barracks(xtend(opts, { onStateChange: render }))
|
||||||
|
var _router = start._router = null
|
||||||
|
var _defaultRoute = null
|
||||||
|
var _rootNode = null
|
||||||
|
var _routes = null
|
||||||
|
|
||||||
start.toString = toString
|
start.toString = toString
|
||||||
start.router = router
|
start.router = router
|
||||||
@@ -29,227 +33,140 @@ function choo () {
|
|||||||
// render the application to a string
|
// render the application to a string
|
||||||
// (str, obj) -> str
|
// (str, obj) -> str
|
||||||
function toString (route, serverState) {
|
function toString (route, serverState) {
|
||||||
const initialState = {}
|
serverState = serverState || {}
|
||||||
const nsState = {}
|
assert.equal(typeof route, 'string', 'choo.app.toString: route must be a string')
|
||||||
|
assert.equal(typeof serverState, 'object', 'choo.app.toString: serverState must be an object')
|
||||||
|
_store.start({ subscriptions: false, reducers: false, effects: false })
|
||||||
|
|
||||||
_models.forEach(function (model) {
|
const state = _store.state({ state: serverState })
|
||||||
const ns = model.namespace
|
const router = createRouter(_defaultRoute, _routes, createSend)
|
||||||
if (ns) {
|
const tree = router(route, state)
|
||||||
if (!nsState[ns]) nsState[ns] = {}
|
return tree.outerHTML || tree.toString()
|
||||||
apply(ns, model.state, nsState)
|
|
||||||
nsState[ns] = xtend(nsState[ns], serverState[ns])
|
function createSend () {
|
||||||
} else {
|
return function send () {
|
||||||
apply(model.namespace, model.state, initialState)
|
assert.fail('choo: send() cannot be called from Node')
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
const state = xtend(initialState, xtend(serverState, nsState))
|
|
||||||
const tree = _router(route, state, function () {
|
|
||||||
throw new Error('send() cannot be called on the server')
|
|
||||||
})
|
|
||||||
|
|
||||||
return tree.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the application
|
// start the application
|
||||||
// (str?, obj?) -> DOMNode
|
// (str?, obj?) -> DOMNode
|
||||||
function start (rootId, opts) {
|
function start (selector, startOpts) {
|
||||||
if (!opts && typeof rootId !== 'string') {
|
if (!startOpts && typeof selector !== 'string') {
|
||||||
opts = rootId
|
startOpts = selector
|
||||||
rootId = null
|
selector = null
|
||||||
}
|
}
|
||||||
opts = opts || {}
|
startOpts = startOpts || {}
|
||||||
const name = opts.name || 'choo'
|
|
||||||
const initialState = {}
|
|
||||||
const reducers = {}
|
|
||||||
const effects = {}
|
|
||||||
|
|
||||||
_models.push(appInit(opts))
|
_store.model(appInit(startOpts))
|
||||||
_models.forEach(function (model) {
|
const createSend = _store.start(startOpts)
|
||||||
if (model.state) apply(model.namespace, model.state, initialState)
|
_router = start._router = createRouter(_defaultRoute, _routes, createSend)
|
||||||
if (model.reducers) apply(model.namespace, model.reducers, reducers)
|
const state = _store.state({state: {}})
|
||||||
if (model.effects) apply(model.namespace, model.effects, effects)
|
|
||||||
})
|
|
||||||
|
|
||||||
// send() is used to trigger actions inside
|
if (!selector) {
|
||||||
// views, effects and subscriptions
|
const tree = _router(state.location.pathname, state)
|
||||||
const send = sendAction({
|
_rootNode = tree
|
||||||
onaction: handleAction,
|
|
||||||
onchange: onchange,
|
|
||||||
state: initialState
|
|
||||||
})
|
|
||||||
|
|
||||||
// subscriptions are loaded after sendAction() is called
|
|
||||||
// because they both need access to send() and can't
|
|
||||||
// react to actions (read-only) - also wait on DOM to
|
|
||||||
// be loaded
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
_models.forEach(function (model) {
|
|
||||||
if (model.subscriptions) {
|
|
||||||
assert.ok(Array.isArray(model.subscriptions), 'subs must be an arr')
|
|
||||||
model.subscriptions.forEach(function (sub) {
|
|
||||||
sub(send)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// If an id is provided, the application will rehydrate
|
|
||||||
// on the node. If no id is provided it will return
|
|
||||||
// a tree that's ready to be appended to the DOM.
|
|
||||||
//
|
|
||||||
// The rootId is determined to find the application root
|
|
||||||
// on update. Since the DOM nodes change between updates,
|
|
||||||
// we must call document.querySelector() to find the root.
|
|
||||||
// Use different names when loading multiple choo applications
|
|
||||||
// on the same page
|
|
||||||
if (rootId) {
|
|
||||||
document.addEventListener('DOMContentLoaded', function (event) {
|
|
||||||
rootId = rootId.replace(/^#/, '')
|
|
||||||
|
|
||||||
const oldTree = document.querySelector('#' + rootId)
|
|
||||||
assert.ok(oldTree, 'could not find node #' + rootId)
|
|
||||||
|
|
||||||
const newTree = _router(send.state().app.location, send.state(), send)
|
|
||||||
|
|
||||||
yo.update(oldTree, newTree)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
rootId = name + '-root'
|
|
||||||
const tree = _router(send.state().app.location, send.state(), send)
|
|
||||||
tree.setAttribute('id', rootId)
|
|
||||||
return tree
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the DOM after every state mutation
|
||||||
|
// (obj, obj, obj, str, fn) -> null
|
||||||
|
function render (data, state, prev, name, createSend) {
|
||||||
|
if (opts.onStateChange) {
|
||||||
|
opts.onStateChange(data, state, prev, name, createSend)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle an action by either reducers, effects
|
const newTree = _router(state.location.pathname, state, prev)
|
||||||
// or both - return the new state when done
|
_rootNode = yo.update(_rootNode, newTree)
|
||||||
// (obj, obj, fn) -> obj
|
|
||||||
function handleAction (action, state, send) {
|
|
||||||
var reducersCalled = false
|
|
||||||
var effectsCalled = false
|
|
||||||
const newState = xtend(state)
|
|
||||||
|
|
||||||
// validate if a namespace exists. Namespaces
|
|
||||||
// are delimited by the first ':'. Perhaps
|
|
||||||
// we'll allow recursive namespaces in the
|
|
||||||
// future - who knows
|
|
||||||
if (/:/.test(action.type)) {
|
|
||||||
const arr = action.type.split(':')
|
|
||||||
var ns = arr.shift()
|
|
||||||
action.type = arr.join(':')
|
|
||||||
}
|
|
||||||
|
|
||||||
const _reducers = ns ? reducers[ns] : reducers
|
|
||||||
if (_reducers && _reducers[action.type]) {
|
|
||||||
if (ns) {
|
|
||||||
const reducedState = _reducers[action.type](action, state[ns])
|
|
||||||
if (!newState[ns]) newState[ns] = {}
|
|
||||||
mutate(newState[ns], xtend(state[ns], reducedState))
|
|
||||||
} else {
|
|
||||||
mutate(newState, reducers[action.type](action, state))
|
|
||||||
}
|
|
||||||
reducersCalled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const _effects = ns ? effects[ns] : effects
|
|
||||||
if (_effects && _effects[action.type]) {
|
|
||||||
if (ns) _effects[action.type](action, state[ns], send)
|
|
||||||
else _effects[action.type](action, state, send)
|
|
||||||
effectsCalled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reducersCalled && !effectsCalled) {
|
|
||||||
throw new Error('Could not find action ' + action.type)
|
|
||||||
}
|
|
||||||
|
|
||||||
// allows (newState === oldState) checks
|
|
||||||
return (reducersCalled) ? newState : state
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the DOM after every state mutation
|
|
||||||
// (obj, obj) -> null
|
|
||||||
function onchange (action, newState, oldState) {
|
|
||||||
if (newState === oldState) return
|
|
||||||
const oldTree = document.querySelector('#' + rootId)
|
|
||||||
assert.ok(oldTree, "Could not find DOM node '#" + rootId + "' to update")
|
|
||||||
const newTree = _router(newState.app.location, newState, send, oldState)
|
|
||||||
newTree.setAttribute('id', rootId)
|
|
||||||
yo.update(oldTree, newTree)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// register all routes on the router
|
// register all routes on the router
|
||||||
// (str?, [fn|[fn]]) -> obj
|
// (str?, [fn|[fn]]) -> obj
|
||||||
function router (defaultRoute, cb) {
|
function router (defaultRoute, routes) {
|
||||||
_router = sheetRouter(defaultRoute, cb)
|
_defaultRoute = defaultRoute
|
||||||
return _router
|
_routes = routes
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a new model
|
// create a new model
|
||||||
// (str?, obj) -> null
|
// (str?, obj) -> null
|
||||||
function model (model) {
|
function model (model) {
|
||||||
_models.push(model)
|
_store.model(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new router with a custom `createRoute()` function
|
||||||
|
// (str?, obj, fn?) -> null
|
||||||
|
function createRouter (defaultRoute, routes, createSend) {
|
||||||
|
var prev = {}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrap (child, route) {
|
||||||
|
const send = createSend(route, true)
|
||||||
|
return function chooWrap (params, state) {
|
||||||
|
const nwPrev = prev
|
||||||
|
const nwState = prev = xtend(state, { params: params })
|
||||||
|
if (opts.freeze !== false) Object.freeze(nwState)
|
||||||
|
return child(nwState, nwPrev, send)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// initial application state model
|
// initial application state model
|
||||||
// obj -> obj
|
// obj -> obj
|
||||||
function appInit (opts) {
|
function appInit (opts) {
|
||||||
const initialLocation = (opts.hash === true)
|
const loc = document.location
|
||||||
? hashMatch(document.location.hash)
|
const state = { pathname: (opts.hash) ? hashMatch(loc.hash) : loc.href }
|
||||||
: document.location.href
|
const reducers = {
|
||||||
|
setLocation: function setLocation (data, state) {
|
||||||
const model = {
|
return { pathname: data.location.replace(/#.*/, '') }
|
||||||
namespace: 'app',
|
|
||||||
state: { location: initialLocation },
|
|
||||||
subscriptions: [],
|
|
||||||
reducers: {
|
|
||||||
// handle href links
|
|
||||||
location: function setLocation (action, state) {
|
|
||||||
return {
|
|
||||||
location: action.location.replace(/#.*/, '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if hash routing explicitly enabled, subscribe to it
|
// if hash routing explicitly enabled, subscribe to it
|
||||||
|
const subs = {}
|
||||||
if (opts.hash === true) {
|
if (opts.hash === true) {
|
||||||
pushLocationSub(function (navigate) {
|
pushLocationSub(function (navigate) {
|
||||||
hash(function (fragment) {
|
hash(function (fragment) {
|
||||||
navigate(hashMatch(fragment))
|
navigate(hashMatch(fragment))
|
||||||
})
|
})
|
||||||
})
|
}, 'handleHash', subs)
|
||||||
// otherwise, subscribe to HTML5 history API
|
|
||||||
} else {
|
} else {
|
||||||
if (opts.history !== false) pushLocationSub(history)
|
if (opts.history !== false) pushLocationSub(history, 'handleHistory', subs)
|
||||||
// enable catching <a href=""></a> links
|
if (opts.href !== false) pushLocationSub(href, 'handleHref', subs)
|
||||||
if (opts.href !== false) pushLocationSub(href)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return model
|
return {
|
||||||
|
namespace: 'location',
|
||||||
|
subscriptions: subs,
|
||||||
|
reducers: reducers,
|
||||||
|
state: state
|
||||||
|
}
|
||||||
|
|
||||||
// create a new subscription that modifies
|
// create a new subscription that modifies
|
||||||
// 'app:location' and push it to be loaded
|
// 'app:location' and push it to be loaded
|
||||||
// fn -> null
|
// (fn, obj) -> null
|
||||||
function pushLocationSub (cb) {
|
function pushLocationSub (cb, key, model) {
|
||||||
model.subscriptions.push(function (send) {
|
model[key] = function (send, done) {
|
||||||
cb(function (href) {
|
cb(function navigate (pathname) {
|
||||||
send('app:location', { location: href })
|
send('location:setLocation', { location: pathname }, done)
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compose an object conditionally
|
|
||||||
// optionally contains a namespace
|
|
||||||
// which is used to nest properties.
|
|
||||||
// (str, obj, obj) -> null
|
|
||||||
function apply (ns, source, target) {
|
|
||||||
Object.keys(source).forEach(function (key) {
|
|
||||||
if (ns) {
|
|
||||||
if (!target[ns]) target[ns] = {}
|
|
||||||
target[ns][key] = source[key]
|
|
||||||
} else target[key] = source[key]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
+15
-5
@@ -5,13 +5,13 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deps": "dependency-check . && dependency-check . --extra --no-dev -i xhr",
|
"deps": "dependency-check . && dependency-check . --extra --no-dev -i xhr",
|
||||||
|
"test:electron": "browserify tests/**/*.js -t es2020 -p proxyquire-universal | tape-run",
|
||||||
|
"test:cov": "browserify tests/**/*.js -t es2020 -p proxyquire-universal -p tape-istanbul/plugin | tape-run | tape-istanbul && istanbul report",
|
||||||
"test:server": "standard && npm run deps && NODE_ENV=test node tests/server/*",
|
"test:server": "standard && npm run deps && NODE_ENV=test node tests/server/*",
|
||||||
"test:server:cov": "standard && npm run deps && NODE_ENV=test istanbul cover tests/server/*",
|
|
||||||
"test:browser": "standard && npm run deps && NODE_ENV=test zuul tests/browser/*",
|
"test:browser": "standard && npm run deps && NODE_ENV=test zuul tests/browser/*",
|
||||||
"test:browser:local": "standard && npm run deps && NODE_ENV=test zuul --local 8080 -- tests/browser/*",
|
"test:browser:local": "standard && npm run deps && NODE_ENV=test zuul --local 8080 -- tests/browser/*",
|
||||||
"test:cov": "npm run test:server:cov",
|
|
||||||
"preversion": "if [ ! -z $SKIP_TEST ]; then npm run test:browser; fi",
|
"preversion": "if [ ! -z $SKIP_TEST ]; then npm run test:browser; fi",
|
||||||
"test": "npm run test:server"
|
"test": "npm run test:electron"
|
||||||
},
|
},
|
||||||
"repository": "yoshuawuyts/choo",
|
"repository": "yoshuawuyts/choo",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -24,29 +24,39 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"barracks": "^8.0.0",
|
||||||
|
"document-ready": "~1.0.2",
|
||||||
"global": "^4.3.0",
|
"global": "^4.3.0",
|
||||||
"hash-match": "^1.0.2",
|
"hash-match": "^1.0.2",
|
||||||
"send-action": "^2.0.2",
|
|
||||||
"sheet-router": "^3.1.0",
|
"sheet-router": "^3.1.0",
|
||||||
"xhr": "^2.2.0",
|
"xhr": "^2.2.0",
|
||||||
"xtend": "^4.0.1",
|
"xtend": "^4.0.1",
|
||||||
"yo-yo": "^1.2.0"
|
"yo-yo": "^1.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"append-child": "~1.0.0",
|
||||||
"bankai": "^2.0.2",
|
"bankai": "^2.0.2",
|
||||||
"browserify": "^13.0.1",
|
"browserify": "^13.0.1",
|
||||||
"browserify-istanbul": "^2.0.0",
|
"browserify-istanbul": "^2.0.0",
|
||||||
"bundle-collapser": "^1.2.1",
|
"bundle-collapser": "^1.2.1",
|
||||||
"dependency-check": "^2.5.1",
|
"dependency-check": "^2.5.1",
|
||||||
"es2020": "^1.0.1",
|
"es2020": "^1.0.1",
|
||||||
|
"geval": "~2.1.1",
|
||||||
"insert-css": "^0.2.0",
|
"insert-css": "^0.2.0",
|
||||||
"istanbul": "^0.4.4",
|
"istanbul": "^0.4.4",
|
||||||
"karma-sauce-launcher": "^1.0.0",
|
"karma-sauce-launcher": "^1.0.0",
|
||||||
|
"min-document": "~2.18.0",
|
||||||
|
"proxyquire": "~1.7.10",
|
||||||
|
"proxyquire-universal": "~1.0.8",
|
||||||
|
"proxyquireify": "~3.2.0",
|
||||||
"server-router": "^2.1.0",
|
"server-router": "^2.1.0",
|
||||||
"sheetify": "^5.0.0",
|
"sheetify": "^5.0.0",
|
||||||
"standard": "^7.1.0",
|
"standard": "^7.1.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-run": "~2.1.4",
|
||||||
|
"yo-yoify": "^3.1.0",
|
||||||
"zuul": "toddself/zuul"
|
"zuul": "toddself/zuul"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ USAGE
|
|||||||
gzip_size () {
|
gzip_size () {
|
||||||
browserify index.js \
|
browserify index.js \
|
||||||
-g unassertify \
|
-g unassertify \
|
||||||
|
-g yo-yoify \
|
||||||
-g es2020 \
|
-g es2020 \
|
||||||
-g uglifyify \
|
-g uglifyify \
|
||||||
-p bundle-collapser/plugin \
|
-p bundle-collapser/plugin \
|
||||||
@@ -24,6 +25,7 @@ gzip_size () {
|
|||||||
min_size () {
|
min_size () {
|
||||||
browserify index.js \
|
browserify index.js \
|
||||||
-g unassertify \
|
-g unassertify \
|
||||||
|
-g yo-yoify \
|
||||||
-g es2020 \
|
-g es2020 \
|
||||||
-g uglifyify \
|
-g uglifyify \
|
||||||
-p bundle-collapser/plugin \
|
-p bundle-collapser/plugin \
|
||||||
@@ -35,6 +37,7 @@ min_size () {
|
|||||||
run_discify () {
|
run_discify () {
|
||||||
browserify index.js --full-paths \
|
browserify index.js --full-paths \
|
||||||
-g unassertify \
|
-g unassertify \
|
||||||
|
-g yo-yoify \
|
||||||
-g es2020 \
|
-g es2020 \
|
||||||
-g uglifyify \
|
-g uglifyify \
|
||||||
| uglifyjs \
|
| uglifyjs \
|
||||||
|
|||||||
+52
-52
@@ -1,60 +1,60 @@
|
|||||||
const tape = require('tape')
|
const test = require('tape')
|
||||||
|
const append = require('append-child')
|
||||||
const choo = require('../../')
|
const choo = require('../../')
|
||||||
|
const view = require('../../html')
|
||||||
|
|
||||||
tape('should render on the client', function (t) {
|
test('state is immutable', function (t) {
|
||||||
t.test('state should not be mutable', function (t) {
|
t.plan(4)
|
||||||
t.plan(4)
|
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
const state = {
|
const state = {
|
||||||
foo: 'baz',
|
foo: 'baz',
|
||||||
beep: 'boop'
|
beep: 'boop'
|
||||||
}
|
}
|
||||||
|
|
||||||
app.model({
|
app.model({
|
||||||
state: state,
|
state: state,
|
||||||
namespace: 'test',
|
namespace: 'test',
|
||||||
reducers: {
|
reducers: {
|
||||||
'no-reducer-mutate': (action, state) => {
|
'no-reducer-mutate': (action, state) => {
|
||||||
return {}
|
return {}
|
||||||
},
|
|
||||||
'mutate-on-return': (action, state) => {
|
|
||||||
delete action.type
|
|
||||||
return action
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
effects: {
|
'mutate-on-return': (action, state) => {
|
||||||
'triggers-reducers': (action, state, send) => {
|
delete action.type
|
||||||
send('test:mutate-on-return', {beep: 'barp'})
|
return action
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
effects: {
|
||||||
let loop = -1
|
'triggers-reducers': (action, state, send, done) => {
|
||||||
|
send('test:mutate-on-return', {beep: 'barp'}, done)
|
||||||
const asserts = [
|
}
|
||||||
(state) => t.deepEqual(state, {foo: 'baz', beep: 'boop'}, 'intial state'),
|
}
|
||||||
(state) => t.deepEqual(state, {foo: 'baz', beep: 'boop'}, 'no change in state'),
|
|
||||||
(state) => t.deepEqual(state, {foo: 'oof', beep: 'boop'}, 'change in state from reducer'),
|
|
||||||
(state) => t.deepEqual(state, {foo: 'oof', beep: 'barp'}, 'change in state from effect')
|
|
||||||
]
|
|
||||||
|
|
||||||
const triggers = [
|
|
||||||
(send) => send('test:no-reducer-mutate'),
|
|
||||||
(send) => send('test:mutate-on-return', {foo: 'oof'}),
|
|
||||||
(send) => send('test:triggers-reducers')
|
|
||||||
]
|
|
||||||
|
|
||||||
app.router((route) => [
|
|
||||||
route('/', function (params, state, send) {
|
|
||||||
++loop
|
|
||||||
asserts[loop] && asserts[loop](state.test)
|
|
||||||
setTimeout(() => triggers[loop] && triggers[loop](send), 5)
|
|
||||||
return choo.view`<div><span class="test">${state.foo}:${state.beep}</span></div>`
|
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
const tree = app.start()
|
|
||||||
document.body.appendChild(tree)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let loop = -1
|
||||||
|
|
||||||
|
const asserts = [
|
||||||
|
(state) => t.deepEqual(state, {foo: 'baz', beep: 'boop'}, 'intial state'),
|
||||||
|
(state) => t.deepEqual(state, {foo: 'baz', beep: 'boop'}, 'no change in state'),
|
||||||
|
(state) => t.deepEqual(state, {foo: 'oof', beep: 'boop'}, 'change in state from reducer'),
|
||||||
|
(state) => t.deepEqual(state, {foo: 'oof', beep: 'barp'}, 'change in state from effect')
|
||||||
|
]
|
||||||
|
|
||||||
|
const triggers = [
|
||||||
|
(send) => send('test:no-reducer-mutate'),
|
||||||
|
(send) => send('test:mutate-on-return', {foo: 'oof'}),
|
||||||
|
(send) => send('test:triggers-reducers')
|
||||||
|
]
|
||||||
|
|
||||||
|
app.router((route) => [
|
||||||
|
route('/', 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>`
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
const tree = app.start()
|
||||||
|
t.on('end', append(tree))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
const test = require('tape')
|
||||||
|
const choo = require('../../')
|
||||||
|
|
||||||
|
test('freeze (default)', function (t) {
|
||||||
|
t.plan(2)
|
||||||
|
const app = choo()
|
||||||
|
|
||||||
|
app.model({
|
||||||
|
state: {
|
||||||
|
foo: 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.router((route) => [
|
||||||
|
route('/', 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()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('noFreeze', function (t) {
|
||||||
|
t.plan(2)
|
||||||
|
const app = choo({freeze: false})
|
||||||
|
|
||||||
|
app.model({
|
||||||
|
state: {
|
||||||
|
foo: 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.router((route) => [
|
||||||
|
route('/', 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()
|
||||||
|
})
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const test = require('tape')
|
||||||
|
const append = require('append-child')
|
||||||
|
const choo = require('../../')
|
||||||
|
const view = require('../../html')
|
||||||
|
|
||||||
|
test('hooks', function (t) {
|
||||||
|
t.plan(9)
|
||||||
|
|
||||||
|
const app = choo({
|
||||||
|
onError: function (err) {
|
||||||
|
t.equal(err.message, 'effect error', 'onError: receives err')
|
||||||
|
},
|
||||||
|
onAction: function (action, state, name, caller, createSend) {
|
||||||
|
if (name === 'explodes') return
|
||||||
|
t.deepEqual(action, {foo: 'bar'}, 'onAction: action data')
|
||||||
|
t.equal(state.clicks, 0, 'onAction: current state: 0 clicks')
|
||||||
|
t.equal(name, 'click', 'onAction: action name')
|
||||||
|
t.equal(caller, '/', 'onAction: caller name')
|
||||||
|
t.equal(typeof createSend, 'function', 'onAction: createSend fn')
|
||||||
|
},
|
||||||
|
onStateChange: function (action, state, prev, createSend) {
|
||||||
|
t.deepEqual(action, {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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.model({
|
||||||
|
state: {
|
||||||
|
clicks: 0
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
click: (action, state) => ({clicks: state.clicks + 1})
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
explodes: (action, state, send, done) => {
|
||||||
|
setTimeout(() => done(new Error('effect error')), 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var sent = false
|
||||||
|
app.router((route) => [
|
||||||
|
route('/', 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))
|
||||||
|
})
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const test = require('tape')
|
||||||
|
const onReady = require('document-ready')
|
||||||
|
const append = require('append-child')
|
||||||
|
const choo = require('../../')
|
||||||
|
const view = 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>`
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
var node = document.createElement('div')
|
||||||
|
node.innerHTML = app.toString('/')
|
||||||
|
node = node.childNodes[0]
|
||||||
|
t.on('end', append(node))
|
||||||
|
|
||||||
|
app.start('#app-root')
|
||||||
|
|
||||||
|
onReady(function () {
|
||||||
|
t.equal(node.innerHTML, 'Hello world!', 'same content')
|
||||||
|
t.equal(typeof node.onclick, 'function', 'attaches dom listeners')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
const test = require('tape')
|
||||||
|
const Event = require('geval/event')
|
||||||
|
const proxyquire = require('proxyquire')
|
||||||
|
const append = require('append-child')
|
||||||
|
const view = require('../../html')
|
||||||
|
|
||||||
|
test('routing', function (t) {
|
||||||
|
t.test('history', function (t) {
|
||||||
|
t.plan(3)
|
||||||
|
|
||||||
|
const history = Event()
|
||||||
|
const choo = proxyquire('../..', {
|
||||||
|
'sheet-router/history': history.listen
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = choo()
|
||||||
|
|
||||||
|
app.model({
|
||||||
|
state: {
|
||||||
|
user: null
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
set: (action, state) => ({user: action.id})
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
open: function (action, state, send, done) {
|
||||||
|
t.deepEqual(action, {id: 1})
|
||||||
|
send('set', {id: 1}, function (err) {
|
||||||
|
if (err) return done(err)
|
||||||
|
history.broadcast('https://foo.com/users/1')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.router('/users', (route) => [
|
||||||
|
route('/users', parentView, [
|
||||||
|
route('/:user', childView)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
|
||||||
|
const tree = app.start()
|
||||||
|
t.on('end', append(tree))
|
||||||
|
|
||||||
|
t.equal(tree.innerHTML.trim(), 'Open')
|
||||||
|
tree.onclick()
|
||||||
|
|
||||||
|
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>`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('hash', function (t) {
|
||||||
|
t.plan(1)
|
||||||
|
|
||||||
|
const hash = Event()
|
||||||
|
const choo = proxyquire('../..', {
|
||||||
|
'sheet-router/hash': hash.listen
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = choo()
|
||||||
|
|
||||||
|
app.model({
|
||||||
|
state: {
|
||||||
|
user: null
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
set: (action, state) => ({user: action.id})
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
open: function (action, state, 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)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
|
||||||
|
const tree = app.start({hash: true})
|
||||||
|
t.on('end', append(tree))
|
||||||
|
|
||||||
|
tree.onclick()
|
||||||
|
|
||||||
|
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>`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('disabling history', function (t) {
|
||||||
|
t.plan(1)
|
||||||
|
|
||||||
|
const choo = proxyquire('../..', {
|
||||||
|
'sheet-router/history': () => t.fail('history listener attached')
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = choo()
|
||||||
|
|
||||||
|
app.router('/', (route) => [
|
||||||
|
route('/', function () {
|
||||||
|
t.pass('rendered')
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
app.start({history: false})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('disabling href', function (t) {
|
||||||
|
t.plan(1)
|
||||||
|
|
||||||
|
const choo = proxyquire('../..', {
|
||||||
|
'sheet-router/href': () => t.fail('href listener attached')
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = choo()
|
||||||
|
|
||||||
|
app.router('/', (route) => [
|
||||||
|
route('/', function () {
|
||||||
|
t.pass('rendered')
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
app.start({href: false})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('viewless nesting', function (t) {
|
||||||
|
t.plan(1)
|
||||||
|
|
||||||
|
const choo = require('../..')
|
||||||
|
const app = choo()
|
||||||
|
|
||||||
|
app.router('/users/123', (route) => [
|
||||||
|
route('/users', [
|
||||||
|
route('/:user', function (state) {
|
||||||
|
t.deepEqual(state.params, {user: '123'})
|
||||||
|
})
|
||||||
|
])
|
||||||
|
])
|
||||||
|
|
||||||
|
app.start()
|
||||||
|
})
|
||||||
|
})
|
||||||
+33
-18
@@ -1,13 +1,15 @@
|
|||||||
const tape = require('tape')
|
const test = require('tape')
|
||||||
|
const minDocument = require('min-document')
|
||||||
const choo = require('../../')
|
const choo = require('../../')
|
||||||
|
const view = require('../../html')
|
||||||
|
|
||||||
tape('should render on the server', function (t) {
|
test('server', function (t) {
|
||||||
t.test('should render a static response', function (t) {
|
t.test('renders a static html response', function (t) {
|
||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.router((route) => [
|
app.router((route) => [
|
||||||
route('/', () => choo.view`<h1>Hello Tokyo!</h1>`)
|
route('/', () => view`<h1>Hello Tokyo!</h1>`)
|
||||||
])
|
])
|
||||||
|
|
||||||
const html = app.toString('/')
|
const html = app.toString('/')
|
||||||
@@ -15,13 +17,26 @@ tape('should render on the server', function (t) {
|
|||||||
t.equal(html, expected, 'strings are equal')
|
t.equal(html, expected, 'strings are equal')
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should accept a state object', function (t) {
|
t.test('renders without a real DOM', function (t) {
|
||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.router((route) => [
|
app.router((route) => [
|
||||||
route('/', function (params, state) {
|
route('/', () => minDocument.createElement('div'))
|
||||||
return choo.view`<h1>meow meow ${state.message}</h1>`
|
])
|
||||||
|
|
||||||
|
const html = app.toString('/')
|
||||||
|
const expected = '<div></div>'
|
||||||
|
t.equal(html, expected, 'strings are equal')
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('receives a state object', function (t) {
|
||||||
|
t.plan(1)
|
||||||
|
|
||||||
|
const app = choo()
|
||||||
|
app.router((route) => [
|
||||||
|
route('/', function (state, prev, send) {
|
||||||
|
return view`<h1>meow meow ${state.message}</h1>`
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -30,14 +45,14 @@ tape('should render on the server', function (t) {
|
|||||||
t.equal(html, expected, 'strings are equal')
|
t.equal(html, expected, 'strings are equal')
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should extend flat existing models', function (t) {
|
t.test('extends flat existing models', function (t) {
|
||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
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((route) => [
|
||||||
route('/', function (params, state) {
|
route('/', function (state, prev, send) {
|
||||||
return choo.view`<h1>${state.foo} ${state.bin} ${state.beep}</h1>`
|
return view`<h1>${state.foo} ${state.bin} ${state.beep}</h1>`
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -47,7 +62,7 @@ tape('should render on the server', function (t) {
|
|||||||
t.equal(html, expected, 'strings are equal')
|
t.equal(html, expected, 'strings are equal')
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should extend namespaced existing models', function (t) {
|
t.test('extends namespaced existing models', function (t) {
|
||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
@@ -56,8 +71,8 @@ tape('should render on the server', function (t) {
|
|||||||
state: { bin: 'baz', beep: 'boop' }
|
state: { bin: 'baz', beep: 'boop' }
|
||||||
})
|
})
|
||||||
app.router((route) => [
|
app.router((route) => [
|
||||||
route('/', function (params, state) {
|
route('/', function (state, prev, send) {
|
||||||
return choo.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>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
@@ -74,12 +89,12 @@ tape('should render on the server', function (t) {
|
|||||||
t.equal(html, expected, 'strings are equal')
|
t.equal(html, expected, 'strings are equal')
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should throw if called without route', function (t) {
|
t.test('throws if called without route', function (t) {
|
||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.router((route) => [
|
app.router((route) => [
|
||||||
route('/', function (params, state, send) {
|
route('/', function (state, prev, send) {
|
||||||
send('hey!')
|
send('hey!')
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
@@ -87,17 +102,17 @@ tape('should render on the server', function (t) {
|
|||||||
t.throws(app.toString.bind(null), /route must be a string/)
|
t.throws(app.toString.bind(null), /route must be a string/)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.test('should throw if calling send()', function (t) {
|
t.test('throws when calling send()', function (t) {
|
||||||
t.plan(1)
|
t.plan(1)
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.router((route) => [
|
app.router((route) => [
|
||||||
route('/', function (params, state, send) {
|
route('/', function (state, prev, send) {
|
||||||
send('hey!')
|
send('hey!')
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
|
||||||
const msg = /send\(\) cannot be called on the server/
|
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