implement choo v5 API (#425)
* implement choo v5 API * fix tests * 5.0.0-3 * 4kb now * bump nanomorph * 5.0.0-4 * fix missing deps stuff * 5.0.0-5 * fix travis * fixup! oops standard * chooexpose * fix state passing * fixup! update example * fixup! incorporate feedback * fixup! more docs * fixup! even more docs
This commit is contained in:
+2
-2
@@ -10,8 +10,8 @@ addons:
|
|||||||
before_install:
|
before_install:
|
||||||
- export DISPLAY=':99.0'
|
- export DISPLAY=':99.0'
|
||||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||||
script: npm run test:cov
|
script: npm run test
|
||||||
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:
|
||||||
global:
|
global:
|
||||||
- secure: t6MYp/rV/HXKQ8yb21fDMAV9fkwZmPFJ/4Za4TeXvVOojFan4Q40j5VZF6wiVBHTemctMRueQ2iuGl8pgNX3jqRHMKvNS5zKCCUHYvmyvMLQiWpkDC3Lz3R9oz2fooY1f0fpnzaE0TmJHoZQcVDYDTZSk/o6fNXJQnI4SUG/nmnb3VAA0H8lRZ1OidkAbBzJNFj8i0ZTuaqwo0uQVhNDSvLLKgM94ryF+DB+c0vKuZr85wn+xnuJ1ZPZM5S+uZrOAY+TnwD1QvstULHnbF6sqH5WuUkUCHpXV84Hz2pr7GL7yCkj65XoNoqvnoR5ETv8yBa5r/OccrCY9NqXSCv/zWiS4vRWybrJq0Mdx67cXHKHjbQMrILHsbRBDxbCJwn+LAQlwRUoktEcI9bNRUhi0Wt/LQmLvyfldt7mMvgl+7srBdlZWPoTZWRZKqu04XpYTaqAnAeNOjfSOOuB29uVTRu7n9pCeKA/gDNYOWCF3CfN++ztbL+E2OpPnpoRWwoSXnXzFQrLofq6zF1bpIrb5UpaqNYGX+PifUYX+hRYKLdOqqAS3JlJBqG+0ArRZgf3sm/uk4ZDesV5Ee9qB7Ty32Vh66wmU8P6wZjgare4TL+hcsgtNRrd6+kZwXfn0mqcqeFovN2yPpQSmF1U5+ki8+o1NcFFYdeucWa7E9ULkVE=
|
- secure: t6MYp/rV/HXKQ8yb21fDMAV9fkwZmPFJ/4Za4TeXvVOojFan4Q40j5VZF6wiVBHTemctMRueQ2iuGl8pgNX3jqRHMKvNS5zKCCUHYvmyvMLQiWpkDC3Lz3R9oz2fooY1f0fpnzaE0TmJHoZQcVDYDTZSk/o6fNXJQnI4SUG/nmnb3VAA0H8lRZ1OidkAbBzJNFj8i0ZTuaqwo0uQVhNDSvLLKgM94ryF+DB+c0vKuZr85wn+xnuJ1ZPZM5S+uZrOAY+TnwD1QvstULHnbF6sqH5WuUkUCHpXV84Hz2pr7GL7yCkj65XoNoqvnoR5ETv8yBa5r/OccrCY9NqXSCv/zWiS4vRWybrJq0Mdx67cXHKHjbQMrILHsbRBDxbCJwn+LAQlwRUoktEcI9bNRUhi0Wt/LQmLvyfldt7mMvgl+7srBdlZWPoTZWRZKqu04XpYTaqAnAeNOjfSOOuB29uVTRu7n9pCeKA/gDNYOWCF3CfN++ztbL+E2OpPnpoRWwoSXnXzFQrLofq6zF1bpIrb5UpaqNYGX+PifUYX+hRYKLdOqqAS3JlJBqG+0ArRZgf3sm/uk4ZDesV5Ee9qB7Ty32Vh66wmU8P6wZjgare4TL+hcsgtNRrd6+kZwXfn0mqcqeFovN2yPpQSmF1U5+ki8+o1NcFFYdeucWa7E9ULkVE=
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
ui: tape
|
|
||||||
browsers:
|
|
||||||
- name: chrome
|
|
||||||
version: latest
|
|
||||||
os: 'Mac 10.11'
|
|
||||||
- name: internet explorer
|
|
||||||
version: 9..11
|
|
||||||
os: 'Windows 10'
|
|
||||||
- name: firefox
|
|
||||||
version: latest
|
|
||||||
os: 'Mac 10.11'
|
|
||||||
- name: microsoftedge
|
|
||||||
version: latest
|
|
||||||
- name: safari
|
|
||||||
version: latest
|
|
||||||
os: 'Mac 10.11'
|
|
||||||
- name: iphone
|
|
||||||
version: '8.4..9.2'
|
|
||||||
- name: android
|
|
||||||
version: '4.3..5.1'
|
|
||||||
- name: opera
|
|
||||||
version: latest
|
|
||||||
platform: 'Windows 10'
|
|
||||||
browserify:
|
|
||||||
- transform:
|
|
||||||
name: es2020
|
|
||||||
global: true
|
|
||||||
- transform: sheetify/transform
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<strong>Fun functional programming</strong>
|
<strong>Fun functional programming</strong>
|
||||||
</div>
|
</div>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
A <code>5kb</code> framework for creating sturdy frontend applications
|
A <code>4kb</code> framework for creating sturdy frontend applications
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
@@ -47,6 +47,10 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h3>
|
<h3>
|
||||||
|
<a href="https://choo.io">
|
||||||
|
Website
|
||||||
|
</a>
|
||||||
|
<span> | </span>
|
||||||
<a href="https://yoshuawuyts.gitbooks.io/choo/content">
|
<a href="https://yoshuawuyts.gitbooks.io/choo/content">
|
||||||
Handbook
|
Handbook
|
||||||
</a>
|
</a>
|
||||||
@@ -55,10 +59,10 @@
|
|||||||
Ecosystem
|
Ecosystem
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://github.com/trainyard/choo-cli">
|
<!-- <a href="https://github.com/trainyard/choo-cli"> -->
|
||||||
CLI
|
<!-- CLI -->
|
||||||
</a>
|
<!-- </a> -->
|
||||||
<span> | </span>
|
<!-- <span> | </span> -->
|
||||||
<a href="https://github.com/yoshuawuyts/choo/blob/master/.github/CONTRIBUTING.md">
|
<a href="https://github.com/yoshuawuyts/choo/blob/master/.github/CONTRIBUTING.md">
|
||||||
Contributing
|
Contributing
|
||||||
</a>
|
</a>
|
||||||
@@ -77,98 +81,64 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Table of Contents</h2>
|
## Table of Content
|
||||||
<details>
|
- [Features](#features)
|
||||||
<summary>Table of Contents</summary>
|
- [Example](#example)
|
||||||
<li><a href="#features">Features</a></li>
|
- [Philosophy](#philosophy)
|
||||||
<li><a href="#demos">Demos</a></li>
|
- [Optimizations](#optimizations)
|
||||||
<li><a href="#example">Example</a></li>
|
- [FAQ](#faq)
|
||||||
<li><a href="#philosophy">Philosophy</a></li>
|
- [API](#api)
|
||||||
<li><a href="#concepts">Concepts</a></li>
|
- [Installation](#installation)
|
||||||
<li><a href="#badges">Badges</a></li>
|
- [See Also](#see-also)
|
||||||
<li><a href="#api">API</a></li>
|
- [Support](#support)
|
||||||
<li><a href="#faq">FAQ</a></li>
|
|
||||||
<li><a href="#installation">Installation</a></li>
|
|
||||||
<li><a href="#see-also">See Also</a></li>
|
|
||||||
<li><a href="#support">Support</a></li>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- __minimal size:__ weighing `5kb`, `choo` is a tiny little framework
|
- __minimal size:__ weighing `4kb`, `choo` is a tiny little framework
|
||||||
- __single state:__ immutable single state helps reason about changes
|
- __event based:__ our performant event system makes writing apps easy
|
||||||
- __small api:__ with only 6 methods, there's not a lot to learn
|
- __small api:__ with only 6 methods there's not much to learn
|
||||||
- __minimal tooling:__ built for the cutting edge `browserify` compiler
|
- __minimal tooling:__ built for the cutting edge `browserify` compiler
|
||||||
- __transparent side effects:__ using `effects` and `subscriptions` brings
|
|
||||||
clarity to IO
|
|
||||||
- __omakase:__ composed out of a balanced selection of open source packages
|
|
||||||
- __isomorphic:__ renders seamlessly in both Node and browsers
|
- __isomorphic:__ renders seamlessly in both Node and browsers
|
||||||
- __very cute:__ choo choo!
|
- __very cute:__ choo choo!
|
||||||
|
|
||||||
## Demos
|
|
||||||
- :truck: [Input example](http://requirebin.com/?gist=229bceda0334cf30e3044d5f5c600960)
|
|
||||||
([repo](examples/title/))
|
|
||||||
- :water_buffalo: [HTTP effects example](https://fork-fang.hyperdev.space/)
|
|
||||||
([repo](https://hyperdev.com/#!/project/fork-fang))
|
|
||||||
- :mailbox: [Mailbox routing](examples/mailbox/)
|
|
||||||
- :ok_hand: [TodoMVC](http://shuheikagawa.com/todomvc-choo/)
|
|
||||||
([repo](https://github.com/shuhei/todomvc-choo))
|
|
||||||
- :fire: [Choo-firebase](https://github.com/fyrkant/choo-firebase)
|
|
||||||
- :seedling: [Grow](https://grow.static.land/)
|
|
||||||
([repo](https://github.com/sethvincent/grow))
|
|
||||||
- :newspaper: [Hacker News Reader](https://hackernews-choo.surge.sh/) ([repo](https://github.com/kvnneff/hackernews-choo))
|
|
||||||
- :earth_africa: [Thing Translator](https://oxism.com/thing-translator/) ([repo](https://github.com/dmotz/thing-translator))
|
|
||||||
|
|
||||||
_note: If you've built something cool using `choo` or are using it in
|
|
||||||
production, we'd love to hear from you!_
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
Let's create an input box that changes the content of a textbox in real time.
|
|
||||||
[Click here to see the app running](http://requirebin.com/?gist=229bceda0334cf30e3044d5f5c600960).
|
|
||||||
```js
|
```js
|
||||||
var html = require('choo/html')
|
var html = require('./html')
|
||||||
var choo = require('choo')
|
var choo = require('./')
|
||||||
|
|
||||||
var app = choo()
|
var app = choo()
|
||||||
|
app.use(logger)
|
||||||
|
app.use(countStore)
|
||||||
|
app.route('/', mainView)
|
||||||
|
app.mount('body')
|
||||||
|
|
||||||
app.model({
|
function mainView (state, emit) {
|
||||||
state: { title: 'Not quite set yet' },
|
|
||||||
reducers: {
|
|
||||||
update: function (state, data) {
|
|
||||||
return { title: data }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function mainView (state, prev, send) {
|
|
||||||
return html`
|
return html`
|
||||||
<main>
|
<body>
|
||||||
<h1>Title: ${state.title}</h1>
|
<h1>count is ${state.count}</h1>
|
||||||
<input type="text" oninput=${update}>
|
<button onclick=${onclick}>Increment</button>
|
||||||
</main>
|
</body>
|
||||||
`
|
`
|
||||||
|
|
||||||
function update (e) {
|
function onclick () {
|
||||||
send('update', e.target.value)
|
emit('increment', 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.router(['/', mainView])
|
function logger (state, emitter) {
|
||||||
|
emitter.on('*', function (messageName, data) {
|
||||||
|
console.log('event', messageName, data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var tree = app.start()
|
function countStore (state, emitter) {
|
||||||
document.body.appendChild(tree)
|
state.count = 0
|
||||||
```
|
emitter.on('increment', function (count) {
|
||||||
|
state.count += count
|
||||||
To run it, save it as `client.js` and run with [bankai][bankai]. `bankai` is
|
emitter.emit('render')
|
||||||
convenient but any [browserify][browserify] based tool should do:
|
})
|
||||||
```sh
|
}
|
||||||
# run and reload on port 8080
|
|
||||||
$ bankai client.js -p 8080 --open
|
|
||||||
|
|
||||||
# compile to static files in `./dist/`
|
|
||||||
$ bankai build index.js dist/
|
|
||||||
|
|
||||||
# deploy to github pages using `tschaub/gh-pages`
|
|
||||||
$ gh-pages -d dist/
|
|
||||||
```
|
```
|
||||||
|
Want to see more examples? Check out the [Choo handbook][handbook].
|
||||||
|
|
||||||
## Philosophy
|
## Philosophy
|
||||||
We believe programming should be fun and light, not stern and stressful. It's
|
We believe programming should be fun and light, not stern and stressful. It's
|
||||||
@@ -193,395 +163,42 @@ 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,
|
performant and easy to reason about. All of which makes for easy to debug code,
|
||||||
better results and super smiley faces.
|
better results and super smiley faces.
|
||||||
|
|
||||||
## Concepts
|
## Optimizations
|
||||||
`choo` cleanly structures internal data flow, so that all pieces of logic can
|
Choo is reasonably fast out of the box. But sometimes you might hit a scenario
|
||||||
be combined into a nice, cohesive machine. Roughly speaking there are two parts
|
where a particular part of the UI slows down the application, and you want to
|
||||||
to `choo`: the views and the models. Models take care of state and logic, and
|
speed it up. Here are some optimizations that are possible.
|
||||||
views are responsible for displaying the interface and responding to user
|
|
||||||
interactions.
|
|
||||||
|
|
||||||
All of `choo`'s state is contained in a single object and whenever it changes
|
### Reordering lists
|
||||||
the views receive a new version of the state which they can use to safely
|
To be implemented. (See [yoshuawuyts/nanomorph#8](https://github.com/yoshuawuyts/nanomorph/issues/8))
|
||||||
render a complete new representation of the DOM. The DOM is efficiently updated
|
|
||||||
using DOM diffing/patching.
|
|
||||||
|
|
||||||
The logic in choo exist in three different kinds of actions, each with their
|
### Caching DOM elements
|
||||||
own role: `effects`, `subscriptions` and `reducers`.
|
Sometimes we want to tell the algorithm to not evaluate certain nodes (and its
|
||||||
|
children). This can be because we're sure they haven't changed, or perhaps
|
||||||
- __Effects__ makes an asynchronous operation and calls another action when
|
because another piece of code is managing that part of the DOM tree. To achieve
|
||||||
it's done.
|
this `nanomorph` evaluates the `.isSameNode()` method on nodes to determine if
|
||||||
|
they should be updated or not.
|
||||||
- __Subscriptions__ (called once when the DOM loads) listens for external input
|
|
||||||
like keyboard or WebSocket events and then calls another action.
|
|
||||||
|
|
||||||
- __Reducers__ receives the current state and returns an updated version of the
|
|
||||||
state which is then sent to the views.
|
|
||||||
|
|
||||||
```txt
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Subscriptions ─┤ User ───┐
|
|
||||||
└─ Effects ◀─────┤ ▼
|
|
||||||
┌─ Reducers ◀─────┴─────────── DOM ◀┐
|
|
||||||
│ │
|
|
||||||
└▶ Router ─────State ───▶ Views ────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Models
|
|
||||||
`models` are objects that contain initial `state`, `subscriptions`, `effects`
|
|
||||||
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
|
|
||||||
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
|
|
||||||
recommended to keep that to a minimum.
|
|
||||||
|
|
||||||
So say we have a `todos` namespace, an `add` reducer and a `todos` model.
|
|
||||||
Outside the model they're called by `send('todos:add')` and
|
|
||||||
`state.todos.items`. Inside the namespaced model they're called by
|
|
||||||
`send('todos:add')` and `state.items`. An example namespaced model:
|
|
||||||
```js
|
|
||||||
var app = choo()
|
|
||||||
app.model({
|
|
||||||
namespace: 'todos',
|
|
||||||
state: { items: [] },
|
|
||||||
reducers: {
|
|
||||||
add: function (state, data) {
|
|
||||||
return { items: state.items.concat(data.payload) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
In most cases using namespaces is beneficial, as having clear boundaries makes
|
|
||||||
it easier to follow logic. But sometimes you need to call `actions` that
|
|
||||||
operate over multiple domains (such as a "logout" `action`), or have a
|
|
||||||
`subscription` that might trigger multiple `reducers` (such as a `websocket`
|
|
||||||
that calls a different `action` based on the incoming data).
|
|
||||||
|
|
||||||
In these cases you probably want to have a `model` that doesn't use namespaces,
|
|
||||||
and has access to the full application state. Try and keep the logic in these
|
|
||||||
`models` to a minimum, and declare as few `reducers` as possible. That way the
|
|
||||||
bulk of your logic will be safely shielded, with only a few points touching every
|
|
||||||
part of your application.
|
|
||||||
|
|
||||||
### Effects
|
|
||||||
`effects` are similar to `reducers` except instead of modifying the state they
|
|
||||||
cause side `effects` by interacting servers, databases, DOM APIs, etc. Often
|
|
||||||
they'll call a reducer when they're done to update the state. For instance, you
|
|
||||||
may have an effect called getUsers that fetches a list of users from a server
|
|
||||||
API using AJAX. Assuming the AJAX request completes successfully, the effect
|
|
||||||
can pass off the list of users to a reducer called receiveUsers which simply
|
|
||||||
updates the state with that list, separating the concerns of interacting with
|
|
||||||
an API from updating the application's state.
|
|
||||||
|
|
||||||
This is an example `effect` that is called once when the application loads and
|
|
||||||
calls the `'todos:add'` `reducer` when it receives data from the server:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var choo = require('choo')
|
var el = html`<div>node</div>`
|
||||||
var http = require('xhr')
|
|
||||||
var app = choo()
|
|
||||||
|
|
||||||
app.model({
|
// tell nanomorph to not compare the DOM tree if they're both divs
|
||||||
namespace: 'todos',
|
el.isSameNode = function (target) {
|
||||||
state: { values: [] },
|
return (target && target.nodeName && target.nodeName === 'DIV')
|
||||||
reducers: {
|
|
||||||
add: function (state, data) {
|
|
||||||
return { todos: data }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
addAndSave: function (state, data, send, done) {
|
|
||||||
var opts = { body: data.payload, json: true }
|
|
||||||
http.post('/todo', opts, function (err, res, body) {
|
|
||||||
if (err) return done(err)
|
|
||||||
data.payload.id = body.id
|
|
||||||
send('todos:add', data, function (err, value) {
|
|
||||||
if (err) return done(err)
|
|
||||||
done(null, value)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
subscriptions: {
|
|
||||||
'called-once-when-the-app-loads': function (send, done) {
|
|
||||||
send('todos:addAndSave', done)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Subscriptions
|
|
||||||
Subscriptions are a way of receiving data from a source. For example when
|
|
||||||
listening for events from a server using `SSE` or `Websockets` for a
|
|
||||||
chat app, or when catching keyboard input for a videogame.
|
|
||||||
|
|
||||||
An example subscription that logs `"dog?"` every second:
|
|
||||||
```js
|
|
||||||
var choo = require('choo')
|
|
||||||
|
|
||||||
var app = choo()
|
|
||||||
app.model({
|
|
||||||
namespace: 'app',
|
|
||||||
effects: {
|
|
||||||
print: function (state, data) {
|
|
||||||
console.log(data.payload)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
subscriptions: {
|
|
||||||
callDog: function (send, done) {
|
|
||||||
setInterval(function () {
|
|
||||||
var data = { payload: 'dog?', myOtherValue: 1000 }
|
|
||||||
send('app:print', data, function (err) {
|
|
||||||
if (err) return done(err)
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
If a `subscription` runs into an error, it can call `done(err)` to signal the
|
|
||||||
error to the error hook.
|
|
||||||
|
|
||||||
### Router
|
|
||||||
The `router` manages which `views` are rendered at any given time. It also
|
|
||||||
supports rendering a default `view` if no routes match.
|
|
||||||
|
|
||||||
```js
|
|
||||||
var app = choo()
|
|
||||||
app.router({ default: '/404' }, [
|
|
||||||
[ '/', require('./views/empty') ],
|
|
||||||
[ '/404', require('./views/error') ],
|
|
||||||
[ '/:mailbox', require('./views/mailbox'), [
|
|
||||||
[ '/:message', require('./views/email') ]
|
|
||||||
]]
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
Routes on the `router` are passed in as a nested array. This means that the
|
|
||||||
entry point of the application also becomes a site map, making it easier to
|
|
||||||
figure out how views relate to each other.
|
|
||||||
|
|
||||||
Under the hood `choo` uses [sheet-router]. Internally the
|
|
||||||
currently rendered route is kept in `state.location`. To access the route
|
|
||||||
`:params` you can use `state.location.params`. If you want to modify
|
|
||||||
the location programmatically the `reducer` for the location can be called
|
|
||||||
using `send('location:set', href)`. This will not work
|
|
||||||
from within namespaced `models`, and usage should preferably be kept to a
|
|
||||||
minimum. Changing views all over the place tends to lead to messiness.
|
|
||||||
|
|
||||||
### Views
|
|
||||||
Views are pure functions that return a DOM tree for the router to render.
|
|
||||||
They’re passed the current state, and any time the state changes they’re run
|
|
||||||
again with the new state.
|
|
||||||
|
|
||||||
Views are 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.
|
|
||||||
|
|
||||||
```js
|
|
||||||
function view (state, prev, send) {
|
|
||||||
return html`
|
|
||||||
<div>
|
|
||||||
<h1>Total todos: ${state.todos.length}</h1>
|
|
||||||
<button onclick=${addTodo}>Add</button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
function addTodo (e) {
|
|
||||||
send('add', { title: 'demo' })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, when the `Add` button is clicked, the view will dispatch an
|
### Pruning dependencies
|
||||||
`add` action that the model’s `add` reducer will receive. [As seen
|
We use the `require('assert')` module from Node core to provide helpful error
|
||||||
above](#models), the reducer will add an item to the state’s `todos` array. The
|
messages in development. In production you probably want to strip this using
|
||||||
state change will cause this view to be run again with the new state, and the
|
[unassertify][unassertify].
|
||||||
resulting DOM tree will be used to [efficiently patch the
|
|
||||||
DOM](#does-choo-use-a-virtual-dom).
|
|
||||||
|
|
||||||
### Plugins
|
To convert inlined HTML to valid DOM nodes we use `require('bel')`. This has
|
||||||
Sometimes it's necessary to change the way `choo` itself works. For example to
|
overhead during runtime, so for production environments we should unwrap this
|
||||||
report whenever an action is triggered, handle errors globally or perist state
|
using [yo-yoify][yo-yoify].
|
||||||
somewhere. This is done through something called `plugins`. Plugins are objects
|
|
||||||
that contain `hook` and `wrap` functions and are passed to `app.use()`:
|
|
||||||
|
|
||||||
```js
|
Setting up browserify transforms can sometimes be a bit of hassle; to make this
|
||||||
var log = require('choo-log')
|
more convenient we recommend using [bankai][bankai] with `--optimize` to
|
||||||
var choo = require('choo')
|
compile your assets for production.
|
||||||
var app = choo()
|
|
||||||
|
|
||||||
app.use(log())
|
|
||||||
|
|
||||||
var tree = app.start()
|
|
||||||
document.body.appendChild(tree)
|
|
||||||
```
|
|
||||||
|
|
||||||
Generally people using `choo` shouldn't be too worried about the specifics of
|
|
||||||
`plugins`, as the internal API is (unfortunately by necessity) quite complex.
|
|
||||||
After all they're the most powerful way to modify a `choo` application.
|
|
||||||
|
|
||||||
__:warning: Warning :warning:: plugins should only be used as a last resort.
|
|
||||||
It creates peer dependencies which makes upgrading versions and switching
|
|
||||||
frameworks a lot harder. Please exhaust all other options before using
|
|
||||||
plugins.__
|
|
||||||
|
|
||||||
If you want to learn more about creating your own `plugins`, and which `hooks`
|
|
||||||
and `wrappers` are available, head on over to [app.use()](#appusehooks).
|
|
||||||
|
|
||||||
## Badges
|
|
||||||
Using `choo` in a project? Show off which version you've used using a badge:
|
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/yoshuawuyts/choo)
|
|
||||||
```md
|
|
||||||
[](https://github.com/yoshuawuyts/choo)
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
This section provides documentation on how each function in `choo` works. It's
|
|
||||||
intended to be a technical reference. If you're interested in learning choo for
|
|
||||||
the first time, consider reading through the [handbook] or
|
|
||||||
[concepts](#concepts) first :sparkles:
|
|
||||||
|
|
||||||
### app = choo(opts)
|
|
||||||
Initialize a new `choo` app. Takes an optional object of handlers that is
|
|
||||||
passed to [app.use()](#appusehooks).
|
|
||||||
|
|
||||||
Opts can also contain the following values:
|
|
||||||
- __opts.history:__ default: `true`. Enable a `subscription` to the browser
|
|
||||||
history API. e.g. updates the internal `location.href` state whenever the
|
|
||||||
browsers "forward" and "backward" buttons are pressed.
|
|
||||||
- __opts.href:__ default: `true`. Handle all relative `<a
|
|
||||||
href="<location>"></a>` clicks and update internal `state.location`
|
|
||||||
accordingly.
|
|
||||||
- __opts.hash:__ default: `false`. Enable a `subscription` to the hash change
|
|
||||||
event, updating the internal `state.location` state whenever the URL hash
|
|
||||||
changes (eg `localhost/#posts/123`). Enabling this option automatically
|
|
||||||
disables `opts.history` and `opts.href`.
|
|
||||||
|
|
||||||
### app.model(obj)
|
|
||||||
Create a new model. Models modify data and perform IO. Takes the following
|
|
||||||
arguments:
|
|
||||||
- __namespace:__ namespace the model so that it cannot access any properties
|
|
||||||
and handlers in other models
|
|
||||||
- __state:__ initial values of `state` inside the model
|
|
||||||
- __reducers:__ synchronous operations that modify state. Triggered by
|
|
||||||
`actions`. Signature of `(state, data)`.
|
|
||||||
- __effects:__ asynchronous operations that don't modify state directly.
|
|
||||||
Triggered by `actions`, can call `actions`. Signature of `(state, data,
|
|
||||||
send, done)`
|
|
||||||
- __subscriptions:__ asynchronous read-only operations that don't modify state
|
|
||||||
directly. Can call `actions`. Signature of `(send, done)`.
|
|
||||||
|
|
||||||
#### send(actionName, data?[,callback])
|
|
||||||
Send a new action to the models with optional data attached. Namespaced models
|
|
||||||
can be accessed by prefixing the name with the namespace separated with a `:`,
|
|
||||||
e.g. `namespace:name`.
|
|
||||||
|
|
||||||
When sending data from inside a `model` it expects exactly three arguments: the name of the action you're calling, the data you want to send, and finally a callback to handle errors through the global `onError()` hook. So if you want to send two values, you'd have to either send an array or object containing them.
|
|
||||||
|
|
||||||
#### done(err?, res?)
|
|
||||||
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] for full
|
|
||||||
documentation. Registered views have a signature of `(state, prev, send)`,
|
|
||||||
where `state` is the current `state`, `prev` is the last state,
|
|
||||||
`state.location.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.
|
|
||||||
|
|
||||||
### app.use(hooks)
|
|
||||||
Register an object of hooks on the application. This is useful to extend the
|
|
||||||
way `choo` works, adding custom behavior and listeners. Generally returning
|
|
||||||
objects of `hooks` is done by returning them from functions (which we call
|
|
||||||
`plugins` throughout the documentation).
|
|
||||||
|
|
||||||
There are several `hooks` and `wrappers` that are picked up by `choo`:
|
|
||||||
- __onError(err, state, createSend):__ called when an `effect` or
|
|
||||||
`subscription` emit an error. If no handler is passed, the default handler
|
|
||||||
will `throw` on each error.
|
|
||||||
- __onAction(state, data, name, caller, createSend):__ called when an
|
|
||||||
`action` is fired.
|
|
||||||
- __onStateChange(state, data, prev, caller, createSend):__ called after a
|
|
||||||
reducer changes the `state`.
|
|
||||||
- __wrapSubscriptions(fn):__ wraps a `subscription` to add custom behavior
|
|
||||||
- __wrapReducers(fn):__ wraps a `reducer` to add custom behavior
|
|
||||||
- __wrapEffects(fn):__ wraps an `effect` to add custom behavior
|
|
||||||
- __wrapInitialState(fn):__ mutate the complete initial `state` to add custom
|
|
||||||
behavior - useful to mutate the state before starting up
|
|
||||||
|
|
||||||
__:warning: Warning :warning:: plugins should only be used as a last resort.
|
|
||||||
It creates peer dependencies which makes upgrading versions and switching
|
|
||||||
frameworks a lot harder. Please exhaust all other options before using
|
|
||||||
plugins.__
|
|
||||||
|
|
||||||
`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.
|
|
||||||
|
|
||||||
Hooks 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 hooks.
|
|
||||||
|
|
||||||
### html = app.toString(route, state?)
|
|
||||||
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 an
|
|
||||||
optional state object. When calling `.toString()` instead of `.start()`, all
|
|
||||||
calls to `send()` are disabled, and `subscriptions`, `effects` and `reducers`
|
|
||||||
aren't loaded.
|
|
||||||
|
|
||||||
### tree = app.start()
|
|
||||||
Start the application. Returns a tree of DOM nodes that can be mounted using
|
|
||||||
`document.body.appendChild()`.
|
|
||||||
|
|
||||||
### view = require('choo/html')\`html\`
|
|
||||||
Tagged template string HTML builder. Built on top of [yo-yo], [bel], and
|
|
||||||
[hyperx]. To register a view on the `router` it should be wrapped in a function
|
|
||||||
with the signature of `(state, prev, send)` where `state` is the current
|
|
||||||
`state`, `prev` is the last state, `state.location.params` is URI partials and
|
|
||||||
`send()` can be called to trigger actions.
|
|
||||||
|
|
||||||
To create listeners for events, create interpolated attributes on elements.
|
|
||||||
```js
|
|
||||||
var html = require('choo/html')
|
|
||||||
html`
|
|
||||||
<button onclick=${log}>click for bananas</button>
|
|
||||||
`
|
|
||||||
|
|
||||||
function log (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Example listeners include: `onclick`, `onsubmit`, `oninput`, `onkeydown`,
|
|
||||||
`onkeyup`. A full list can be found [at the yo-yo
|
|
||||||
repo](https://github.com/maxogden/yo-yo/blob/master/update-events.js). When
|
|
||||||
creating listeners always remember to call `e.preventDefault()` and
|
|
||||||
`e.stopPropagation()` 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.
|
|
||||||
|
|
||||||
### mount('selector', html)
|
|
||||||
Use `choo/mount` to mount a tree of DOMNodes at a given selector. This is
|
|
||||||
especially useful to mount a `<body>` tag on the document body after rendering
|
|
||||||
from the server. It makes sure all `<script>` tags and similar are persisted so
|
|
||||||
no duplicate download calls are triggered.
|
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
### Why is it called choo?
|
### Why is it called choo?
|
||||||
@@ -590,141 +207,73 @@ _"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 `choo`.
|
critical for serious bizcorp using a train themed framework.
|
||||||
:steam_locomotive::train::train::train:
|
:steam_locomotive::train::train::train:
|
||||||
|
|
||||||
### Why is it a framework, and not a library?
|
|
||||||
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
|
|
||||||
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
|
|
||||||
productive right from the start without needing to plough through layers of
|
|
||||||
boilerplate.
|
|
||||||
|
|
||||||
### Is it called choo, choo.js or...?
|
### Is it called choo, choo.js or...?
|
||||||
It's called "choo", though we're fine if you call it "choo-choo" 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 /
|
"chugga-chugga-choo-choo" too. The only time "choo.js" is tolerated is if /
|
||||||
when you shimmy like you're a locomotive.
|
when you shimmy like you're a locomotive.
|
||||||
|
|
||||||
### How does choo compare to X?
|
|
||||||
Ah, so this is where I get to rant. `choo` (_chugga-chugga-chugga-choo-choo!_)
|
|
||||||
was built because other options didn't quite cut it for me, so instead of
|
|
||||||
presenting some faux-objective chart with skewed benchmarks and checklists I'll
|
|
||||||
give you my opinions directly. Ready? Here goes:
|
|
||||||
- __react:__ despite being at the root of a giant paradigm shift for frontend
|
|
||||||
(thank you forever!), `react` is kind of big (`155kb` was it?). They also
|
|
||||||
like classes a lot, and enforce a _lot_ of abstractions. It also encourages
|
|
||||||
the use of `JSX` and `babel` which break _JavaScript, The Language™_. And all
|
|
||||||
that without making clear how code should flow, which is crucial in a team
|
|
||||||
setting. I don't like complicated things and in my view `react` is one of
|
|
||||||
them. `react` is not for me.
|
|
||||||
- __mithril:__ never used it, never will. I didn't like the API, but if you
|
|
||||||
like it maybe it's worth a shot - the API seems small enough. I wouldn't know
|
|
||||||
how pleasant it is past face value.
|
|
||||||
- __preact:__ a pretty cool idea; seems to fix most of what is wrong with
|
|
||||||
`react`. However it doesn't fix the large dependencies `react` seems to use
|
|
||||||
(e.g. `react-router` and friends) and doesn't help at all with architecture.
|
|
||||||
If `react` is your jam, and you will not budge, sitting at `3kb` this is
|
|
||||||
probably a welcome gift.
|
|
||||||
- __angular:__ definitely not for me. I like small things with a clear mental
|
|
||||||
model; `angular` doesn't tick any box in my book of nice things.
|
|
||||||
- __angular2:__ I'm not sure what's exactly changed, but I know the addition of
|
|
||||||
`TypeScript` and `RxJS` definitely hasn't made things simpler. Last I checked
|
|
||||||
it was `~200kb` in size before including some monstrous extra deps. I guess
|
|
||||||
`angular` and I will just never get along.
|
|
||||||
- __mercury:__ ah, `mercury` is an interesting one. It seemed like a brilliant
|
|
||||||
idea until I started using it - the abstractions felt heavy, and it took team
|
|
||||||
members a long time to pick up. In the end I think using `mercury` helped
|
|
||||||
shaped `choo` greatly, despite not working out for me.
|
|
||||||
- __deku:__ `deku` is fun. I even contributed a bit in the early days. It could
|
|
||||||
probably best be described as "a functional version of `react`". The
|
|
||||||
dependence on `JSX` isn't great, but give it a shot if you think it looks
|
|
||||||
neat.
|
|
||||||
- __cycle:__ `cycle`'s pretty good - unlike most frameworks it lays out a clear
|
|
||||||
architecture which helps with reasoning about it. That said, it's built on
|
|
||||||
`virtual-dom` and `xstream` which are a bit heavy for my taste. `choo` works
|
|
||||||
pretty well for FRP style programming, but something like [inu] might be
|
|
||||||
an interesting alternative.
|
|
||||||
- __vue:__ like `cycle`, `vue` is pretty good. But it also uses tech that
|
|
||||||
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.
|
|
||||||
|
|
||||||
### 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?
|
|
||||||
- __views:__ [`yo-yo`][yo-yo], [`bel`][bel]
|
|
||||||
- __models:__ [`barracks`](https://github.com/yoshuawuyts/barracks),
|
|
||||||
[`xtend`](https://github.com/raynos/xtend)
|
|
||||||
- __routes:__ [`sheet-router`][sheet-router]
|
|
||||||
- __http:__ [`xhr`](https://github.com/Raynos/xhr)
|
|
||||||
|
|
||||||
### Does choo use a virtual-dom?
|
### Does choo use a virtual-dom?
|
||||||
`choo` uses [morphdom][morphdom], which diffs real DOM nodes instead of virtual
|
`choo` uses [nanomorph][nanomorph], which diffs real DOM nodes instead of
|
||||||
nodes. It turns out that [browsers are actually ridiculously good at dealing
|
virtual nodes. It turns out that [browsers are actually ridiculously good at
|
||||||
with DOM nodes][morphdom-bench], and it has the added benefit of working with
|
dealing with DOM nodes][morphdom-bench], and it has the added benefit of
|
||||||
_any_ library that produces valid DOM nodes. So to put a long answer short:
|
working with _any_ library that produces valid DOM nodes. So to put a long
|
||||||
we're using something even better.
|
answer short: we're using something even better.
|
||||||
|
|
||||||
### How can I optimize choo?
|
### How can I support older browsers?
|
||||||
`choo` really shines when coupled with `browserify` transforms. They can do
|
Template strings aren't supported in all browsers, and parsing them creates
|
||||||
things like reduce file size, prune dependencies and clean up boilerplate code.
|
significant overhead. To optimize we recommend running `browserify` with
|
||||||
Consider running some of the following:
|
[yo-yoify][yo-yoify] as a global transform or using [bankai][bankai] directly.
|
||||||
- [unassertify](https://github.com/twada/unassertify) - remove `assert()`
|
|
||||||
statements which reduces file size. Use as a `--global` transform
|
|
||||||
- [es2020](https://github.com/yoshuawuyts/es2020) - backport `const`,
|
|
||||||
`arrow functions` and `template strings` to older browsers. Should be run as
|
|
||||||
a `--global` transform
|
|
||||||
- [yo-yoify](https://github.com/shama/yo-yoify) - replace the internal `hyperx`
|
|
||||||
dependency with `document.createElement` calls; greatly speeds up performance
|
|
||||||
too
|
|
||||||
- [uglifyify](https://github.com/hughsk/uglifyify) - minify your code using
|
|
||||||
UglifyJS2. Use as a `--global` transform
|
|
||||||
- [bulkify](https://www.npmjs.com/package/bulkify) - transform inline
|
|
||||||
[bulk-require](https://www.npmjs.com/package/bulk-require) calls into
|
|
||||||
statically resolvable require maps
|
|
||||||
- [envify](https://github.com/hughsk/envify) - replace `process.env` values
|
|
||||||
with plain strings
|
|
||||||
|
|
||||||
### Choo + Internet Explorer & Safari
|
|
||||||
Out of the box `choo` only supports runtimes which support:
|
|
||||||
* `const`
|
|
||||||
* `arrow functions` (e.g. `() => {}`)
|
|
||||||
* `template strings`
|
|
||||||
|
|
||||||
This does not include Safari 9 or any version of IE. If support for these
|
|
||||||
platforms is required you will have to provide some sort of transform that
|
|
||||||
makes this functionality available in older browsers. The test suite uses
|
|
||||||
[es2020](https://github.com/yoshuawuyts/es2020) as a global transform, but
|
|
||||||
anything else which might satisfy this requirement is fair game.
|
|
||||||
|
|
||||||
Generally for production builds you'll want to run:
|
|
||||||
```sh
|
```sh
|
||||||
$ NODE_ENV=production browserify \
|
$ browserify -g yo-yoify
|
||||||
-t envify \
|
|
||||||
-g yo-yoify \
|
|
||||||
-g unassertify \
|
|
||||||
-g es2020 \
|
|
||||||
-g uglifyify \
|
|
||||||
| uglifyjs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Hey, doesn't this look a lot like Elm?
|
### Is choo production ready?
|
||||||
Yup, it's greatly inspired by the `elm` architecture. But contrary to `elm`,
|
|
||||||
`choo` doesn't introduce a completely new language to build web applications.
|
|
||||||
|
|
||||||
### Is it production ready?
|
|
||||||
Sure.
|
Sure.
|
||||||
|
|
||||||
## Browser Test Status
|
## API
|
||||||
<a href="https://saucelabs.com/u/yoshuawuyts">
|
This section provides documentation on how each function in `choo` works. It's
|
||||||
<img
|
intended to be a technical reference. If you're interested in learning choo for
|
||||||
src="https://saucelabs.com/browser-matrix/yoshuawuyts.svg"
|
the first time, consider reading through the [handbook][handbook] first
|
||||||
alt="Sauce Test Status"/>
|
:sparkles:
|
||||||
</a>
|
|
||||||
|
### `app = choo([opts])`
|
||||||
|
Initialize a new `choo` instance. `opts` can also contain the following values:
|
||||||
|
- __opts.history:__ default: `true`. LIsten for url changes through the
|
||||||
|
history API.
|
||||||
|
- __opts.href:__ default: `true`. Handle all relative `<a
|
||||||
|
href="<location>"></a>` clicks and call `emit('render')`
|
||||||
|
|
||||||
|
### `app.use(callback(state, emitter))`
|
||||||
|
Call a function and pass it a `state` and `emitter`. `emitter` is an instance
|
||||||
|
of [nanobus](https://github.com/yoshuawuyts/nanobus/). You can listen to
|
||||||
|
messages by calling `emitter.on()` and emit messages by calling `bus.emit()`.
|
||||||
|
|
||||||
|
Choo fires messages when certain events happen:
|
||||||
|
- __`.on('DOMContentLoaded')`__: when the DOM has succesfully finished loading
|
||||||
|
- __`.on('render')`__: when the DOM re-renders
|
||||||
|
- __`.on('pushState')`__: when the history API is triggered
|
||||||
|
|
||||||
|
### `app.route(routeName, handler)`
|
||||||
|
Register a route on the router
|
||||||
|
|
||||||
|
### `app.mount(selector)`
|
||||||
|
Start the application and mount it on the given `querySelector`
|
||||||
|
|
||||||
|
### `tree = app.start()`
|
||||||
|
Start the application. Returns a tree of DOM nodes that can be mounted using
|
||||||
|
`document.body.appendChild()`.
|
||||||
|
|
||||||
|
### `app.toString(location, [state])`
|
||||||
|
Render the application to a string. Useful for rendering on the server.
|
||||||
|
`.use()` calls w
|
||||||
|
|
||||||
|
### `framework/html`
|
||||||
|
Create DOM nodes from template string literals. Exposes
|
||||||
|
[bel](https://github.com/shama/bel). Can be optimized using
|
||||||
|
[yo-yoify][yo-yoify].
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
```sh
|
```sh
|
||||||
@@ -732,13 +281,7 @@ $ npm install choo
|
|||||||
```
|
```
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
- [choo-handbook](https://github.com/yoshuawuyts/choo-handbook) - the little
|
- [bankai](https://github.com/yoshuawuyts/bankai) - streaming asset compiler
|
||||||
`choo` guide
|
|
||||||
- [awesome-choo](https://github.com/YerkoPalma/awesome-choo) - Awesome things
|
|
||||||
related with choo framework
|
|
||||||
- [kaktus](https://github.com/kaktus/kaktus) - A new minimalistic web browser
|
|
||||||
- [budo](https://github.com/mattdesl/budo) - quick prototyping tool for
|
|
||||||
`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
|
||||||
@@ -747,9 +290,6 @@ $ npm install choo
|
|||||||
humans
|
humans
|
||||||
- [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for
|
- [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for
|
||||||
`browserify`
|
`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
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
Creating a quality framework takes a lot of time. Unlike others frameworks,
|
Creating a quality framework takes a lot of time. Unlike others frameworks,
|
||||||
@@ -840,8 +380,9 @@ Become a backer, and buy us a coffee (or perhaps lunch?) every month or so.
|
|||||||
[es2020]: https://github.com/yoshuawuyts/es2020
|
[es2020]: https://github.com/yoshuawuyts/es2020
|
||||||
[handbook]: https://github.com/yoshuawuyts/choo-handbook
|
[handbook]: https://github.com/yoshuawuyts/choo-handbook
|
||||||
[hyperx]: https://github.com/substack/hyperx
|
[hyperx]: https://github.com/substack/hyperx
|
||||||
[inu]: https://github.com/ahdinosaur/inu
|
|
||||||
[morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks
|
[morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks
|
||||||
[morphdom]: https://github.com/patrick-steele-idem/morphdom
|
[nanomorph]: https://github.com/yoshuawuyts/nanomorph
|
||||||
[sheet-router]: https://github.com/yoshuawuyts/sheet-router
|
[nanorouter]: https://github.com/yoshuawuyts/nanorouter
|
||||||
[yo-yo]: https://github.com/maxogden/yo-yo
|
[yo-yo]: https://github.com/maxogden/yo-yo
|
||||||
|
[yo-yoify]: https://github.com/shama/yo-yoify
|
||||||
|
[unassertify]: https://github.com/unassert-js/unassertify
|
||||||
|
|||||||
@@ -0,0 +1,350 @@
|
|||||||
|
var mutate = require('xtend/mutable')
|
||||||
|
var expose = require('choo-expose')
|
||||||
|
var css = require('sheetify')
|
||||||
|
var html = require('bel')
|
||||||
|
var choo = require('../')
|
||||||
|
|
||||||
|
css('todomvc-common/base.css')
|
||||||
|
css('todomvc-app-css/index.css')
|
||||||
|
|
||||||
|
// we export this so tests can run
|
||||||
|
if (module.parent) {
|
||||||
|
exports.todoStore = todoStore
|
||||||
|
} else {
|
||||||
|
var app = choo()
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
var persist = require('choo-persist')
|
||||||
|
var logger = require('choo-log')
|
||||||
|
app.use(persist())
|
||||||
|
app.use(logger())
|
||||||
|
}
|
||||||
|
app.use(expose())
|
||||||
|
app.use(todoStore)
|
||||||
|
|
||||||
|
app.route('/', mainView)
|
||||||
|
app.route('#active', mainView)
|
||||||
|
app.route('#completed', mainView)
|
||||||
|
app.mount('body')
|
||||||
|
}
|
||||||
|
|
||||||
|
function mainView (state, emit) {
|
||||||
|
emit('log:debug', 'Rendering main view')
|
||||||
|
return html`
|
||||||
|
<body>
|
||||||
|
<section class="todoapp">
|
||||||
|
${Header(state, emit)}
|
||||||
|
${TodoList(state, emit)}
|
||||||
|
${Footer(state, emit)}
|
||||||
|
</section>
|
||||||
|
<footer class="info">
|
||||||
|
<p>Double-click to edit a todo</p>
|
||||||
|
<p>choo by <a href="https://yoshuawuyts.com/">Yoshua Wuyts</a></p>
|
||||||
|
<p>Created by <a href="http://shuheikagawa.com">Shuhei Kagawa</a></p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function todoStore (state, emitter) {
|
||||||
|
if (!state.todos) {
|
||||||
|
state.todos = {}
|
||||||
|
|
||||||
|
state.todos.active = []
|
||||||
|
state.todos.done = []
|
||||||
|
state.todos.all = []
|
||||||
|
|
||||||
|
state.todos.idCounter = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.on('DOMContentLoaded', function () {
|
||||||
|
emitter.emit('log:debug', 'Loading todos model')
|
||||||
|
|
||||||
|
// CRUD
|
||||||
|
emitter.on('todos:create', create)
|
||||||
|
emitter.on('todos:update', update)
|
||||||
|
emitter.on('todos:delete', del)
|
||||||
|
|
||||||
|
// Shorthand
|
||||||
|
emitter.on('todos:edit', edit)
|
||||||
|
emitter.on('todos:unedit', unedit)
|
||||||
|
emitter.on('todos:toggle', toggle)
|
||||||
|
emitter.on('todos:toggleAll', toggleAll)
|
||||||
|
emitter.on('todos:deleteCompleted', deleteCompleted)
|
||||||
|
})
|
||||||
|
|
||||||
|
function create (name) {
|
||||||
|
var item = {
|
||||||
|
id: state.todos.idCounter,
|
||||||
|
editing: false,
|
||||||
|
done: false,
|
||||||
|
name: name
|
||||||
|
}
|
||||||
|
|
||||||
|
state.todos.idCounter += 1
|
||||||
|
state.todos.active.push(item)
|
||||||
|
state.todos.all.push(item)
|
||||||
|
emitter.emit('render')
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit (id) {
|
||||||
|
state.todos.all.forEach(function (todo) {
|
||||||
|
if (todo.id === id) todo.editing = true
|
||||||
|
})
|
||||||
|
emitter.emit('render')
|
||||||
|
}
|
||||||
|
|
||||||
|
function unedit (id) {
|
||||||
|
state.todos.all.forEach(function (todo) {
|
||||||
|
if (todo.id === id) todo.editing = false
|
||||||
|
})
|
||||||
|
emitter.emit('render')
|
||||||
|
}
|
||||||
|
|
||||||
|
function update (newTodo) {
|
||||||
|
var todo = state.todos.all.filter(function (todo) {
|
||||||
|
return todo.id === newTodo.id
|
||||||
|
})[0]
|
||||||
|
|
||||||
|
if (newTodo.done && todo.done === false) {
|
||||||
|
state.todos.active.splice(state.todos.active.indexOf(todo), 1)
|
||||||
|
state.todos.done.push(todo)
|
||||||
|
} else if (newTodo.done === false && todo.done) {
|
||||||
|
state.todos.done.splice(state.todos.done.indexOf(todo), 1)
|
||||||
|
state.todos.active.push(todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutate(todo, newTodo)
|
||||||
|
emitter.emit('render')
|
||||||
|
}
|
||||||
|
|
||||||
|
function del (id) {
|
||||||
|
var i = null
|
||||||
|
var todo = null
|
||||||
|
state.todos.all.forEach(function (_todo, j) {
|
||||||
|
if (_todo.id === id) {
|
||||||
|
i = j
|
||||||
|
todo = _todo
|
||||||
|
}
|
||||||
|
})
|
||||||
|
state.todos.all.splice(i, 1)
|
||||||
|
|
||||||
|
if (todo.done) {
|
||||||
|
var done = state.todos.done
|
||||||
|
var doneIndex = done[todo]
|
||||||
|
done.splice(doneIndex, 1)
|
||||||
|
} else {
|
||||||
|
var active = state.todos.active
|
||||||
|
var activeIndex = active[todo]
|
||||||
|
active.splice(activeIndex, 1)
|
||||||
|
}
|
||||||
|
emitter.emit('render')
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCompleted (data) {
|
||||||
|
var done = state.todos.done
|
||||||
|
done.forEach(function (todo) {
|
||||||
|
var index = state.todos.all.indexOf(todo)
|
||||||
|
state.todos.all.splice(index, 1)
|
||||||
|
})
|
||||||
|
state.todos.done = []
|
||||||
|
emitter.emit('render')
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle (id) {
|
||||||
|
var todo = state.todos.all.filter(function (todo) {
|
||||||
|
return todo.id === id
|
||||||
|
})[0]
|
||||||
|
var done = todo.done
|
||||||
|
todo.done = !done
|
||||||
|
var arr = done ? state.todos.done : state.todos.active
|
||||||
|
var target = done ? state.todos.active : state.todos.done
|
||||||
|
var index = arr.indexOf(todo)
|
||||||
|
arr.splice(index, 1)
|
||||||
|
target.push(todo)
|
||||||
|
emitter.emit('render')
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAll (data) {
|
||||||
|
var todos = state.todos.all
|
||||||
|
var allDone = state.todos.all.length &&
|
||||||
|
state.todos.done.length === state.todos.all.length
|
||||||
|
|
||||||
|
todos.forEach(function (todo) {
|
||||||
|
todo.done = !allDone
|
||||||
|
})
|
||||||
|
|
||||||
|
if (allDone) {
|
||||||
|
state.todos.done = []
|
||||||
|
state.todos.active = state.todos.all
|
||||||
|
} else {
|
||||||
|
state.todos.done = state.todos.all
|
||||||
|
state.todos.active = []
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.emit('render')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Footer (state, emit) {
|
||||||
|
var filter = window.location.hash.replace(/^#/, '')
|
||||||
|
var activeCount = state.todos.active.length
|
||||||
|
var hasDone = state.todos.done.length
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<footer class="footer">
|
||||||
|
<span class="todo-count">
|
||||||
|
<strong>${activeCount}</strong>
|
||||||
|
item${state.todos.all === 1 ? '' : 's'} left
|
||||||
|
</span>
|
||||||
|
<ul class="filters">
|
||||||
|
${filterButton('All', '', filter, emit)}
|
||||||
|
${filterButton('Active', 'active', filter, emit)}
|
||||||
|
${filterButton('Completed', 'completed', filter, emit)}
|
||||||
|
</ul>
|
||||||
|
${hasDone ? deleteCompleted(emit) : ''}
|
||||||
|
</footer>
|
||||||
|
`
|
||||||
|
|
||||||
|
function filterButton (name, filter, currentFilter, emit) {
|
||||||
|
var filterClass = filter === currentFilter
|
||||||
|
? 'selected'
|
||||||
|
: ''
|
||||||
|
|
||||||
|
var uri = '#' + name.toLowerCase()
|
||||||
|
if (uri === '#all') uri = '/'
|
||||||
|
return html`
|
||||||
|
<li>
|
||||||
|
<a href=${uri} class=${filterClass}>
|
||||||
|
${name}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCompleted (emit) {
|
||||||
|
return html`
|
||||||
|
<button class="clear-completed" onclick=${deleteAllCompleted}>
|
||||||
|
Clear completed
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
|
||||||
|
function deleteAllCompleted () {
|
||||||
|
emit('todos:deleteCompleted')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header (todos, emit) {
|
||||||
|
return html`
|
||||||
|
<header class="header">
|
||||||
|
<h1>todos</h1>
|
||||||
|
<input class="new-todo"
|
||||||
|
autofocus
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
onkeydown=${createTodo} />
|
||||||
|
</header>
|
||||||
|
`
|
||||||
|
|
||||||
|
function createTodo (e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
emit('todos:create', e.target.value)
|
||||||
|
e.target.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function TodoItem (todo, emit) {
|
||||||
|
return html`
|
||||||
|
<li class=${classList({ completed: todo.done, editing: todo.editing })}>
|
||||||
|
<div class="view">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle"
|
||||||
|
checked="${todo.done}"
|
||||||
|
onchange=${toggle} />
|
||||||
|
<label ondblclick=${edit}>${todo.name}</label>
|
||||||
|
<button
|
||||||
|
class="destroy"
|
||||||
|
onclick=${destroy}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="edit"
|
||||||
|
value=${todo.name}
|
||||||
|
onkeydown=${handleEditKeydown}
|
||||||
|
onblur=${update} />
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
|
||||||
|
function toggle (e) {
|
||||||
|
emit('todos:toggle', todo.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit (e) {
|
||||||
|
emit('todos:edit', todo.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy (e) {
|
||||||
|
emit('todos:delete', todo.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function update (e) {
|
||||||
|
emit('todos:update', {
|
||||||
|
id: todo.id,
|
||||||
|
editing: false,
|
||||||
|
name: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditKeydown (e) {
|
||||||
|
if (e.keyCode === 13) update(e) // Enter
|
||||||
|
else if (e.code === 27) emit('todos:unedit') // Escape
|
||||||
|
}
|
||||||
|
|
||||||
|
function classList (classes) {
|
||||||
|
var str = ''
|
||||||
|
var keys = Object.keys(classes)
|
||||||
|
for (var i = 0, len = keys.length; i < len; i++) {
|
||||||
|
var key = keys[i]
|
||||||
|
var val = classes[key]
|
||||||
|
if (val) str += (key + ' ')
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function TodoList (state, emit) {
|
||||||
|
var filter = window.location.hash.replace(/^#/, '')
|
||||||
|
var items = filter === 'completed'
|
||||||
|
? state.todos.done
|
||||||
|
: filter === 'active'
|
||||||
|
? state.todos.active
|
||||||
|
: state.todos.all
|
||||||
|
|
||||||
|
var allDone = state.todos.done.length === state.todos.all.length
|
||||||
|
|
||||||
|
var nodes = items.map(function (todo) {
|
||||||
|
return TodoItem(todo, emit)
|
||||||
|
})
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<section class="main">
|
||||||
|
<input
|
||||||
|
class="toggle-all"
|
||||||
|
type="checkbox"
|
||||||
|
checked=${allDone}
|
||||||
|
onchange=${toggleAll}/>
|
||||||
|
<label for="toggle-all" style="display: none;">
|
||||||
|
Mark all as done
|
||||||
|
</label>
|
||||||
|
<ul class="todo-list">
|
||||||
|
${nodes}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
|
||||||
|
function toggleAll () {
|
||||||
|
emit('todos:toggleAll')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "choo-todomvc-example",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"deps": "dependency-check . && dependency-check . --extra --no-dev",
|
||||||
|
"start": "bankai start --open",
|
||||||
|
"build": "bankai build --optimize",
|
||||||
|
"test": "standard && npm run deps && node test.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"choo-expose": "^1.0.0",
|
||||||
|
"sheetify": "^6.0.1",
|
||||||
|
"todomvc-app-css": "^2.0.6",
|
||||||
|
"todomvc-common": "^1.0.3",
|
||||||
|
"xtend": "^4.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"bankai": "^5.4.0",
|
||||||
|
"dependency-check": "^2.8.0",
|
||||||
|
"standard": "^9.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
var EventEmitter = require('events').EventEmitter
|
||||||
|
var spok = require('spok')
|
||||||
|
var tape = require('tape')
|
||||||
|
|
||||||
|
var example = require('./')
|
||||||
|
var todoStore = example.todoStore
|
||||||
|
|
||||||
|
tape('should initialize empty state', function (t) {
|
||||||
|
var emitter = new EventEmitter()
|
||||||
|
var state = {}
|
||||||
|
todoStore(state, emitter)
|
||||||
|
spok(t, state, {
|
||||||
|
todos: {
|
||||||
|
idCounter: 0,
|
||||||
|
active: spok.arrayElements(0),
|
||||||
|
done: spok.arrayElements(0),
|
||||||
|
all: spok.arrayElements(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.end()
|
||||||
|
})
|
||||||
|
|
||||||
|
tape('restore previous state', function (t) {
|
||||||
|
var emitter = new EventEmitter()
|
||||||
|
var state = {
|
||||||
|
todos: {
|
||||||
|
idCounter: 100,
|
||||||
|
active: [],
|
||||||
|
done: [],
|
||||||
|
all: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
todoStore(state, emitter)
|
||||||
|
spok(t, state, {
|
||||||
|
todos: {
|
||||||
|
idCounter: 100
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.end()
|
||||||
|
})
|
||||||
|
|
||||||
|
tape('todos:create', function (t) {
|
||||||
|
var emitter = new EventEmitter()
|
||||||
|
var state = {}
|
||||||
|
todoStore(state, emitter)
|
||||||
|
emitter.emit('DOMContentLoaded')
|
||||||
|
|
||||||
|
emitter.emit('todos:create', 'same as it ever was')
|
||||||
|
spok(t, state.todos, {
|
||||||
|
all: spok.arrayElements(1),
|
||||||
|
active: spok.arrayElements(1),
|
||||||
|
done: spok.arrayElements(0)
|
||||||
|
})
|
||||||
|
spok(t, state.todos.all[0], {
|
||||||
|
name: 'same as it ever was',
|
||||||
|
editing: false,
|
||||||
|
done: false,
|
||||||
|
id: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
emitter.emit('todos:create', 'and another one down')
|
||||||
|
spok(t, state.todos, {
|
||||||
|
all: spok.arrayElements(2),
|
||||||
|
active: spok.arrayElements(2),
|
||||||
|
done: spok.arrayElements(0)
|
||||||
|
})
|
||||||
|
spok(t, state.todos.all[1], {
|
||||||
|
name: 'and another one down',
|
||||||
|
editing: false,
|
||||||
|
done: false,
|
||||||
|
id: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
})
|
||||||
|
|
||||||
|
tape('todos:update', function (t) {
|
||||||
|
var emitter = new EventEmitter()
|
||||||
|
var state = {}
|
||||||
|
todoStore(state, emitter)
|
||||||
|
emitter.emit('DOMContentLoaded')
|
||||||
|
|
||||||
|
emitter.emit('todos:create', 'same as it ever was')
|
||||||
|
emitter.emit('todos:create', 'and another one down')
|
||||||
|
|
||||||
|
emitter.emit('todos:update', {
|
||||||
|
id: 0,
|
||||||
|
editing: true,
|
||||||
|
name: 'been here all along'
|
||||||
|
})
|
||||||
|
spok(t, state.todos.all[0], {
|
||||||
|
name: 'been here all along',
|
||||||
|
editing: true,
|
||||||
|
done: false,
|
||||||
|
id: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
emitter.emit('todos:update', {
|
||||||
|
done: true,
|
||||||
|
id: 1
|
||||||
|
})
|
||||||
|
spok(t, state.todos, {
|
||||||
|
all: spok.arrayElements(2),
|
||||||
|
active: spok.arrayElements(1),
|
||||||
|
done: spok.arrayElements(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
emitter.emit('todos:update', {
|
||||||
|
done: false,
|
||||||
|
id: 1
|
||||||
|
})
|
||||||
|
spok(t, state.todos, {
|
||||||
|
all: spok.arrayElements(2),
|
||||||
|
active: spok.arrayElements(2),
|
||||||
|
done: spok.arrayElements(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
})
|
||||||
|
|
||||||
|
// tape('todos:delete')
|
||||||
|
// tape('todos:edit')
|
||||||
|
// tape('todos:unedit')
|
||||||
|
// tape('todos:toggle')
|
||||||
|
// tape('todos:toggleAll')
|
||||||
|
// tape('todos:deleteCompleted')
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
const choo = require('../../')
|
|
||||||
const html = require('../../html')
|
|
||||||
const plur = require('plur')
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
app.model({
|
|
||||||
state: {
|
|
||||||
counter: 0
|
|
||||||
},
|
|
||||||
reducers: {
|
|
||||||
increment: (state, data) => ({ counter: state.counter + 1 }),
|
|
||||||
decrement: (state, data) => ({ counter: state.counter - 1 })
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
incrementAsync: function (state, data, send, done) {
|
|
||||||
setTimeout(() => send('increment', done), 1000)
|
|
||||||
},
|
|
||||||
decrementAsync: function (state, data, send, done) {
|
|
||||||
setTimeout(() => send('decrement', done), 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function mainView (state, prev, send) {
|
|
||||||
const count = state.counter
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<main class="app">
|
|
||||||
<h1>Async counter</h1>
|
|
||||||
<p>Clicked ${count} ${plur('time', count)}!</p>
|
|
||||||
<button onclick=${() => send('increment')}>Increment</button>
|
|
||||||
<button onclick=${() => send('decrement')}>Decrement</button>
|
|
||||||
<button onclick=${() => send('incrementAsync')}>Increment async</button>
|
|
||||||
<button onclick=${() => send('decrementAsync')}>Decrement async</button>
|
|
||||||
</main>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
app.router([ '/', mainView ])
|
|
||||||
|
|
||||||
const tree = app.start()
|
|
||||||
document.body.appendChild(tree)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cancellable-counter",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "client.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "bankai start --entry=client -p 8080"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "Juan Soto <juan@juansoto.me>",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"bankai": "^3.2.0",
|
|
||||||
"plur": "^2.1.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const choo = require('../../')
|
|
||||||
|
|
||||||
const mainView = require('./views/main')
|
|
||||||
|
|
||||||
const app = choo({
|
|
||||||
onError: function (err, state, createSend) {
|
|
||||||
console.trace()
|
|
||||||
console.groupCollapsed(`Error: ${err.message}`)
|
|
||||||
console.error(err)
|
|
||||||
console.groupEnd()
|
|
||||||
const send = createSend('onError: ')
|
|
||||||
send('app:error', err)
|
|
||||||
},
|
|
||||||
onAction: function (state, data, name, caller, createSend) {
|
|
||||||
console.groupCollapsed(`Action: ${caller} -> ${name}`)
|
|
||||||
console.log(data)
|
|
||||||
console.groupEnd()
|
|
||||||
},
|
|
||||||
onStateChange: function (state, data, prev, createSend) {
|
|
||||||
console.groupCollapsed('State')
|
|
||||||
console.log(prev)
|
|
||||||
console.log(state)
|
|
||||||
console.groupEnd()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.model(require('./models/error'))
|
|
||||||
app.model(require('./models/api'))
|
|
||||||
|
|
||||||
app.router(['/', mainView])
|
|
||||||
|
|
||||||
const tree = app.start()
|
|
||||||
document.body.appendChild(tree)
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
const http = require('../../../http')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
namespace: 'api',
|
|
||||||
state: {
|
|
||||||
title: 'Button pushing machine 3000'
|
|
||||||
},
|
|
||||||
reducers: {
|
|
||||||
set: (state, data) => ({ 'title': data })
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
good: function (state, data, send, done) {
|
|
||||||
request('/good', send, done)
|
|
||||||
},
|
|
||||||
bad: (state, data, send, done) => request('/bad', send, done)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function request (uri, send, done) {
|
|
||||||
http(uri, { json: true }, function (err, res, body) {
|
|
||||||
if (err) return done(new Error('HTTP error'))
|
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
const message = (body && body.message)
|
|
||||||
? body.message
|
|
||||||
: 'unknown server error'
|
|
||||||
return done(new Error(message))
|
|
||||||
}
|
|
||||||
if (!body) return done(new Error('fatal: no body received'))
|
|
||||||
send('api:set', body.message || body.title, done)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
// Create a new error. Errors are pushed into an array so they can be
|
|
||||||
// queued. We're doing a bit of timestamp trickery here so each error
|
|
||||||
// is shown for exactly one second before being dropped. Probably
|
|
||||||
// useful for real world scenarios; especially when coupled to
|
|
||||||
// fancy CSS animation thingies
|
|
||||||
|
|
||||||
// Each error is displayed 1 second
|
|
||||||
const ERROR_TIMEOUT = 1000
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
namespace: 'app',
|
|
||||||
state: {
|
|
||||||
errors: [],
|
|
||||||
errorTimeDone: 0,
|
|
||||||
triggerTime: null
|
|
||||||
},
|
|
||||||
reducers: {
|
|
||||||
setError: function (state, data) {
|
|
||||||
return {
|
|
||||||
errors: state.errors.concat(data.message),
|
|
||||||
errorTimeDone: data.errorTimeDone
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'delError': function (state, data) {
|
|
||||||
state.errors.shift()
|
|
||||||
return { errors: state.errors }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
error: function (err, state, send, done) {
|
|
||||||
const timeDone = state.errorTimeDone
|
|
||||||
const now = Date.now()
|
|
||||||
|
|
||||||
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,13 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "http",
|
|
||||||
"private": true,
|
|
||||||
"main": "client.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "NODE_ENV=development node server.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"bankai": "^2.0.2",
|
|
||||||
"browserify": "^13.0.1",
|
|
||||||
"server-router": "^2.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
const serverRouter = require('server-router')
|
|
||||||
const browserify = require('browserify')
|
|
||||||
const bankai = require('bankai')
|
|
||||||
const http = require('http')
|
|
||||||
|
|
||||||
const PORT = 8080
|
|
||||||
|
|
||||||
const server = http.createServer(createRouter())
|
|
||||||
server.listen(PORT, () => process.stdout.write(`listening on port ${PORT}\n`))
|
|
||||||
|
|
||||||
var index = 0
|
|
||||||
const errors = [
|
|
||||||
'a giant robot invaded robot town!',
|
|
||||||
'something weird crawled out of the swamp',
|
|
||||||
'oh no, the bear people invaded the server!',
|
|
||||||
'the NSA',
|
|
||||||
'12 goats started a band',
|
|
||||||
'we kinda just gave up'
|
|
||||||
]
|
|
||||||
|
|
||||||
function createRouter () {
|
|
||||||
const router = serverRouter('/404')
|
|
||||||
|
|
||||||
const html = bankai.html({ css: false })
|
|
||||||
router.on('/', (req, res) => html(req, res).pipe(res))
|
|
||||||
|
|
||||||
const js = bankai.js(browserify, require.resolve('./client.js'))
|
|
||||||
router.on('/bundle.js', (req, res) => js(req, res).pipe(res))
|
|
||||||
|
|
||||||
router.on('/good', (req, res) => {
|
|
||||||
res.end('{ "message": "all is well in robo town!" }')
|
|
||||||
})
|
|
||||||
|
|
||||||
router.on('/bad', (req, res) => {
|
|
||||||
res.statusCode = 500
|
|
||||||
res.end(`{ "message": "${errors[index++ % (errors.length)]}" }`)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.on('/404', (req, res) => {
|
|
||||||
res.statusCode = 404
|
|
||||||
res.end('{ "message": "the server is confused" }')
|
|
||||||
})
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
const html = require('../../../html')
|
|
||||||
|
|
||||||
module.exports = function (state, prev, send) {
|
|
||||||
const error = state.app.errors[0]
|
|
||||||
const title = state.api.title
|
|
||||||
return html`
|
|
||||||
<section>
|
|
||||||
<h1>${title}</h1>
|
|
||||||
<h2>Latest error: ${error}</h2>
|
|
||||||
<button onclick=${(e) => send('api:good')}>OK!</button>
|
|
||||||
<button onclick=${(e) => send('api:bad')}>Naughty</button>
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
const log = require('choo-log')
|
|
||||||
const sf = require('sheetify')
|
|
||||||
|
|
||||||
const choo = require('../../')
|
|
||||||
|
|
||||||
sf('css-wipe/dest/bundle')
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
app.use(log())
|
|
||||||
|
|
||||||
app.model(require('./models/inbox'))
|
|
||||||
app.model(require('./models/spam'))
|
|
||||||
app.model(require('./models/sent'))
|
|
||||||
|
|
||||||
app.router([
|
|
||||||
['/', require('./views/empty')],
|
|
||||||
['/:mailbox', require('./views/mailbox'), [
|
|
||||||
['/:message', require('./views/email')]
|
|
||||||
]]
|
|
||||||
])
|
|
||||||
|
|
||||||
const tree = app.start()
|
|
||||||
document.body.appendChild(tree)
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
const dateformat = require('dateformat')
|
|
||||||
const html = require('../../../html')
|
|
||||||
|
|
||||||
module.exports = function (state, prev, send) {
|
|
||||||
const params = state.params
|
|
||||||
const mailbox = params.mailbox
|
|
||||||
const messages = state[mailbox].messages
|
|
||||||
return html`
|
|
||||||
<div>
|
|
||||||
<div class="db cf w-100">
|
|
||||||
<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">From</th>
|
|
||||||
<div class="fl mb3 w-25 mt0 b">To</th>
|
|
||||||
</div>
|
|
||||||
${messages.map(function (msg) {
|
|
||||||
return createMessage(msg, mailbox)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMessage (message, mailbox) {
|
|
||||||
return html`
|
|
||||||
<div class="db cf w-100">
|
|
||||||
<a href="${'/' + mailbox + '/' + message.id}">
|
|
||||||
<div class="fl mb3 w-25 f6 link">
|
|
||||||
${dateformat(message.date, 'mmmm dS')}
|
|
||||||
</div>
|
|
||||||
<div class="fl mb3 w-25 f6 link">${message.subject}</div>
|
|
||||||
<div class="fl mb3 w-25 f6 link">${message.from}</div>
|
|
||||||
<div class="fl mb3 w-25 f6 link">${message.to}</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
const html = require('../../../html')
|
|
||||||
|
|
||||||
module.exports = function (state, prev, send) {
|
|
||||||
const params = state.params
|
|
||||||
const mailbox = params.mailbox
|
|
||||||
const message = params.message
|
|
||||||
|
|
||||||
const email = state[mailbox].messages.filter(function (msg) {
|
|
||||||
return String(msg.id) === message
|
|
||||||
})[0]
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div>
|
|
||||||
${email ? createEmail(email) : 'error: no email found'}
|
|
||||||
</div
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function createEmail (message) {
|
|
||||||
return html`
|
|
||||||
<div class="mail">
|
|
||||||
<dl>
|
|
||||||
<dt>From</dt>
|
|
||||||
<dd>${message.from}</dd>
|
|
||||||
<dt>To</dt>
|
|
||||||
<dd>${message.to}</dd>
|
|
||||||
<dt>Date</dt>
|
|
||||||
<dd>${message.date}</dd>
|
|
||||||
</dl>
|
|
||||||
<h4>${message.subject}</h4>
|
|
||||||
<p>${message.body}</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
const html = require('../../../html')
|
|
||||||
|
|
||||||
module.exports = function (state, prev, send) {
|
|
||||||
return html`
|
|
||||||
<section>
|
|
||||||
<p>Select a mailbox</p>
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
const dateformat = require('dateformat')
|
|
||||||
const html = require('../../../html')
|
|
||||||
|
|
||||||
module.exports = function () {
|
|
||||||
return function (state, prev, send) {
|
|
||||||
const params = state.params
|
|
||||||
const mailbox = params.mailbox
|
|
||||||
const message = params.message
|
|
||||||
const messages = state[mailbox].messages
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
const email = state[mailbox].messages.filter(function (msg) {
|
|
||||||
return String(msg.id) === message
|
|
||||||
})[0]
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<section class="fl mt4 w-80 db">
|
|
||||||
<div>
|
|
||||||
${createHeader()}
|
|
||||||
${messages.map(function (msg) {
|
|
||||||
return createMessage(msg, mailbox)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
${email ? createEmail(email) : 'error: no email found'}
|
|
||||||
</div
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
} else {
|
|
||||||
return html`
|
|
||||||
<section class="fl mt4 w-80 db">
|
|
||||||
${createHeader()}
|
|
||||||
${messages.map(function (msg) {
|
|
||||||
return createMessage(msg, mailbox)
|
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createHeader () {
|
|
||||||
return html`
|
|
||||||
<div class="db cf w-100">
|
|
||||||
<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">From</th>
|
|
||||||
<div class="fl mb3 w-25 mt0 b">To</th>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMessage (message, mailbox) {
|
|
||||||
return html`
|
|
||||||
<div class="db cf w-100">
|
|
||||||
<a href="${'/' + mailbox + '/' + message.id}">
|
|
||||||
<div class="fl mb3 w-25 f6 link">
|
|
||||||
${dateformat(message.date, 'mmmm dS')}
|
|
||||||
</div>
|
|
||||||
<div class="fl mb3 w-25 f6 link">${message.subject}</div>
|
|
||||||
<div class="fl mb3 w-25 f6 link">${message.from}</div>
|
|
||||||
<div class="fl mb3 w-25 f6 link">${message.to}</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function createEmail (message) {
|
|
||||||
return html`
|
|
||||||
<div class="mail">
|
|
||||||
<dl>
|
|
||||||
<dt>From</dt>
|
|
||||||
<dd>${message.from}</dd>
|
|
||||||
<dt>To</dt>
|
|
||||||
<dd>${message.to}</dd>
|
|
||||||
<dt>Date</dt>
|
|
||||||
<dd>${message.date}</dd>
|
|
||||||
</dl>
|
|
||||||
<h4>${message.subject}</h4>
|
|
||||||
<p>${message.body}</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const html = require('../../../html')
|
|
||||||
|
|
||||||
const mailboxes = [ 'inbox', 'spam', 'sent' ]
|
|
||||||
|
|
||||||
module.exports = function (state, prev, send) {
|
|
||||||
return html`
|
|
||||||
<aside class="fl mt4 w-20 db">
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<h2 class="f4 b lh0">Mailbox</h2>
|
|
||||||
</li>
|
|
||||||
${mailboxes.map(function (mailbox) {
|
|
||||||
const messages = mailbox.messages
|
|
||||||
return createLi(mailbox, messages)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function createLi (mailbox, messages) {
|
|
||||||
return html`
|
|
||||||
<li class="mt4 f6">
|
|
||||||
<a href="/${mailbox}">
|
|
||||||
${mailbox.charAt(0).toUpperCase() + mailbox.slice(1)}
|
|
||||||
<span>${messages}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
const pathname = require('pathname-match')
|
|
||||||
const html = require('../../../html')
|
|
||||||
|
|
||||||
module.exports = function (state, prev, send) {
|
|
||||||
const location = state.location.pathname
|
|
||||||
return html`
|
|
||||||
<span class="fl mt4 w-100 f4 b">
|
|
||||||
URL: ${pathname(location) || '/'}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
namespace: 'inbox',
|
|
||||||
state: {
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
subject: 'Welcome to Choo',
|
|
||||||
from: 'choochoo@choojs.com',
|
|
||||||
to: 'user@example.com',
|
|
||||||
date: new Date(),
|
|
||||||
body: 'Welcome to Choo. We hope you enjoy your stay'
|
|
||||||
}, {
|
|
||||||
id: 2,
|
|
||||||
subject: 'Great Choo Resources',
|
|
||||||
from: 'choochoo@choojs.com',
|
|
||||||
to: 'user@example.com',
|
|
||||||
date: new Date(),
|
|
||||||
body: 'Have you seen choocasts.com? How about chooaddons.com?'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
reducers: { }
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
namespace: 'sent',
|
|
||||||
state: {
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
subject: 'Should I use Choo',
|
|
||||||
from: 'user@example.com',
|
|
||||||
to: 'choochoo@choojs.com',
|
|
||||||
date: new Date(),
|
|
||||||
body: 'Choo looks pretty good, should I use it?'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
reducers: { }
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
namespace: 'spam',
|
|
||||||
state: {
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
subject: 'You have one the lottery!!!111ONEONE',
|
|
||||||
from: '419@thereallotteryhonest.com',
|
|
||||||
to: 'user@example.com',
|
|
||||||
date: new Date(),
|
|
||||||
body: 'You have ONE the lottery! You only have to send us a small amount of monies to claim your prize'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
reducers: { }
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "mailbox",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": "true",
|
|
||||||
"main": "client.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "NODE_ENV=development node server.js"
|
|
||||||
},
|
|
||||||
"author": "Yoshua Wuyts <i@yoshuawuyts.com>",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"choo-log": "^1.4.1",
|
|
||||||
"css-wipe": "^4.2.1",
|
|
||||||
"dateformat": "^1.0.12",
|
|
||||||
"pathname-match": "^1.1.3",
|
|
||||||
"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,30 +0,0 @@
|
|||||||
const serverRouter = require('server-router')
|
|
||||||
const browserify = require('browserify')
|
|
||||||
const bankai = require('bankai')
|
|
||||||
const http = require('http')
|
|
||||||
|
|
||||||
const PORT = 8080
|
|
||||||
|
|
||||||
const server = http.createServer(createRouter())
|
|
||||||
server.listen(PORT, () => process.stdout.write(`listening on port ${PORT}\n`))
|
|
||||||
|
|
||||||
function createRouter () {
|
|
||||||
const router = serverRouter('/404')
|
|
||||||
|
|
||||||
const js = bankai.js(browserify, require.resolve('./client.js'))
|
|
||||||
router.on('/bundle.js', (req, res) => js(req, res).pipe(res))
|
|
||||||
router.on('/:inbox/bundle.js', (req, res) => js(req, res).pipe(res))
|
|
||||||
|
|
||||||
const html = bankai.html({ css: false })
|
|
||||||
router.on('/', (req, res) => html(req, res).pipe(res))
|
|
||||||
router.on('/:inbox', (req, res) => html(req, res).pipe(res))
|
|
||||||
router.on('/:inbox/:message_id', (req, res) => html(req, res).pipe(res))
|
|
||||||
|
|
||||||
router.on('/hi', (req, res) => res.end('{ "message": "hi back!" }'))
|
|
||||||
router.on('/404', (req, res) => {
|
|
||||||
res.statusCode = 404
|
|
||||||
res.end('{ "message": "the server is confused" }')
|
|
||||||
})
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
const html = require('../../../html')
|
|
||||||
|
|
||||||
const emailList = require('../elements/email-list')
|
|
||||||
const pathname = require('../elements/pathname')
|
|
||||||
const email = require('../elements/email')
|
|
||||||
const nav = require('../elements/nav')
|
|
||||||
|
|
||||||
module.exports = function (state, prev, send) {
|
|
||||||
return html`
|
|
||||||
<main class="mw5 mw7-ns center cf">
|
|
||||||
${pathname(state, prev, send)}
|
|
||||||
${nav(state, prev, send)}
|
|
||||||
<section class="fl mt4 w-80 db">
|
|
||||||
${emailList(state, prev, send)}
|
|
||||||
${email(state, prev, send)}
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
const html = require('../../../html')
|
|
||||||
|
|
||||||
const empty = require('../elements/empty-mailbox')
|
|
||||||
const pathname = require('../elements/pathname')
|
|
||||||
const nav = require('../elements/nav')
|
|
||||||
|
|
||||||
module.exports = function (state, prev, send) {
|
|
||||||
return html`
|
|
||||||
<main class="mw5 mw7-ns center cf">
|
|
||||||
${pathname(state, prev, send)}
|
|
||||||
${nav(state, prev, send)}
|
|
||||||
${empty(state, prev, send)}
|
|
||||||
</main>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const html = require('../../../html')
|
|
||||||
|
|
||||||
const emailList = require('../elements/email-list')
|
|
||||||
const pathname = require('../elements/pathname')
|
|
||||||
const nav = require('../elements/nav')
|
|
||||||
|
|
||||||
module.exports = function (state, prev, send) {
|
|
||||||
return html`
|
|
||||||
<main class="mw5 mw7-ns center cf">
|
|
||||||
<button onclick=${goHome}>Home</button>
|
|
||||||
${pathname(state, prev, send)}
|
|
||||||
${nav(state, prev, send)}
|
|
||||||
<section class="fl mt4 w-80 db">
|
|
||||||
${emailList(state, prev, send)}
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
`
|
|
||||||
|
|
||||||
function goHome () {
|
|
||||||
send('location:set', { pathname: '/' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
const mount = require('../../mount')
|
|
||||||
const choo = require('../../')
|
|
||||||
|
|
||||||
const mainView = require('./view')
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
namespace: 'message',
|
|
||||||
state: {
|
|
||||||
server: 'rehydration has kicked in, server data was tossed',
|
|
||||||
client: 'hello client!'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.router(['/', mainView])
|
|
||||||
|
|
||||||
if (module.parent) {
|
|
||||||
module.exports = app
|
|
||||||
} else {
|
|
||||||
const tree = app.start()
|
|
||||||
mount('#app-root', tree)
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "http",
|
|
||||||
"private": true,
|
|
||||||
"main": "client.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "NODE_ENV=development node server.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"bankai": "^2.0.2",
|
|
||||||
"bl": "^1.1.2",
|
|
||||||
"browserify": "^13.0.1",
|
|
||||||
"hyperstream": "^1.2.2",
|
|
||||||
"server-router": "^2.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
const serverRouter = require('server-router')
|
|
||||||
const hyperstream = require('hyperstream')
|
|
||||||
const browserify = require('browserify')
|
|
||||||
const bankai = require('bankai')
|
|
||||||
const http = require('http')
|
|
||||||
|
|
||||||
const PORT = 8080
|
|
||||||
const client = require('./client')
|
|
||||||
|
|
||||||
// If an incoming request accepts "text/html", render the
|
|
||||||
// appropriate HTML. Else use the API server
|
|
||||||
const apiRouter = createRouter()
|
|
||||||
const server = http.createServer(function (req, res) {
|
|
||||||
if (/text\/html/.test(req.headers.accept)) handleHtml(req, res)
|
|
||||||
else apiRouter(req, res)
|
|
||||||
})
|
|
||||||
server.listen(PORT, () => process.stdout.write(`listening on port ${PORT}\n`))
|
|
||||||
|
|
||||||
// create a new router
|
|
||||||
// null -> fn
|
|
||||||
function createRouter () {
|
|
||||||
const apiRouter = serverRouter('/404')
|
|
||||||
|
|
||||||
apiRouter.on('/404', (req, res) => res.end('404 not found'))
|
|
||||||
apiRouter.on('/', (req, res) => res.end('nothing to be found here'))
|
|
||||||
|
|
||||||
const js = bankai.js(browserify, require.resolve('./client.js'))
|
|
||||||
apiRouter.on('/bundle.js', (req, res) => js(req, res).pipe(res))
|
|
||||||
|
|
||||||
return apiRouter
|
|
||||||
}
|
|
||||||
|
|
||||||
// render the client to string
|
|
||||||
// based on the requested url
|
|
||||||
// (obj, obj) -> null
|
|
||||||
const createIndex = bankai.html({ favicon: false, css: false })
|
|
||||||
function handleHtml (req, res) {
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
||||||
|
|
||||||
const state = { message: { server: 'hello server!' } }
|
|
||||||
const inner = client.toString(req.url, state)
|
|
||||||
const hs = hyperstream({ 'body': { _appendHtml: inner } })
|
|
||||||
|
|
||||||
createIndex(req, res).pipe(hs).pipe(res)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const assert = require('assert')
|
|
||||||
const html = require('../../html')
|
|
||||||
|
|
||||||
module.exports = function (state, prev, send) {
|
|
||||||
const serverMessage = state.message.server
|
|
||||||
const clientMessage = state.message.client
|
|
||||||
|
|
||||||
assert.equal(typeof serverMessage, 'string', 'server should be a string')
|
|
||||||
assert.equal(typeof clientMessage, 'string', 'client should be a string')
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<section id="app-root">
|
|
||||||
<h1>server message: ${serverMessage}</h1>
|
|
||||||
<h1>client message: ${clientMessage}</h1>
|
|
||||||
<p>${`
|
|
||||||
The first message is passed in by the server on compile time,
|
|
||||||
the second message was set by the client.
|
|
||||||
The more static the data you pass in, the more cachable your site
|
|
||||||
becomes (and thus performant). Try and keep the amount of properties
|
|
||||||
you pass in on the server to a minimum for most applications - it'll
|
|
||||||
make life a lot easier in the long run, hah.
|
|
||||||
`}</p>
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
const choo = require('../../')
|
|
||||||
const html = require('../../html')
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
app.model(createModel())
|
|
||||||
app.router(['/', mainView])
|
|
||||||
|
|
||||||
const tree = app.start()
|
|
||||||
document.body.appendChild(tree)
|
|
||||||
|
|
||||||
function mainView (state, prev, send) {
|
|
||||||
return html`
|
|
||||||
<div>${state.logger.msg}</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function createModel () {
|
|
||||||
const stream = new window.EventSource('/sse')
|
|
||||||
return {
|
|
||||||
namespace: 'logger',
|
|
||||||
state: {
|
|
||||||
msg: ''
|
|
||||||
},
|
|
||||||
subscriptions: [
|
|
||||||
function (send, done) {
|
|
||||||
stream.onerror = (e) => {
|
|
||||||
send('logger:error', { payload: JSON.stringify(e) }, done)
|
|
||||||
}
|
|
||||||
stream.onmessage = (e) => {
|
|
||||||
const msg = JSON.parse(e.data).message
|
|
||||||
send('logger:print', { payload: msg }, done)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
reducers: {
|
|
||||||
'print': (state, data) => {
|
|
||||||
return ({ msg: state.msg + ' ' + data.payload })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
close: (state, data, send, done) => {
|
|
||||||
stream.close()
|
|
||||||
done()
|
|
||||||
},
|
|
||||||
error: (state, data, send, done) => {
|
|
||||||
console.error(`error: ${data.payload}`)
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "http",
|
|
||||||
"private": true,
|
|
||||||
"main": "client.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "NODE_ENV=development node server.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"bankai": "^2.0.2",
|
|
||||||
"browserify": "^13.0.1",
|
|
||||||
"server-router": "^2.1.0",
|
|
||||||
"sse": "0.0.6"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
const serverRouter = require('server-router')
|
|
||||||
const browserify = require('browserify')
|
|
||||||
const bankai = require('bankai')
|
|
||||||
const http = require('http')
|
|
||||||
const SSE = require('sse')
|
|
||||||
|
|
||||||
const clients = []
|
|
||||||
const PORT = 8080
|
|
||||||
|
|
||||||
const server = http.createServer(createRouter())
|
|
||||||
server.listen(PORT, () => {
|
|
||||||
process.stdout.write(`listening on port ${PORT}\n`)
|
|
||||||
var sse = new SSE(server)
|
|
||||||
|
|
||||||
// code adapted from: https://github.com/markbrown4/server-sent-events-demo
|
|
||||||
sse.on('connection', (stream) => {
|
|
||||||
clients.push(stream)
|
|
||||||
console.log('Opened connection 🎉')
|
|
||||||
|
|
||||||
var json = JSON.stringify({ message: 'Gotcha' })
|
|
||||||
stream.send(json)
|
|
||||||
console.log('Sent: ' + json)
|
|
||||||
|
|
||||||
stream.on('close', () => {
|
|
||||||
clients.splice(clients.indexOf(stream), 1)
|
|
||||||
console.log('Closed connection 😱')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// emit data every 1.5 seconds
|
|
||||||
setInterval(() => {
|
|
||||||
var json = JSON.stringify({ message: 'Hello hello!' })
|
|
||||||
clients.forEach((stream) => {
|
|
||||||
stream.send(json)
|
|
||||||
console.log('Sent: ' + json)
|
|
||||||
})
|
|
||||||
}, 1500)
|
|
||||||
|
|
||||||
function createRouter () {
|
|
||||||
const router = serverRouter('/404')
|
|
||||||
|
|
||||||
const html = bankai.html({ css: false })
|
|
||||||
router.on('/', (req, res) => html(req, res).pipe(res))
|
|
||||||
|
|
||||||
const js = bankai.js(browserify, require.resolve('./client.js'))
|
|
||||||
router.on('/bundle.js', (req, res) => js(req, res).pipe(res))
|
|
||||||
|
|
||||||
router.on('/404', (req, res) => {
|
|
||||||
res.statusCode = 404
|
|
||||||
res.end('{ "message": "the server is confused" }')
|
|
||||||
})
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
const html = require('../../../html')
|
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
|
||||||
const error = state.app.error[0]
|
|
||||||
const title = state.api.title
|
|
||||||
return html`
|
|
||||||
<section>
|
|
||||||
<h1>${title}</h1>
|
|
||||||
<h2>Latest error: ${error}</h2>
|
|
||||||
<button onclick=${(e) => send('api:good')}>OK!</button>
|
|
||||||
<button onclick=${(e) => send('api:bad')}>Naughty</button>
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const choo = require('../../')
|
|
||||||
|
|
||||||
const stopwatch = require('./models/stopwatch')
|
|
||||||
const mainView = require('./views/main')
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
app.model(stopwatch)
|
|
||||||
|
|
||||||
app.router(['/', mainView])
|
|
||||||
|
|
||||||
const tree = app.start()
|
|
||||||
document.body.appendChild(tree)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const raf = require('raf')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
state: {
|
|
||||||
elapsed: 0,
|
|
||||||
startTime: 0,
|
|
||||||
start: false,
|
|
||||||
laps: []
|
|
||||||
},
|
|
||||||
reducers: {
|
|
||||||
start: (state, data) => ({ start: true, startTime: Date.now() - state.elapsed }),
|
|
||||||
stop: (state, data) => ({ start: false }),
|
|
||||||
update: (state, data) => ({ elapsed: data }),
|
|
||||||
reset: (state, data) => ({ startTime: Date.now(), elapsed: 0, laps: [] }),
|
|
||||||
add: (state, data) => ({ laps: state.laps.concat(data) })
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
now: (state, data, send, done) => {
|
|
||||||
if (state.start) {
|
|
||||||
let elapsed = data - state.startTime
|
|
||||||
send('update', elapsed, done)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
subscriptions: [
|
|
||||||
(send, done) => {
|
|
||||||
raf(function loop () {
|
|
||||||
send('now', Date.now(), done)
|
|
||||||
raf(loop)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "stopwatch",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "client.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "bankai start --entry=client.js -p 8080"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "traducer <traducer21@gmail.com>",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"bankai": "^3.2.0",
|
|
||||||
"raf": "^3.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"es2020": "^1.1.7"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
function formatMinutes (minutes) {
|
|
||||||
return `${minutes >= 10 ? minutes : (minutes < 10) ? '0' + minutes : '00'}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSeconds (seconds) {
|
|
||||||
return `${seconds < 10 ? '0' + seconds : seconds}`
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function format (elapsed) {
|
|
||||||
const minutes = formatMinutes(Math.floor((elapsed / 1000) / 60))
|
|
||||||
const seconds = formatSeconds(Math.floor((elapsed / 1000) % 60))
|
|
||||||
const ms = formatSeconds(Math.floor((elapsed % 1000) / 10))
|
|
||||||
|
|
||||||
return `${minutes}:${seconds}.${ms}`
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
const html = require('../../../html')
|
|
||||||
const format = require('../utilities/format')
|
|
||||||
|
|
||||||
function toggle (state, send) {
|
|
||||||
if (state.start) {
|
|
||||||
send('stop')
|
|
||||||
} else {
|
|
||||||
send('start')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = (state, prev, send) => {
|
|
||||||
const start = state.start
|
|
||||||
const elapsed = state.elapsed
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<main class="app">
|
|
||||||
<h1>stopwatch</h1>
|
|
||||||
<p>${format(elapsed)}</p>
|
|
||||||
<button
|
|
||||||
onclick=${e => toggle(state, send)}
|
|
||||||
>${start ? 'stop' : 'start'}</button>
|
|
||||||
<button
|
|
||||||
style="display:${start ? 'none' : 'inline'};"
|
|
||||||
onclick=${e => send('reset')}
|
|
||||||
>reset</button>
|
|
||||||
<button
|
|
||||||
style="display:${start ? 'inline' : 'none'};"
|
|
||||||
onclick=${e => send('add', format(elapsed))}
|
|
||||||
>lap</button>
|
|
||||||
<ol>
|
|
||||||
${state.laps.map(lap => html`<li>${lap}</li>`)}
|
|
||||||
</ol>
|
|
||||||
</main>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
const choo = require('../../')
|
|
||||||
const html = require('../../html')
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
app.model({
|
|
||||||
namespace: 'input',
|
|
||||||
state: {
|
|
||||||
title: 'my demo app'
|
|
||||||
},
|
|
||||||
reducers: {
|
|
||||||
update: (state, data) => ({ title: data.payload })
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
update: (state, data, send, done) => {
|
|
||||||
document.title = data.payload
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const mainView = (state, prev, send) => {
|
|
||||||
return html`
|
|
||||||
<main class="app">
|
|
||||||
<h1>${state.input.title}</h1>
|
|
||||||
<label>Set the title</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder=${state.input.title}
|
|
||||||
oninput=${(e) => send('input:update', { payload: e.target.value })}>
|
|
||||||
</main>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
app.router(['/', mainView])
|
|
||||||
const tree = app.start()
|
|
||||||
document.body.appendChild(tree)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "title",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "client.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "bankai start --entry=client.js -p 8080 --open"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "Yoshua Wuyts <i@yoshuawuyts.com>",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
|
||||||
"bankai": "^3.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Vanilla example</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script src="../../dist/choo.min.js"></script>
|
|
||||||
<script src="../../dist/html.min.js"></script>
|
|
||||||
<script>
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
namespace: 'counter',
|
|
||||||
state: { count: 0 },
|
|
||||||
reducers: {
|
|
||||||
increment: (state, data) => ({count: state.count + 1}),
|
|
||||||
decrement: (state, data) => ({count: state.count - 1})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const mainView = (state, prev, send) => {
|
|
||||||
return html`
|
|
||||||
<main class="app">
|
|
||||||
<h1>Counter example</h1>
|
|
||||||
<div>
|
|
||||||
Count: ${state.counter.count}
|
|
||||||
<button onclick=${(e) => send('counter:increment')}>+</button>
|
|
||||||
<button onclick=${(e) => send('counter:decrement')}>-</button>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
app.router(['/', mainView])
|
|
||||||
const tree = app.start()
|
|
||||||
document.body.appendChild(tree)
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1 +1 @@
|
|||||||
module.exports = require('yo-yo')
|
module.exports = require('bel')
|
||||||
|
|||||||
@@ -1,216 +1,120 @@
|
|||||||
const createLocation = require('sheet-router/create-location')
|
var documentReady = require('document-ready')
|
||||||
const onHistoryChange = require('sheet-router/history')
|
var nanorouter = require('nanorouter')
|
||||||
const sheetRouter = require('sheet-router')
|
var nanomount = require('nanomount')
|
||||||
const onHref = require('sheet-router/href')
|
var nanomorph = require('nanomorph')
|
||||||
const walk = require('sheet-router/walk')
|
var nanoraf = require('nanoraf')
|
||||||
const mutate = require('xtend/mutable')
|
var nanobus = require('nanobus')
|
||||||
const barracks = require('barracks')
|
var assert = require('assert')
|
||||||
const nanoraf = require('nanoraf')
|
|
||||||
const assert = require('assert')
|
|
||||||
const xtend = require('xtend')
|
|
||||||
const yo = require('yo-yo')
|
|
||||||
|
|
||||||
module.exports = choo
|
var onHistoryChange = require('./lib/history')
|
||||||
|
var onHref = require('./lib/href')
|
||||||
|
|
||||||
// framework for creating sturdy web applications
|
module.exports = Framework
|
||||||
// null -> fn
|
|
||||||
function choo (opts) {
|
function Framework (opts) {
|
||||||
opts = opts || {}
|
opts = opts || {}
|
||||||
|
|
||||||
const _store = start._store = barracks()
|
var routerOpts = {
|
||||||
var _router = start._router = null
|
default: opts.defaultRoute || '/404',
|
||||||
var _routerOpts = null
|
curry: true
|
||||||
var _rootNode = null
|
|
||||||
var _routes = null
|
|
||||||
var _frame = null
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
_store.use({ onStateChange: render })
|
|
||||||
}
|
|
||||||
_store.use(opts)
|
|
||||||
|
|
||||||
start.toString = toString
|
|
||||||
start.router = router
|
|
||||||
start.model = model
|
|
||||||
start.start = start
|
|
||||||
start.stop = _store.stop
|
|
||||||
start.use = use
|
|
||||||
|
|
||||||
return start
|
|
||||||
|
|
||||||
// render the application to a string
|
|
||||||
// (str, obj) -> str
|
|
||||||
function toString (route, serverState) {
|
|
||||||
serverState = serverState || {}
|
|
||||||
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 })
|
|
||||||
|
|
||||||
const state = _store.state({ state: serverState })
|
|
||||||
const router = createRouter(_routerOpts, _routes, createSend)
|
|
||||||
const tree = router(route, state)
|
|
||||||
return tree.outerHTML || tree.toString()
|
|
||||||
|
|
||||||
function createSend () {
|
|
||||||
return function send () {
|
|
||||||
assert.ok(false, 'choo: send() cannot be called from Node')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the application
|
var router = nanorouter(routerOpts)
|
||||||
// (str?, obj?) -> DOMNode
|
var bus = nanobus()
|
||||||
function start () {
|
var rerender = null
|
||||||
_store.model(createLocationModel(opts))
|
var tree = null
|
||||||
const createSend = _store.start(opts)
|
var state = {}
|
||||||
_router = start._router = createRouter(_routerOpts, _routes, createSend)
|
|
||||||
const state = _store.state({state: {}})
|
|
||||||
|
|
||||||
const tree = _router(state.location.href, state)
|
return {
|
||||||
assert.ok(tree, 'choo.start: the router should always return a valid DOM node')
|
toString: toString,
|
||||||
assert.equal(typeof tree, 'object', 'choo.start: the router should always return a valid DOM node')
|
use: register,
|
||||||
_rootNode = tree
|
mount: mount,
|
||||||
tree.done = done
|
route: route,
|
||||||
|
start: start
|
||||||
return tree
|
|
||||||
|
|
||||||
// allow a 'mount' function to return the new node
|
|
||||||
// html -> null
|
|
||||||
function done (newNode) {
|
|
||||||
_rootNode = newNode
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the DOM after every state mutation
|
function route (route, handler) {
|
||||||
// (obj, obj, obj, str, fn) -> null
|
router.on(route, function (params) {
|
||||||
function render (state, data, prev, name, createSend) {
|
return function () {
|
||||||
if (!_frame) {
|
state.params = params
|
||||||
_frame = nanoraf(function (state, prev) {
|
return handler(state, emit)
|
||||||
const newTree = _router(state.location.href, state, prev)
|
}
|
||||||
_rootNode = yo.update(_rootNode, newTree)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_frame(state, prev)
|
|
||||||
|
function register (cb) {
|
||||||
|
cb(state, bus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// register all routes on the router
|
function start () {
|
||||||
// (str?, [fn|[fn]]) -> obj
|
tree = router(createLocation(), state, emit)
|
||||||
function router (defaultRoute, routes) {
|
rerender = nanoraf(function () {
|
||||||
_routerOpts = defaultRoute
|
var newTree = router(createLocation(), state, emit)
|
||||||
_routes = routes
|
tree = nanomorph(tree, newTree)
|
||||||
}
|
})
|
||||||
|
|
||||||
// create a new model
|
bus.on('render', rerender)
|
||||||
// (str?, obj) -> null
|
|
||||||
function model (model) {
|
|
||||||
_store.model(model)
|
|
||||||
}
|
|
||||||
|
|
||||||
// register a plugin
|
|
||||||
// (obj) -> null
|
|
||||||
function use (hooks) {
|
|
||||||
assert.equal(typeof hooks, 'object', 'choo.use: hooks should be an object')
|
|
||||||
_store.use(hooks)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new router with a custom `createRoute()` function
|
|
||||||
// (str?, obj) -> null
|
|
||||||
function createRouter (routerOpts, routes, createSend) {
|
|
||||||
var prev = null
|
|
||||||
if (!routes) {
|
|
||||||
routes = routerOpts
|
|
||||||
routerOpts = {}
|
|
||||||
}
|
|
||||||
routerOpts = mutate({ thunk: 'match' }, routerOpts)
|
|
||||||
const router = sheetRouter(routerOpts, routes)
|
|
||||||
walk(router, wrap)
|
|
||||||
|
|
||||||
return router
|
|
||||||
|
|
||||||
function wrap (route, handler) {
|
|
||||||
const send = createSend('view: ' + route, true)
|
|
||||||
return function chooWrap (params) {
|
|
||||||
return function (state) {
|
|
||||||
// TODO(yw): find a way to wrap handlers so params shows up in state
|
|
||||||
const nwState = xtend(state)
|
|
||||||
nwState.location = xtend(nwState.location, { params: params })
|
|
||||||
|
|
||||||
const nwPrev = prev
|
|
||||||
prev = nwState // save for next time
|
|
||||||
|
|
||||||
if (opts.freeze !== false) Object.freeze(nwState)
|
|
||||||
return handler(nwState, nwPrev, send)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// application location model
|
|
||||||
// obj -> obj
|
|
||||||
function createLocationModel (opts) {
|
|
||||||
return {
|
|
||||||
namespace: 'location',
|
|
||||||
state: mutate(createLocation(), { params: {} }),
|
|
||||||
subscriptions: createSubscriptions(opts),
|
|
||||||
effects: { set: setLocation, touch: touchLocation },
|
|
||||||
reducers: { update: updateLocation }
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the location on the state
|
|
||||||
// try and jump to an anchor on the page if it exists
|
|
||||||
// (obj, obj) -> obj
|
|
||||||
function updateLocation (state, data) {
|
|
||||||
if (opts.history !== false && data.hash && data.hash !== state.hash) {
|
|
||||||
try {
|
|
||||||
const el = document.querySelector(data.hash)
|
|
||||||
if (el) el.scrollIntoView(true)
|
|
||||||
} catch (e) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// update internal location only
|
|
||||||
// (str, obj, fn, fn) -> null
|
|
||||||
function touchLocation (state, data, send, done) {
|
|
||||||
const newLocation = createLocation(state, data)
|
|
||||||
send('location:update', newLocation, done)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set a new location e.g. "/foo/bar#baz?beep=boop"
|
|
||||||
// (str, obj, fn, fn) -> null
|
|
||||||
function setLocation (state, data, send, done) {
|
|
||||||
const newLocation = createLocation(state, data)
|
|
||||||
|
|
||||||
// update url bar if it changed
|
|
||||||
if (opts.history !== false && newLocation.href !== state.href) {
|
|
||||||
window.history.pushState({}, null, newLocation.href)
|
|
||||||
}
|
|
||||||
|
|
||||||
send('location:update', newLocation, done)
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSubscriptions (opts) {
|
|
||||||
const subs = {}
|
|
||||||
|
|
||||||
if (opts.history !== false) {
|
if (opts.history !== false) {
|
||||||
subs.handleHistory = function (send, done) {
|
onHistoryChange(function (href) {
|
||||||
onHistoryChange(function navigate (href) {
|
bus.emit('pushState', window.location.href)
|
||||||
send('location:touch', href, done)
|
scrollIntoView()
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.href !== false) {
|
if (opts.href !== false) {
|
||||||
subs.handleHref = function (send, done) {
|
onHref(function (location) {
|
||||||
onHref(function navigate (location) {
|
var href = location.href
|
||||||
send('location:set', location, done)
|
var currHref = window.location.href
|
||||||
|
if (href === currHref) return
|
||||||
|
window.history.pushState({}, null, href)
|
||||||
|
bus.emit('pushState', window.location.href)
|
||||||
|
bus.emit('render')
|
||||||
|
scrollIntoView()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return subs
|
documentReady(function () {
|
||||||
|
bus.emit('DOMContentLoaded')
|
||||||
|
})
|
||||||
|
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
function emit (eventName, data) {
|
||||||
|
bus.emit(eventName, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function mount (selector) {
|
||||||
|
var newTree = start()
|
||||||
|
documentReady(function () {
|
||||||
|
var root = document.querySelector(selector)
|
||||||
|
assert.ok(root, 'could not query selector: ' + selector)
|
||||||
|
nanomount(root, newTree)
|
||||||
|
tree = root
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toString (location, _state) {
|
||||||
|
state = _state || {}
|
||||||
|
var html = router(location)
|
||||||
|
assert.equal()
|
||||||
|
return html.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollIntoView () {
|
||||||
|
var hash = window.location.hash
|
||||||
|
if (hash) {
|
||||||
|
try {
|
||||||
|
var el = document.querySelector(hash)
|
||||||
|
if (el) el.scrollIntoView(true)
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLocation () {
|
||||||
|
var pathname = window.location.pathname.replace(/\/$/, '')
|
||||||
|
var hash = window.location.hash.replace(/^#/, '/')
|
||||||
|
return pathname + hash
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
var document = require('global/document')
|
||||||
|
var window = require('global/window')
|
||||||
|
var assert = require('assert')
|
||||||
|
|
||||||
|
module.exports = history
|
||||||
|
|
||||||
|
// listen to html5 pushstate events
|
||||||
|
// and update router accordingly
|
||||||
|
// fn(str) -> null
|
||||||
|
function history (cb) {
|
||||||
|
assert.equal(typeof cb, 'function', 'sheet-router/history: cb must be a function')
|
||||||
|
window.onpopstate = function () {
|
||||||
|
cb({
|
||||||
|
pathname: document.location.pathname,
|
||||||
|
search: document.location.search,
|
||||||
|
href: document.location.href,
|
||||||
|
hash: document.location.hash
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
var window = require('global/window')
|
||||||
|
var assert = require('assert')
|
||||||
|
|
||||||
|
module.exports = href
|
||||||
|
|
||||||
|
var noRoutingAttrName = 'data-no-routing'
|
||||||
|
|
||||||
|
// handle a click if is anchor tag with an href
|
||||||
|
// and url lives on the same domain. Replaces
|
||||||
|
// trailing '#' so empty links work as expected.
|
||||||
|
// (fn(str), obj?) -> undefined
|
||||||
|
function href (cb, root) {
|
||||||
|
assert.equal(typeof cb, 'function', 'sheet-router/href: cb must be a function')
|
||||||
|
|
||||||
|
window.onclick = function (e) {
|
||||||
|
if ((e.button && e.button !== 0) || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return
|
||||||
|
|
||||||
|
var node = (function traverse (node) {
|
||||||
|
if (!node || node === root) return
|
||||||
|
if (node.localName !== 'a') return traverse(node.parentNode)
|
||||||
|
if (node.href === undefined) return traverse(node.parentNode)
|
||||||
|
if (window.location.host !== node.host) return traverse(node.parentNode)
|
||||||
|
return node
|
||||||
|
})(e.target)
|
||||||
|
|
||||||
|
if (!node) return
|
||||||
|
|
||||||
|
var isRoutingDisabled = node.hasAttribute(noRoutingAttrName)
|
||||||
|
if (isRoutingDisabled) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
cb(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// mount.js
|
|
||||||
const documentReady = require('document-ready')
|
|
||||||
const assert = require('assert')
|
|
||||||
const yo = require('yo-yo')
|
|
||||||
|
|
||||||
module.exports = mount
|
|
||||||
|
|
||||||
// (str, html) -> null
|
|
||||||
function mount (selector, newTree) {
|
|
||||||
assert.equal(typeof selector, 'string', 'choo/mount: selector should be a string')
|
|
||||||
assert.equal(typeof newTree, 'object', 'choo/mount: newTree should be an object')
|
|
||||||
|
|
||||||
const done = newTree.done
|
|
||||||
|
|
||||||
documentReady(function onReady () {
|
|
||||||
const _rootNode = document.querySelector(selector)
|
|
||||||
assert.ok(_rootNode, 'could not query selector: ' + selector)
|
|
||||||
|
|
||||||
// copy script tags from the old newTree to the new newTree so
|
|
||||||
// we can pass a <body> element straight up
|
|
||||||
if (_rootNode.nodeName === 'BODY') {
|
|
||||||
const children = _rootNode.childNodes
|
|
||||||
for (var i = 0; i < children.length; i++) {
|
|
||||||
if (children[i].nodeName === 'SCRIPT') {
|
|
||||||
newTree.appendChild(children[i].cloneNode(true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newNode = yo.update(_rootNode, newTree)
|
|
||||||
assert.equal(newNode, _rootNode, 'choo/mount: The DOM node: \n' +
|
|
||||||
newNode.outerHTML + '\n is not equal to \n' + newTree.outerHTML +
|
|
||||||
'choo cannot begin diffing.' +
|
|
||||||
' Make sure the same initial tree is rendered in the browser' +
|
|
||||||
' as on the server. Check out the choo handbook for more information')
|
|
||||||
|
|
||||||
// pass the node we mounted on back into choo
|
|
||||||
if (done) done(newNode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+19
-59
@@ -1,23 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "choo",
|
"name": "choo",
|
||||||
"version": "4.1.0",
|
"version": "5.0.0-5",
|
||||||
"description": "A 5kb framework for creating sturdy frontend applications",
|
"description": "A 5kb framework for creating sturdy frontend applications",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deps": "./scripts/test deps",
|
"deps": "dependency-check --entry ./html.js . && dependency-check . --extra --no-dev --entry ./html.js",
|
||||||
"test:electron": "./scripts/test electron",
|
"start": "bankai start example --open",
|
||||||
"test:cov": "./scripts/test cov",
|
"test": "standard && npm run deps && node test.js",
|
||||||
"test:server": "./scripts/test server",
|
"build:dev": "echo 'to be implemented'",
|
||||||
"test:browser": "./scripts/test browser",
|
"build:min": "echo 'to be implemented'",
|
||||||
"test:browser-local": "./scripts/test browser-local",
|
"prepublish": "npm run build:dev && npm run build:min"
|
||||||
"preversion": "if [ ! -z $SKIP_TEST ]; then npm run test:browser; fi",
|
|
||||||
"test": "npm run test:electron",
|
|
||||||
"build:dev": "./scripts/build dev",
|
|
||||||
"build:min": "./scripts/build min",
|
|
||||||
"prepublish": "npm run build:dev && npm run build:min",
|
|
||||||
"instrument:discify": "./scripts/instrument discify",
|
|
||||||
"instrument:minified": "./scripts/instrument minified",
|
|
||||||
"instrument:gzip": "./scripts/instrument gzip"
|
|
||||||
},
|
},
|
||||||
"repository": "yoshuawuyts/choo",
|
"repository": "yoshuawuyts/choo",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -28,53 +20,21 @@
|
|||||||
"composable",
|
"composable",
|
||||||
"tiny"
|
"tiny"
|
||||||
],
|
],
|
||||||
"files": [
|
|
||||||
"index.js",
|
|
||||||
"mount.js",
|
|
||||||
"html.js",
|
|
||||||
"dist/"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"barracks": "^9.1.0",
|
"bel": "^4.5.1",
|
||||||
"document-ready": "~1.0.2",
|
"document-ready": "^1.0.3",
|
||||||
"global": "^4.3.0",
|
"global": "^4.3.1",
|
||||||
"nanoraf": "^2.1.1",
|
"nanobus": "^2.1.0",
|
||||||
"sheet-router": "^4.0.1",
|
"nanomorph": "^4.0.0",
|
||||||
"xtend": "^4.0.1",
|
"nanomount": "^1.0.0",
|
||||||
"yo-yo": "^1.2.2"
|
"nanoraf": "^3.0.0",
|
||||||
|
"nanorouter": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"append-child": "~1.0.0",
|
"dependency-check": "^2.8.0",
|
||||||
"bankai": "^3.1.0",
|
"spok": "^0.7.0",
|
||||||
"browserify": "^13.0.1",
|
"standard": "^9.0.1",
|
||||||
"browserify-istanbul": "^2.0.0",
|
"tape": "^4.6.3"
|
||||||
"bundle-collapser": "^1.2.1",
|
|
||||||
"dependency-check": "^2.5.1",
|
|
||||||
"disc": "^1.3.2",
|
|
||||||
"envify": "^3.4.1",
|
|
||||||
"es2020": "^1.0.1",
|
|
||||||
"geval": "~2.1.1",
|
|
||||||
"gzip-size-cli": "^1.0.0",
|
|
||||||
"insert-css": "^1.0.0",
|
|
||||||
"istanbul": "^0.4.4",
|
|
||||||
"min-document": "~2.19.0",
|
|
||||||
"pretty-bytes-cli": "^2.0.0",
|
|
||||||
"proxyquire": "~1.7.10",
|
|
||||||
"proxyquire-universal": "~1.0.8",
|
|
||||||
"proxyquireify": "~3.2.0",
|
|
||||||
"server-router": "^3.0.0",
|
|
||||||
"sheetify": "^5.1.0",
|
|
||||||
"standard": "^8.0.0",
|
|
||||||
"standard-markdown": "^2.2.0",
|
|
||||||
"tachyons": "^4.0.0-beta.19",
|
|
||||||
"tape": "^4.5.1",
|
|
||||||
"tape-istanbul": "~1.0.2",
|
|
||||||
"tape-run": "~2.1.4",
|
|
||||||
"uglifyify": "^3.0.2",
|
|
||||||
"uglifyjs": "^2.4.10",
|
|
||||||
"unassertify": "^2.0.3",
|
|
||||||
"yo-yoify": "^3.1.0",
|
|
||||||
"zuul": "toddself/zuul"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
usage () {
|
|
||||||
printf "Usage: build-umd <argument>\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
# use zopfli for better compression if available
|
|
||||||
gzip () {
|
|
||||||
zopfli -h 2>/dev/null
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
zopfli "$1" -i1000 -c
|
|
||||||
else
|
|
||||||
gzip -c "$1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
build_dev () {
|
|
||||||
mkdir -p dist/
|
|
||||||
NODE_ENV=development browserify index.js \
|
|
||||||
--standalone=choo \
|
|
||||||
-t envify \
|
|
||||||
-g yo-yoify \
|
|
||||||
-g es2020 \
|
|
||||||
> dist/choo.js
|
|
||||||
NODE_ENV=development browserify html.js \
|
|
||||||
--standalone=html \
|
|
||||||
> dist/html.js
|
|
||||||
}
|
|
||||||
|
|
||||||
build_min () {
|
|
||||||
mkdir -p dist/
|
|
||||||
NODE_ENV=production browserify index.js \
|
|
||||||
--standalone=choo \
|
|
||||||
-t envify \
|
|
||||||
-g unassertify \
|
|
||||||
-g yo-yoify \
|
|
||||||
-g es2020 \
|
|
||||||
-g uglifyify \
|
|
||||||
-t envify \
|
|
||||||
-p bundle-collapser/plugin \
|
|
||||||
| uglifyjs \
|
|
||||||
> dist/choo.min.js
|
|
||||||
NODE_ENV=production browserify html.js \
|
|
||||||
--standalone=html \
|
|
||||||
> dist/html.min.js
|
|
||||||
}
|
|
||||||
|
|
||||||
build_gz () {
|
|
||||||
build_min
|
|
||||||
gzip dist/choo.min.js > dist/choo.min.js.gz
|
|
||||||
cp dist/choo.min.js.gz dist/choo.gz
|
|
||||||
}
|
|
||||||
|
|
||||||
# parse CLI flags
|
|
||||||
while true; do
|
|
||||||
case "$1" in
|
|
||||||
-h|--help) usage && exit 1 ;;
|
|
||||||
-- ) shift; break ;;
|
|
||||||
* ) break ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
d|dev) shift; build_dev "$@" ;;
|
|
||||||
m|min) shift; build_min "$@" ;;
|
|
||||||
g|gzip) shift; build_gz "$@" ;;
|
|
||||||
*) shift; build_min "$@" ;;
|
|
||||||
esac
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
usage () {
|
|
||||||
cat << USAGE
|
|
||||||
script/test-size
|
|
||||||
Commands:
|
|
||||||
g, gzip (default) Output gzip size
|
|
||||||
m, minified Output size minified
|
|
||||||
d, discify Run discify
|
|
||||||
USAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
# use zopfli for better compression if available
|
|
||||||
gzip () {
|
|
||||||
zopfli -h 2>/dev/null
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
zopfli "$1" -i1000 -c | wc -c
|
|
||||||
else
|
|
||||||
gzip-size < "$1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
gzip_size () {
|
|
||||||
mkdir -p tmp/
|
|
||||||
browserify index.js \
|
|
||||||
-g unassertify \
|
|
||||||
-g yo-yoify \
|
|
||||||
-g es2020 \
|
|
||||||
-g uglifyify \
|
|
||||||
-p bundle-collapser/plugin \
|
|
||||||
| uglifyjs \
|
|
||||||
> tmp/bundle.min.js
|
|
||||||
|
|
||||||
gzip tmp/bundle.min.js | pretty-bytes
|
|
||||||
rm -rf tmp/
|
|
||||||
}
|
|
||||||
|
|
||||||
min_size () {
|
|
||||||
browserify index.js \
|
|
||||||
-g unassertify \
|
|
||||||
-g yo-yoify \
|
|
||||||
-g es2020 \
|
|
||||||
-g uglifyify \
|
|
||||||
-p bundle-collapser/plugin \
|
|
||||||
| uglifyjs \
|
|
||||||
| wc -c \
|
|
||||||
| pretty-bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
run_discify () {
|
|
||||||
browserify index.js --full-paths \
|
|
||||||
-g unassertify \
|
|
||||||
-g yo-yoify \
|
|
||||||
-g es2020 \
|
|
||||||
-g uglifyify \
|
|
||||||
| uglifyjs \
|
|
||||||
| discify --open
|
|
||||||
}
|
|
||||||
|
|
||||||
# set CLI flags
|
|
||||||
getopt -T > /dev/null
|
|
||||||
if [ "$?" -eq 4 ]; then
|
|
||||||
args="$(getopt --long help discify minified --options hmdg -- "$*")"
|
|
||||||
else
|
|
||||||
args="$(getopt h "$*")";
|
|
||||||
fi
|
|
||||||
[ ! $? -eq 0 ] && { usage && exit 2; }
|
|
||||||
eval set -- "$args"
|
|
||||||
|
|
||||||
# parse CLI flags
|
|
||||||
while true; do
|
|
||||||
case "$1" in
|
|
||||||
-h|--help) usage && exit 1 ;;
|
|
||||||
-- ) shift; break ;;
|
|
||||||
* ) break ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
d|discify) shift; run_discify "$@" ;;
|
|
||||||
m|minified) shift; printf "min: %s\n" "$(min_size)" "$@" ;;
|
|
||||||
g|gzip|*) shift; printf "gzip: %s\n" "$(gzip_size)" "$@" ;;
|
|
||||||
esac
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# setting exit because we're linting and need to exit on failure
|
|
||||||
set -e -o pipefail
|
|
||||||
|
|
||||||
usage () {
|
|
||||||
printf "Usage: test\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
lint () {
|
|
||||||
standard > /dev/tty
|
|
||||||
standard-markdown > /dev/tty
|
|
||||||
}
|
|
||||||
|
|
||||||
test_electron () {
|
|
||||||
check_deps
|
|
||||||
lint
|
|
||||||
browserify tests/**/*.js \
|
|
||||||
-t es2020 \
|
|
||||||
-p proxyquire-universal \
|
|
||||||
| tape-run
|
|
||||||
}
|
|
||||||
|
|
||||||
test_cov () {
|
|
||||||
check_deps
|
|
||||||
lint
|
|
||||||
browserify tests/**/*.js \
|
|
||||||
-t es2020 \
|
|
||||||
-p proxyquire-universal \
|
|
||||||
-p tape-istanbul/plugin \
|
|
||||||
| tape-run \
|
|
||||||
| tape-istanbul \
|
|
||||||
&& istanbul report
|
|
||||||
}
|
|
||||||
|
|
||||||
test_server () {
|
|
||||||
lint
|
|
||||||
standard
|
|
||||||
check_deps
|
|
||||||
NODE_ENV=test node tests/server/*
|
|
||||||
}
|
|
||||||
|
|
||||||
test_browser () {
|
|
||||||
NODE_ENV=test zuul tests/browser/*
|
|
||||||
}
|
|
||||||
|
|
||||||
test_browser_local () {
|
|
||||||
lint
|
|
||||||
check_deps
|
|
||||||
NODE_ENV=test zuul --local 8080 -- tests/browser/*
|
|
||||||
}
|
|
||||||
|
|
||||||
check_deps () {
|
|
||||||
dependency-check . --entry 'index.js'
|
|
||||||
dependency-check . --entry 'index.js' --extra --no-dev \
|
|
||||||
-i document-ready \
|
|
||||||
-i global
|
|
||||||
}
|
|
||||||
|
|
||||||
# set CLI flags
|
|
||||||
# parse CLI flags
|
|
||||||
while true; do
|
|
||||||
case "$1" in
|
|
||||||
-h|--help) usage && exit 1 ;;
|
|
||||||
-- ) shift; break ;;
|
|
||||||
* ) break ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
e|electron) shift; test_electron "$@";;
|
|
||||||
b|browser) shift; test_browser "$@";;
|
|
||||||
l|browser-local) shift; test_browser_local "$@";;
|
|
||||||
s|server) shift; test_server "$@";;
|
|
||||||
lint) shift; lint "$@";;
|
|
||||||
c|cov) shift; test_cov "$@";;
|
|
||||||
d|deps) shift; check_deps "$@";;
|
|
||||||
esac
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
const append = require('append-child')
|
|
||||||
const test = require('tape')
|
|
||||||
|
|
||||||
const choo = require('../../')
|
|
||||||
const html = require('../../html')
|
|
||||||
|
|
||||||
test('state is immutable', function (t) {
|
|
||||||
t.plan(4)
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
const state = {
|
|
||||||
foo: 'baz',
|
|
||||||
beep: 'boop'
|
|
||||||
}
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
state: state,
|
|
||||||
namespace: 'test',
|
|
||||||
reducers: {
|
|
||||||
'no-reducer-mutate': (state, data) => {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
'mutate-on-return': (state, data) => {
|
|
||||||
delete data.type
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
'triggers-reducers': (state, data, send, done) => {
|
|
||||||
send('test:mutate-on-return', {beep: 'barp'}, done)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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([
|
|
||||||
['/', function (state, prev, send) {
|
|
||||||
++loop
|
|
||||||
asserts[loop] && asserts[loop](state.test)
|
|
||||||
setTimeout(() => triggers[loop] && triggers[loop](send), 5)
|
|
||||||
return html`
|
|
||||||
<div><span class="test">${state.foo}:${state.beep}</span></div>
|
|
||||||
`
|
|
||||||
}]
|
|
||||||
])
|
|
||||||
|
|
||||||
const tree = app.start()
|
|
||||||
t.on('end', append(tree))
|
|
||||||
})
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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(['/', function (state) {
|
|
||||||
// Modifying frozen objects can lead to TypeError
|
|
||||||
try {
|
|
||||||
state.foo = ''
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
t.equal(state.foo, 'bar', 'cannot modify property')
|
|
||||||
// Modifying frozen objects can lead to TypeError
|
|
||||||
try {
|
|
||||||
state.bar = 'baz'
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
t.equal(state.bar, undefined, 'cannot add property')
|
|
||||||
return document.createElement('div')
|
|
||||||
}])
|
|
||||||
|
|
||||||
app.start()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('noFreeze', function (t) {
|
|
||||||
t.plan(2)
|
|
||||||
const app = choo({freeze: false})
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
state: {
|
|
||||||
foo: 'bar'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.router(['/', function (state) {
|
|
||||||
state.foo = ''
|
|
||||||
t.equal(state.foo, '', 'can modify property')
|
|
||||||
state.bar = 'baz'
|
|
||||||
t.equal(state.bar, 'baz', 'can add property')
|
|
||||||
return document.createElement('div')
|
|
||||||
}])
|
|
||||||
|
|
||||||
app.start()
|
|
||||||
})
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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 (state, data, name, caller, createSend) {
|
|
||||||
if (name === 'explodes') return
|
|
||||||
t.deepEqual(data, {foo: 'bar'}, 'onAction: action data')
|
|
||||||
t.equal(state.clicks, 0, 'onAction: current state: 0 clicks')
|
|
||||||
t.equal(name, 'click', 'onAction: data name')
|
|
||||||
t.equal(caller, 'view: /', 'onAction: caller name')
|
|
||||||
t.equal(typeof createSend, 'function', 'onAction: createSend fn')
|
|
||||||
},
|
|
||||||
onStateChange: function (state, data, prev, createSend) {
|
|
||||||
t.deepEqual(data, {foo: 'bar'}, 'onState: action data')
|
|
||||||
t.deepEqual(state.clicks, 1, 'onState: new state: 1 clicks')
|
|
||||||
t.deepEqual(prev.clicks, 0, 'onState: prev state: 0 clicks')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
state: {
|
|
||||||
clicks: 0
|
|
||||||
},
|
|
||||||
reducers: {
|
|
||||||
click: (state, data) => ({clicks: state.clicks + 1})
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
explodes: (state, data, send, done) => {
|
|
||||||
setTimeout(() => done(new Error('effect error')), 5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var sent = false
|
|
||||||
app.router(['/', function (state, prev, send) {
|
|
||||||
if (!sent) {
|
|
||||||
send('click', {foo: 'bar'})
|
|
||||||
send('explodes')
|
|
||||||
}
|
|
||||||
sent = true
|
|
||||||
return view`<span></span>`
|
|
||||||
}])
|
|
||||||
|
|
||||||
const tree = app.start()
|
|
||||||
t.on('end', append(tree))
|
|
||||||
})
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
const test = require('tape')
|
|
||||||
const onReady = require('document-ready')
|
|
||||||
const append = require('append-child')
|
|
||||||
const mount = require('../../mount')
|
|
||||||
const choo = require('../../')
|
|
||||||
const html = require('../../html')
|
|
||||||
|
|
||||||
test('rehydration', function (t) {
|
|
||||||
t.plan(2)
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
const node = html`
|
|
||||||
<section id="app-root">
|
|
||||||
<div>Hello squirrel!</span>
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
|
|
||||||
app.router(['/', function (state, prev, send) {
|
|
||||||
return html`
|
|
||||||
<section id="app-root">
|
|
||||||
<div onclick=${() => send('test')}>Hello world!</span>
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
}])
|
|
||||||
|
|
||||||
append(node)
|
|
||||||
|
|
||||||
const tree = app.start()
|
|
||||||
mount('#app-root', tree)
|
|
||||||
|
|
||||||
onReady(function () {
|
|
||||||
const newNode = document.querySelector('#app-root')
|
|
||||||
const el = newNode.children[0]
|
|
||||||
t.equal(el.innerHTML, 'Hello world!', 'same as it ever was')
|
|
||||||
t.equal(typeof el.onclick, 'function', 'attaches dom listeners')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
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(2)
|
|
||||||
|
|
||||||
const history = Event()
|
|
||||||
const choo = proxyquire('../..', {
|
|
||||||
'sheet-router/history': history.listen
|
|
||||||
})
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
app.model({
|
|
||||||
state: {
|
|
||||||
user: null
|
|
||||||
},
|
|
||||||
reducers: {
|
|
||||||
set: (state, data) => ({user: data.id})
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
open: function (state, data, send, done) {
|
|
||||||
t.deepEqual(data, {id: 1})
|
|
||||||
send('set', {id: 1}, function (err) {
|
|
||||||
if (err) return done(err)
|
|
||||||
history.broadcast('/users/1')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.router({ default: '/users' }, [
|
|
||||||
['/users', parentView, [
|
|
||||||
['/: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)
|
|
||||||
|
|
||||||
// resetLocation()
|
|
||||||
// const hash = Event()
|
|
||||||
// const choo = proxyquire('../..', {
|
|
||||||
// 'sheet-router/hash': hash.listen
|
|
||||||
// })
|
|
||||||
|
|
||||||
// const app = choo({hash: true})
|
|
||||||
|
|
||||||
// app.model({
|
|
||||||
// state: {
|
|
||||||
// user: null
|
|
||||||
// },
|
|
||||||
// reducers: {
|
|
||||||
// set: (state, data) => ({user: action.id})
|
|
||||||
// },
|
|
||||||
// effects: {
|
|
||||||
// open: function (state, data, send, done) {
|
|
||||||
// send('set', {id: 1}, function (err) {
|
|
||||||
// if (err) return done(err)
|
|
||||||
// hash.broadcast('#users/1')
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// app.router({ default: '/users' }, [
|
|
||||||
// ['/users', parentView, [
|
|
||||||
// ['/:user', childView]
|
|
||||||
// ]]
|
|
||||||
// ])
|
|
||||||
|
|
||||||
// const tree = app.start()
|
|
||||||
// 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`<p>${state.user}</p>`
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
t.test('disabling history', function (t) {
|
|
||||||
t.plan(1)
|
|
||||||
|
|
||||||
resetLocation()
|
|
||||||
const choo = proxyquire('../..', {
|
|
||||||
'sheet-router/history': () => t.fail('history listener attached')
|
|
||||||
})
|
|
||||||
|
|
||||||
const app = choo({ history: false })
|
|
||||||
|
|
||||||
app.router('/', [
|
|
||||||
['/', function () {
|
|
||||||
t.pass('rendered')
|
|
||||||
return document.createElement('div')
|
|
||||||
}]
|
|
||||||
])
|
|
||||||
|
|
||||||
app.start()
|
|
||||||
})
|
|
||||||
|
|
||||||
t.test('disabling href', function (t) {
|
|
||||||
t.plan(1)
|
|
||||||
|
|
||||||
const choo = proxyquire('../..', {
|
|
||||||
'sheet-router/href': () => t.fail('href listener attached')
|
|
||||||
})
|
|
||||||
|
|
||||||
const app = choo({ href: false })
|
|
||||||
app.router(['/', function () {
|
|
||||||
t.pass('rendered')
|
|
||||||
return document.createElement('div')
|
|
||||||
}])
|
|
||||||
app.start()
|
|
||||||
})
|
|
||||||
|
|
||||||
t.test('viewless nesting', function (t) {
|
|
||||||
t.plan(1)
|
|
||||||
|
|
||||||
const choo = require('../..')
|
|
||||||
const app = choo()
|
|
||||||
|
|
||||||
app.router({ default: '/users/123' }, [
|
|
||||||
['/users', [
|
|
||||||
['/:user', function (state) {
|
|
||||||
t.deepEqual(state.location.params, {user: '123'})
|
|
||||||
return document.createElement('div')
|
|
||||||
}]
|
|
||||||
]]
|
|
||||||
])
|
|
||||||
|
|
||||||
app.start()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function resetLocation () {
|
|
||||||
window.history.pushState({}, null, '/')
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
const test = require('tape')
|
|
||||||
const minDocument = require('min-document')
|
|
||||||
const choo = require('../../')
|
|
||||||
const view = require('../../html')
|
|
||||||
|
|
||||||
test('server', function (t) {
|
|
||||||
t.test('renders a static html response', function (t) {
|
|
||||||
t.plan(1)
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
app.router(['/', () => view`<h1>Hello Tokyo!</h1>`])
|
|
||||||
|
|
||||||
const html = app.toString('/')
|
|
||||||
const expected = '<h1>Hello Tokyo!</h1>'
|
|
||||||
t.equal(html, expected, 'strings are equal')
|
|
||||||
})
|
|
||||||
|
|
||||||
t.test('renders without a real DOM', function (t) {
|
|
||||||
t.plan(1)
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
app.router(['/', () => minDocument.createElement('div')])
|
|
||||||
|
|
||||||
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(['/', (state, prev, send) => {
|
|
||||||
return view`<h1>meow meow ${state.message}</h1>`
|
|
||||||
}])
|
|
||||||
|
|
||||||
const html = app.toString('/', { message: 'nyan!' })
|
|
||||||
const expected = '<h1>meow meow nyan!</h1>'
|
|
||||||
t.equal(html, expected, 'strings are equal')
|
|
||||||
})
|
|
||||||
|
|
||||||
t.test('extends flat existing models', function (t) {
|
|
||||||
t.plan(1)
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
app.model({ state: { bin: 'baz', beep: 'boop' } })
|
|
||||||
app.router(['/', (state, prev, send) => {
|
|
||||||
return view`<h1>${state.foo} ${state.bin} ${state.beep}</h1>`
|
|
||||||
}])
|
|
||||||
|
|
||||||
const state = { foo: 'bar!', beep: 'beep' }
|
|
||||||
const html = app.toString('/', state)
|
|
||||||
const expected = '<h1>bar! baz beep</h1>'
|
|
||||||
t.equal(html, expected, 'strings are equal')
|
|
||||||
})
|
|
||||||
|
|
||||||
t.test('extends namespaced existing models', function (t) {
|
|
||||||
t.plan(1)
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
app.model({
|
|
||||||
namespace: 'hello',
|
|
||||||
state: { bin: 'baz', beep: 'boop' }
|
|
||||||
})
|
|
||||||
app.router(['/', (state, prev, send) => {
|
|
||||||
return view`
|
|
||||||
<h1>${state.hello.foo} ${state.hello.bin} ${state.hello.beep}</h1>
|
|
||||||
`
|
|
||||||
}])
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
hello: {
|
|
||||||
foo: 'bar!',
|
|
||||||
beep: 'beep'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const html = app.toString('/', state)
|
|
||||||
const expected = '<h1>bar! baz beep</h1>'
|
|
||||||
t.equal(html, expected, 'strings are equal')
|
|
||||||
})
|
|
||||||
|
|
||||||
t.test('throws if called without route', function (t) {
|
|
||||||
t.plan(1)
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
app.router(['/', (state, prev, send) => send('hey!')])
|
|
||||||
t.throws(app.toString.bind(null), /route must be a string/)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.test('throws when calling send()', function (t) {
|
|
||||||
t.plan(1)
|
|
||||||
|
|
||||||
const app = choo()
|
|
||||||
app.router(['/', (state, prev, send) => send('hey!')])
|
|
||||||
const msg = /send\(\) cannot be called/
|
|
||||||
t.throws(app.toString.bind(null, '/', { message: 'nyan!' }), msg)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user