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:
|
||||
- export DISPLAY=':99.0'
|
||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
script: npm run test:cov
|
||||
after_script: npm i -g codecov.io && cat ./coverage/lcov.info | codecov
|
||||
script: npm run test
|
||||
# after_script: npm i -g codecov.io && cat ./coverage/lcov.info | codecov
|
||||
env:
|
||||
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=
|
||||
|
||||
@@ -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>
|
||||
</div>
|
||||
<div align="center">
|
||||
A <code>5kb</code> framework for creating sturdy frontend applications
|
||||
A <code>4kb</code> framework for creating sturdy frontend applications
|
||||
</div>
|
||||
|
||||
<br />
|
||||
@@ -47,18 +47,22 @@
|
||||
|
||||
<div align="center">
|
||||
<h3>
|
||||
<a href="https://choo.io">
|
||||
Website
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://yoshuawuyts.gitbooks.io/choo/content">
|
||||
Handbook
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/YerkoPalma/awesome-choo">
|
||||
Ecosystem
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/trainyard/choo-cli">
|
||||
CLI
|
||||
</a>
|
||||
<a href="https://github.com/YerkoPalma/awesome-choo">
|
||||
Ecosystem
|
||||
</a>
|
||||
<span> | </span>
|
||||
<!-- <a href="https://github.com/trainyard/choo-cli"> -->
|
||||
<!-- CLI -->
|
||||
<!-- </a> -->
|
||||
<!-- <span> | </span> -->
|
||||
<a href="https://github.com/yoshuawuyts/choo/blob/master/.github/CONTRIBUTING.md">
|
||||
Contributing
|
||||
</a>
|
||||
@@ -77,98 +81,64 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2>Table of Contents</h2>
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
<li><a href="#features">Features</a></li>
|
||||
<li><a href="#demos">Demos</a></li>
|
||||
<li><a href="#example">Example</a></li>
|
||||
<li><a href="#philosophy">Philosophy</a></li>
|
||||
<li><a href="#concepts">Concepts</a></li>
|
||||
<li><a href="#badges">Badges</a></li>
|
||||
<li><a href="#api">API</a></li>
|
||||
<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>
|
||||
## Table of Content
|
||||
- [Features](#features)
|
||||
- [Example](#example)
|
||||
- [Philosophy](#philosophy)
|
||||
- [Optimizations](#optimizations)
|
||||
- [FAQ](#faq)
|
||||
- [API](#api)
|
||||
- [Installation](#installation)
|
||||
- [See Also](#see-also)
|
||||
- [Support](#support)
|
||||
|
||||
## Features
|
||||
- __minimal size:__ weighing `5kb`, `choo` is a tiny little framework
|
||||
- __single state:__ immutable single state helps reason about changes
|
||||
- __small api:__ with only 6 methods, there's not a lot to learn
|
||||
- __minimal size:__ weighing `4kb`, `choo` is a tiny little framework
|
||||
- __event based:__ our performant event system makes writing apps easy
|
||||
- __small api:__ with only 6 methods there's not much to learn
|
||||
- __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
|
||||
- __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
|
||||
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
|
||||
var html = require('choo/html')
|
||||
var choo = require('choo')
|
||||
var html = require('./html')
|
||||
var choo = require('./')
|
||||
|
||||
var app = choo()
|
||||
app.use(logger)
|
||||
app.use(countStore)
|
||||
app.route('/', mainView)
|
||||
app.mount('body')
|
||||
|
||||
app.model({
|
||||
state: { title: 'Not quite set yet' },
|
||||
reducers: {
|
||||
update: function (state, data) {
|
||||
return { title: data }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function mainView (state, prev, send) {
|
||||
function mainView (state, emit) {
|
||||
return html`
|
||||
<main>
|
||||
<h1>Title: ${state.title}</h1>
|
||||
<input type="text" oninput=${update}>
|
||||
</main>
|
||||
<body>
|
||||
<h1>count is ${state.count}</h1>
|
||||
<button onclick=${onclick}>Increment</button>
|
||||
</body>
|
||||
`
|
||||
|
||||
function update (e) {
|
||||
send('update', e.target.value)
|
||||
function onclick () {
|
||||
emit('increment', 1)
|
||||
}
|
||||
}
|
||||
|
||||
app.router(['/', mainView])
|
||||
function logger (state, emitter) {
|
||||
emitter.on('*', function (messageName, data) {
|
||||
console.log('event', messageName, data)
|
||||
})
|
||||
}
|
||||
|
||||
var tree = app.start()
|
||||
document.body.appendChild(tree)
|
||||
```
|
||||
|
||||
To run it, save it as `client.js` and run with [bankai][bankai]. `bankai` is
|
||||
convenient but any [browserify][browserify] based tool should do:
|
||||
```sh
|
||||
# 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/
|
||||
function countStore (state, emitter) {
|
||||
state.count = 0
|
||||
emitter.on('increment', function (count) {
|
||||
state.count += count
|
||||
emitter.emit('render')
|
||||
})
|
||||
}
|
||||
```
|
||||
Want to see more examples? Check out the [Choo handbook][handbook].
|
||||
|
||||
## Philosophy
|
||||
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,
|
||||
better results and super smiley faces.
|
||||
|
||||
## Concepts
|
||||
`choo` cleanly structures internal data flow, so that all pieces of logic can
|
||||
be combined into a nice, cohesive machine. Roughly speaking there are two parts
|
||||
to `choo`: the views and the models. Models take care of state and logic, and
|
||||
views are responsible for displaying the interface and responding to user
|
||||
interactions.
|
||||
## Optimizations
|
||||
Choo is reasonably fast out of the box. But sometimes you might hit a scenario
|
||||
where a particular part of the UI slows down the application, and you want to
|
||||
speed it up. Here are some optimizations that are possible.
|
||||
|
||||
All of `choo`'s state is contained in a single object and whenever it changes
|
||||
the views receive a new version of the state which they can use to safely
|
||||
render a complete new representation of the DOM. The DOM is efficiently updated
|
||||
using DOM diffing/patching.
|
||||
### Reordering lists
|
||||
To be implemented. (See [yoshuawuyts/nanomorph#8](https://github.com/yoshuawuyts/nanomorph/issues/8))
|
||||
|
||||
The logic in choo exist in three different kinds of actions, each with their
|
||||
own role: `effects`, `subscriptions` and `reducers`.
|
||||
|
||||
- __Effects__ makes an asynchronous operation and calls another action when
|
||||
it's done.
|
||||
|
||||
- __Subscriptions__ (called once when the DOM loads) listens for external input
|
||||
like keyboard or WebSocket events and then calls another action.
|
||||
|
||||
- __Reducers__ receives the current state and returns an updated version of the
|
||||
state which is then sent to the views.
|
||||
|
||||
```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:
|
||||
### Caching DOM elements
|
||||
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
|
||||
because another piece of code is managing that part of the DOM tree. To achieve
|
||||
this `nanomorph` evaluates the `.isSameNode()` method on nodes to determine if
|
||||
they should be updated or not.
|
||||
|
||||
```js
|
||||
var choo = require('choo')
|
||||
var http = require('xhr')
|
||||
var app = choo()
|
||||
var el = html`<div>node</div>`
|
||||
|
||||
app.model({
|
||||
namespace: 'todos',
|
||||
state: { values: [] },
|
||||
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' })
|
||||
}
|
||||
// tell nanomorph to not compare the DOM tree if they're both divs
|
||||
el.isSameNode = function (target) {
|
||||
return (target && target.nodeName && target.nodeName === 'DIV')
|
||||
}
|
||||
```
|
||||
|
||||
In this example, when the `Add` button is clicked, the view will dispatch an
|
||||
`add` action that the model’s `add` reducer will receive. [As seen
|
||||
above](#models), the reducer will add an item to the state’s `todos` array. The
|
||||
state change will cause this view to be run again with the new state, and the
|
||||
resulting DOM tree will be used to [efficiently patch the
|
||||
DOM](#does-choo-use-a-virtual-dom).
|
||||
### Pruning dependencies
|
||||
We use the `require('assert')` module from Node core to provide helpful error
|
||||
messages in development. In production you probably want to strip this using
|
||||
[unassertify][unassertify].
|
||||
|
||||
### Plugins
|
||||
Sometimes it's necessary to change the way `choo` itself works. For example to
|
||||
report whenever an action is triggered, handle errors globally or perist state
|
||||
somewhere. This is done through something called `plugins`. Plugins are objects
|
||||
that contain `hook` and `wrap` functions and are passed to `app.use()`:
|
||||
To convert inlined HTML to valid DOM nodes we use `require('bel')`. This has
|
||||
overhead during runtime, so for production environments we should unwrap this
|
||||
using [yo-yoify][yo-yoify].
|
||||
|
||||
```js
|
||||
var log = require('choo-log')
|
||||
var choo = require('choo')
|
||||
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.
|
||||
Setting up browserify transforms can sometimes be a bit of hassle; to make this
|
||||
more convenient we recommend using [bankai][bankai] with `--optimize` to
|
||||
compile your assets for production.
|
||||
|
||||
## FAQ
|
||||
### 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.
|
||||
|
||||
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:
|
||||
|
||||
### 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...?
|
||||
It's called "choo", though we're fine if you call it "choo-choo" or
|
||||
"chugga-chugga-choo-choo" too. The only time "choo.js" is tolerated is if /
|
||||
when you shimmy like you're a locomotive.
|
||||
|
||||
### How does choo compare to X?
|
||||
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?
|
||||
`choo` uses [morphdom][morphdom], which diffs real DOM nodes instead of virtual
|
||||
nodes. It turns out that [browsers are actually ridiculously good at dealing
|
||||
with DOM nodes][morphdom-bench], and it has the added benefit of working with
|
||||
_any_ library that produces valid DOM nodes. So to put a long answer short:
|
||||
we're using something even better.
|
||||
`choo` uses [nanomorph][nanomorph], which diffs real DOM nodes instead of
|
||||
virtual nodes. It turns out that [browsers are actually ridiculously good at
|
||||
dealing with DOM nodes][morphdom-bench], and it has the added benefit of
|
||||
working with _any_ library that produces valid DOM nodes. So to put a long
|
||||
answer short: we're using something even better.
|
||||
|
||||
### How can I optimize choo?
|
||||
`choo` really shines when coupled with `browserify` transforms. They can do
|
||||
things like reduce file size, prune dependencies and clean up boilerplate code.
|
||||
Consider running some of the following:
|
||||
- [unassertify](https://github.com/twada/unassertify) - remove `assert()`
|
||||
statements which reduces file size. Use as a `--global` transform
|
||||
- [es2020](https://github.com/yoshuawuyts/es2020) - backport `const`,
|
||||
`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:
|
||||
### How can I support older browsers?
|
||||
Template strings aren't supported in all browsers, and parsing them creates
|
||||
significant overhead. To optimize we recommend running `browserify` with
|
||||
[yo-yoify][yo-yoify] as a global transform or using [bankai][bankai] directly.
|
||||
```sh
|
||||
$ NODE_ENV=production browserify \
|
||||
-t envify \
|
||||
-g yo-yoify \
|
||||
-g unassertify \
|
||||
-g es2020 \
|
||||
-g uglifyify \
|
||||
| uglifyjs
|
||||
$ browserify -g yo-yoify
|
||||
```
|
||||
|
||||
## Hey, doesn't this look a lot like Elm?
|
||||
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?
|
||||
### Is choo production ready?
|
||||
Sure.
|
||||
|
||||
## Browser Test Status
|
||||
<a href="https://saucelabs.com/u/yoshuawuyts">
|
||||
<img
|
||||
src="https://saucelabs.com/browser-matrix/yoshuawuyts.svg"
|
||||
alt="Sauce Test Status"/>
|
||||
</a>
|
||||
## 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][handbook] first
|
||||
:sparkles:
|
||||
|
||||
### `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
|
||||
```sh
|
||||
@@ -732,13 +281,7 @@ $ npm install choo
|
||||
```
|
||||
|
||||
## See Also
|
||||
- [choo-handbook](https://github.com/yoshuawuyts/choo-handbook) - the little
|
||||
`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`
|
||||
- [bankai](https://github.com/yoshuawuyts/bankai) - streaming asset compiler
|
||||
- [stack.gl](http://stack.gl/) - open software ecosystem for WebGL
|
||||
- [yo-yo](https://github.com/maxogden/yo-yo) - tiny library for modular UI
|
||||
- [bel](https://github.com/shama/bel) - composable DOM elements using template
|
||||
@@ -747,9 +290,6 @@ $ npm install choo
|
||||
humans
|
||||
- [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for
|
||||
`browserify`
|
||||
- [pull-stream](https://github.com/pull-stream/pull-stream) - minimal streams
|
||||
- [es2020](https://github.com/yoshuawuyts/es2020) - because in hindsight we
|
||||
don't need most of ES6
|
||||
|
||||
## Support
|
||||
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
|
||||
[handbook]: https://github.com/yoshuawuyts/choo-handbook
|
||||
[hyperx]: https://github.com/substack/hyperx
|
||||
[inu]: https://github.com/ahdinosaur/inu
|
||||
[morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks
|
||||
[morphdom]: https://github.com/patrick-steele-idem/morphdom
|
||||
[sheet-router]: https://github.com/yoshuawuyts/sheet-router
|
||||
[nanomorph]: https://github.com/yoshuawuyts/nanomorph
|
||||
[nanorouter]: https://github.com/yoshuawuyts/nanorouter
|
||||
[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')
|
||||
const onHistoryChange = require('sheet-router/history')
|
||||
const sheetRouter = require('sheet-router')
|
||||
const onHref = require('sheet-router/href')
|
||||
const walk = require('sheet-router/walk')
|
||||
const mutate = require('xtend/mutable')
|
||||
const barracks = require('barracks')
|
||||
const nanoraf = require('nanoraf')
|
||||
const assert = require('assert')
|
||||
const xtend = require('xtend')
|
||||
const yo = require('yo-yo')
|
||||
var documentReady = require('document-ready')
|
||||
var nanorouter = require('nanorouter')
|
||||
var nanomount = require('nanomount')
|
||||
var nanomorph = require('nanomorph')
|
||||
var nanoraf = require('nanoraf')
|
||||
var nanobus = require('nanobus')
|
||||
var assert = require('assert')
|
||||
|
||||
module.exports = choo
|
||||
var onHistoryChange = require('./lib/history')
|
||||
var onHref = require('./lib/href')
|
||||
|
||||
// framework for creating sturdy web applications
|
||||
// null -> fn
|
||||
function choo (opts) {
|
||||
module.exports = Framework
|
||||
|
||||
function Framework (opts) {
|
||||
opts = opts || {}
|
||||
|
||||
const _store = start._store = barracks()
|
||||
var _router = start._router = null
|
||||
var _routerOpts = null
|
||||
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')
|
||||
}
|
||||
}
|
||||
var routerOpts = {
|
||||
default: opts.defaultRoute || '/404',
|
||||
curry: true
|
||||
}
|
||||
|
||||
// start the application
|
||||
// (str?, obj?) -> DOMNode
|
||||
function start () {
|
||||
_store.model(createLocationModel(opts))
|
||||
const createSend = _store.start(opts)
|
||||
_router = start._router = createRouter(_routerOpts, _routes, createSend)
|
||||
const state = _store.state({state: {}})
|
||||
var router = nanorouter(routerOpts)
|
||||
var bus = nanobus()
|
||||
var rerender = null
|
||||
var tree = null
|
||||
var state = {}
|
||||
|
||||
const tree = _router(state.location.href, state)
|
||||
assert.ok(tree, 'choo.start: the router should always return a valid DOM node')
|
||||
assert.equal(typeof tree, 'object', 'choo.start: the router should always return a valid DOM node')
|
||||
_rootNode = tree
|
||||
tree.done = done
|
||||
|
||||
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
|
||||
// (obj, obj, obj, str, fn) -> null
|
||||
function render (state, data, prev, name, createSend) {
|
||||
if (!_frame) {
|
||||
_frame = nanoraf(function (state, prev) {
|
||||
const newTree = _router(state.location.href, state, prev)
|
||||
_rootNode = yo.update(_rootNode, newTree)
|
||||
})
|
||||
}
|
||||
_frame(state, prev)
|
||||
}
|
||||
|
||||
// register all routes on the router
|
||||
// (str?, [fn|[fn]]) -> obj
|
||||
function router (defaultRoute, routes) {
|
||||
_routerOpts = defaultRoute
|
||||
_routes = routes
|
||||
}
|
||||
|
||||
// create a new model
|
||||
// (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 }
|
||||
toString: toString,
|
||||
use: register,
|
||||
mount: mount,
|
||||
route: route,
|
||||
start: start
|
||||
}
|
||||
|
||||
// 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
|
||||
function route (route, handler) {
|
||||
router.on(route, function (params) {
|
||||
return function () {
|
||||
state.params = params
|
||||
return handler(state, emit)
|
||||
}
|
||||
}
|
||||
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)
|
||||
function register (cb) {
|
||||
cb(state, bus)
|
||||
}
|
||||
|
||||
// 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)
|
||||
function start () {
|
||||
tree = router(createLocation(), state, emit)
|
||||
rerender = nanoraf(function () {
|
||||
var newTree = router(createLocation(), state, emit)
|
||||
tree = nanomorph(tree, newTree)
|
||||
})
|
||||
|
||||
// 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 = {}
|
||||
bus.on('render', rerender)
|
||||
|
||||
if (opts.history !== false) {
|
||||
subs.handleHistory = function (send, done) {
|
||||
onHistoryChange(function navigate (href) {
|
||||
send('location:touch', href, done)
|
||||
onHistoryChange(function (href) {
|
||||
bus.emit('pushState', window.location.href)
|
||||
scrollIntoView()
|
||||
})
|
||||
|
||||
if (opts.href !== false) {
|
||||
onHref(function (location) {
|
||||
var href = location.href
|
||||
var currHref = window.location.href
|
||||
if (href === currHref) return
|
||||
window.history.pushState({}, null, href)
|
||||
bus.emit('pushState', window.location.href)
|
||||
bus.emit('render')
|
||||
scrollIntoView()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.href !== false) {
|
||||
subs.handleHref = function (send, done) {
|
||||
onHref(function navigate (location) {
|
||||
send('location:set', location, done)
|
||||
})
|
||||
}
|
||||
}
|
||||
documentReady(function () {
|
||||
bus.emit('DOMContentLoaded')
|
||||
})
|
||||
|
||||
return subs
|
||||
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",
|
||||
"version": "4.1.0",
|
||||
"version": "5.0.0-5",
|
||||
"description": "A 5kb framework for creating sturdy frontend applications",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"deps": "./scripts/test deps",
|
||||
"test:electron": "./scripts/test electron",
|
||||
"test:cov": "./scripts/test cov",
|
||||
"test:server": "./scripts/test server",
|
||||
"test:browser": "./scripts/test browser",
|
||||
"test:browser-local": "./scripts/test browser-local",
|
||||
"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"
|
||||
"deps": "dependency-check --entry ./html.js . && dependency-check . --extra --no-dev --entry ./html.js",
|
||||
"start": "bankai start example --open",
|
||||
"test": "standard && npm run deps && node test.js",
|
||||
"build:dev": "echo 'to be implemented'",
|
||||
"build:min": "echo 'to be implemented'",
|
||||
"prepublish": "npm run build:dev && npm run build:min"
|
||||
},
|
||||
"repository": "yoshuawuyts/choo",
|
||||
"keywords": [
|
||||
@@ -28,53 +20,21 @@
|
||||
"composable",
|
||||
"tiny"
|
||||
],
|
||||
"files": [
|
||||
"index.js",
|
||||
"mount.js",
|
||||
"html.js",
|
||||
"dist/"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"barracks": "^9.1.0",
|
||||
"document-ready": "~1.0.2",
|
||||
"global": "^4.3.0",
|
||||
"nanoraf": "^2.1.1",
|
||||
"sheet-router": "^4.0.1",
|
||||
"xtend": "^4.0.1",
|
||||
"yo-yo": "^1.2.2"
|
||||
"bel": "^4.5.1",
|
||||
"document-ready": "^1.0.3",
|
||||
"global": "^4.3.1",
|
||||
"nanobus": "^2.1.0",
|
||||
"nanomorph": "^4.0.0",
|
||||
"nanomount": "^1.0.0",
|
||||
"nanoraf": "^3.0.0",
|
||||
"nanorouter": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"append-child": "~1.0.0",
|
||||
"bankai": "^3.1.0",
|
||||
"browserify": "^13.0.1",
|
||||
"browserify-istanbul": "^2.0.0",
|
||||
"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"
|
||||
"dependency-check": "^2.8.0",
|
||||
"spok": "^0.7.0",
|
||||
"standard": "^9.0.1",
|
||||
"tape": "^4.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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