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:
Yoshua Wuyts
2017-03-21 03:00:45 +01:00
committed by GitHub
parent 6fc525fe05
commit 8e8e870d45
61 changed files with 807 additions and 2707 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ addons:
before_install: before_install:
- export DISPLAY=':99.0' - export DISPLAY=':99.0'
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
script: npm run test:cov script: npm run test
after_script: npm i -g codecov.io && cat ./coverage/lcov.info | codecov # after_script: npm i -g codecov.io && cat ./coverage/lcov.info | codecov
env: env:
global: global:
- secure: t6MYp/rV/HXKQ8yb21fDMAV9fkwZmPFJ/4Za4TeXvVOojFan4Q40j5VZF6wiVBHTemctMRueQ2iuGl8pgNX3jqRHMKvNS5zKCCUHYvmyvMLQiWpkDC3Lz3R9oz2fooY1f0fpnzaE0TmJHoZQcVDYDTZSk/o6fNXJQnI4SUG/nmnb3VAA0H8lRZ1OidkAbBzJNFj8i0ZTuaqwo0uQVhNDSvLLKgM94ryF+DB+c0vKuZr85wn+xnuJ1ZPZM5S+uZrOAY+TnwD1QvstULHnbF6sqH5WuUkUCHpXV84Hz2pr7GL7yCkj65XoNoqvnoR5ETv8yBa5r/OccrCY9NqXSCv/zWiS4vRWybrJq0Mdx67cXHKHjbQMrILHsbRBDxbCJwn+LAQlwRUoktEcI9bNRUhi0Wt/LQmLvyfldt7mMvgl+7srBdlZWPoTZWRZKqu04XpYTaqAnAeNOjfSOOuB29uVTRu7n9pCeKA/gDNYOWCF3CfN++ztbL+E2OpPnpoRWwoSXnXzFQrLofq6zF1bpIrb5UpaqNYGX+PifUYX+hRYKLdOqqAS3JlJBqG+0ArRZgf3sm/uk4ZDesV5Ee9qB7Ty32Vh66wmU8P6wZjgare4TL+hcsgtNRrd6+kZwXfn0mqcqeFovN2yPpQSmF1U5+ki8+o1NcFFYdeucWa7E9ULkVE= - secure: t6MYp/rV/HXKQ8yb21fDMAV9fkwZmPFJ/4Za4TeXvVOojFan4Q40j5VZF6wiVBHTemctMRueQ2iuGl8pgNX3jqRHMKvNS5zKCCUHYvmyvMLQiWpkDC3Lz3R9oz2fooY1f0fpnzaE0TmJHoZQcVDYDTZSk/o6fNXJQnI4SUG/nmnb3VAA0H8lRZ1OidkAbBzJNFj8i0ZTuaqwo0uQVhNDSvLLKgM94ryF+DB+c0vKuZr85wn+xnuJ1ZPZM5S+uZrOAY+TnwD1QvstULHnbF6sqH5WuUkUCHpXV84Hz2pr7GL7yCkj65XoNoqvnoR5ETv8yBa5r/OccrCY9NqXSCv/zWiS4vRWybrJq0Mdx67cXHKHjbQMrILHsbRBDxbCJwn+LAQlwRUoktEcI9bNRUhi0Wt/LQmLvyfldt7mMvgl+7srBdlZWPoTZWRZKqu04XpYTaqAnAeNOjfSOOuB29uVTRu7n9pCeKA/gDNYOWCF3CfN++ztbL+E2OpPnpoRWwoSXnXzFQrLofq6zF1bpIrb5UpaqNYGX+PifUYX+hRYKLdOqqAS3JlJBqG+0ArRZgf3sm/uk4ZDesV5Ee9qB7Ty32Vh66wmU8P6wZjgare4TL+hcsgtNRrd6+kZwXfn0mqcqeFovN2yPpQSmF1U5+ki8+o1NcFFYdeucWa7E9ULkVE=
-28
View File
@@ -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
+133 -592
View File
@@ -7,7 +7,7 @@
<strong>Fun functional programming</strong> <strong>Fun functional programming</strong>
</div> </div>
<div align="center"> <div align="center">
A <code>5kb</code> framework for creating sturdy frontend applications A <code>4kb</code> framework for creating sturdy frontend applications
</div> </div>
<br /> <br />
@@ -47,6 +47,10 @@
<div align="center"> <div align="center">
<h3> <h3>
<a href="https://choo.io">
Website
</a>
<span> | </span>
<a href="https://yoshuawuyts.gitbooks.io/choo/content"> <a href="https://yoshuawuyts.gitbooks.io/choo/content">
Handbook Handbook
</a> </a>
@@ -55,10 +59,10 @@
Ecosystem Ecosystem
</a> </a>
<span> | </span> <span> | </span>
<a href="https://github.com/trainyard/choo-cli"> <!-- <a href="https://github.com/trainyard/choo-cli"> -->
CLI <!-- CLI -->
</a> <!-- </a> -->
<span> | </span> <!-- <span> | </span> -->
<a href="https://github.com/yoshuawuyts/choo/blob/master/.github/CONTRIBUTING.md"> <a href="https://github.com/yoshuawuyts/choo/blob/master/.github/CONTRIBUTING.md">
Contributing Contributing
</a> </a>
@@ -77,98 +81,64 @@
</a> </a>
</div> </div>
<h2>Table of Contents</h2> ## Table of Content
<details> - [Features](#features)
<summary>Table of Contents</summary> - [Example](#example)
<li><a href="#features">Features</a></li> - [Philosophy](#philosophy)
<li><a href="#demos">Demos</a></li> - [Optimizations](#optimizations)
<li><a href="#example">Example</a></li> - [FAQ](#faq)
<li><a href="#philosophy">Philosophy</a></li> - [API](#api)
<li><a href="#concepts">Concepts</a></li> - [Installation](#installation)
<li><a href="#badges">Badges</a></li> - [See Also](#see-also)
<li><a href="#api">API</a></li> - [Support](#support)
<li><a href="#faq">FAQ</a></li>
<li><a href="#installation">Installation</a></li>
<li><a href="#see-also">See Also</a></li>
<li><a href="#support">Support</a></li>
</details>
## Features ## Features
- __minimal size:__ weighing `5kb`, `choo` is a tiny little framework - __minimal size:__ weighing `4kb`, `choo` is a tiny little framework
- __single state:__ immutable single state helps reason about changes - __event based:__ our performant event system makes writing apps easy
- __small api:__ with only 6 methods, there's not a lot to learn - __small api:__ with only 6 methods there's not much to learn
- __minimal tooling:__ built for the cutting edge `browserify` compiler - __minimal tooling:__ built for the cutting edge `browserify` compiler
- __transparent side effects:__ using `effects` and `subscriptions` brings
clarity to IO
- __omakase:__ composed out of a balanced selection of open source packages
- __isomorphic:__ renders seamlessly in both Node and browsers - __isomorphic:__ renders seamlessly in both Node and browsers
- __very cute:__ choo choo! - __very cute:__ choo choo!
## Demos
- :truck: [Input example](http://requirebin.com/?gist=229bceda0334cf30e3044d5f5c600960)
([repo](examples/title/))
- :water_buffalo: [HTTP effects example](https://fork-fang.hyperdev.space/)
([repo](https://hyperdev.com/#!/project/fork-fang))
- :mailbox: [Mailbox routing](examples/mailbox/)
- :ok_hand: [TodoMVC](http://shuheikagawa.com/todomvc-choo/)
([repo](https://github.com/shuhei/todomvc-choo))
- :fire: [Choo-firebase](https://github.com/fyrkant/choo-firebase)
- :seedling: [Grow](https://grow.static.land/)
([repo](https://github.com/sethvincent/grow))
- :newspaper: [Hacker News Reader](https://hackernews-choo.surge.sh/) ([repo](https://github.com/kvnneff/hackernews-choo))
- :earth_africa: [Thing Translator](https://oxism.com/thing-translator/) ([repo](https://github.com/dmotz/thing-translator))
_note: If you've built something cool using `choo` or are using it in
production, we'd love to hear from you!_
## Example ## Example
Let's create an input box that changes the content of a textbox in real time.
[Click here to see the app running](http://requirebin.com/?gist=229bceda0334cf30e3044d5f5c600960).
```js ```js
var html = require('choo/html') var html = require('./html')
var choo = require('choo') var choo = require('./')
var app = choo() var app = choo()
app.use(logger)
app.use(countStore)
app.route('/', mainView)
app.mount('body')
app.model({ function mainView (state, emit) {
state: { title: 'Not quite set yet' },
reducers: {
update: function (state, data) {
return { title: data }
}
}
})
function mainView (state, prev, send) {
return html` return html`
<main> <body>
<h1>Title: ${state.title}</h1> <h1>count is ${state.count}</h1>
<input type="text" oninput=${update}> <button onclick=${onclick}>Increment</button>
</main> </body>
` `
function update (e) { function onclick () {
send('update', e.target.value) emit('increment', 1)
} }
} }
app.router(['/', mainView]) function logger (state, emitter) {
emitter.on('*', function (messageName, data) {
console.log('event', messageName, data)
})
}
var tree = app.start() function countStore (state, emitter) {
document.body.appendChild(tree) state.count = 0
``` emitter.on('increment', function (count) {
state.count += count
To run it, save it as `client.js` and run with [bankai][bankai]. `bankai` is emitter.emit('render')
convenient but any [browserify][browserify] based tool should do: })
```sh }
# run and reload on port 8080
$ bankai client.js -p 8080 --open
# compile to static files in `./dist/`
$ bankai build index.js dist/
# deploy to github pages using `tschaub/gh-pages`
$ gh-pages -d dist/
``` ```
Want to see more examples? Check out the [Choo handbook][handbook].
## Philosophy ## Philosophy
We believe programming should be fun and light, not stern and stressful. It's We believe programming should be fun and light, not stern and stressful. It's
@@ -193,395 +163,42 @@ is laid out. And once an application is built, we want it to be small,
performant and easy to reason about. All of which makes for easy to debug code, performant and easy to reason about. All of which makes for easy to debug code,
better results and super smiley faces. better results and super smiley faces.
## Concepts ## Optimizations
`choo` cleanly structures internal data flow, so that all pieces of logic can Choo is reasonably fast out of the box. But sometimes you might hit a scenario
be combined into a nice, cohesive machine. Roughly speaking there are two parts where a particular part of the UI slows down the application, and you want to
to `choo`: the views and the models. Models take care of state and logic, and speed it up. Here are some optimizations that are possible.
views are responsible for displaying the interface and responding to user
interactions.
All of `choo`'s state is contained in a single object and whenever it changes ### Reordering lists
the views receive a new version of the state which they can use to safely To be implemented. (See [yoshuawuyts/nanomorph#8](https://github.com/yoshuawuyts/nanomorph/issues/8))
render a complete new representation of the DOM. The DOM is efficiently updated
using DOM diffing/patching.
The logic in choo exist in three different kinds of actions, each with their ### Caching DOM elements
own role: `effects`, `subscriptions` and `reducers`. Sometimes we want to tell the algorithm to not evaluate certain nodes (and its
children). This can be because we're sure they haven't changed, or perhaps
- __Effects__ makes an asynchronous operation and calls another action when because another piece of code is managing that part of the DOM tree. To achieve
it's done. this `nanomorph` evaluates the `.isSameNode()` method on nodes to determine if
they should be updated or not.
- __Subscriptions__ (called once when the DOM loads) listens for external input
like keyboard or WebSocket events and then calls another action.
- __Reducers__ receives the current state and returns an updated version of the
state which is then sent to the views.
```txt
┌─────────────────┐
│ Subscriptions ─┤ User ───┐
└─ Effects ◀─────┤ ▼
┌─ Reducers ◀─────┴─────────── DOM ◀┐
│ │
└▶ Router ─────State ───▶ Views ────┘
```
### Models
`models` are objects that contain initial `state`, `subscriptions`, `effects`
and `reducers`. They're generally grouped around a theme (or domain, if you
like). To provide some sturdiness to your `models`, they can either be
namespaced or not. Namespacing means that only state within the model can be
accessed. Models can still trigger actions on other models, though it's
recommended to keep that to a minimum.
So say we have a `todos` namespace, an `add` reducer and a `todos` model.
Outside the model they're called by `send('todos:add')` and
`state.todos.items`. Inside the namespaced model they're called by
`send('todos:add')` and `state.items`. An example namespaced model:
```js
var app = choo()
app.model({
namespace: 'todos',
state: { items: [] },
reducers: {
add: function (state, data) {
return { items: state.items.concat(data.payload) }
}
}
})
```
In most cases using namespaces is beneficial, as having clear boundaries makes
it easier to follow logic. But sometimes you need to call `actions` that
operate over multiple domains (such as a "logout" `action`), or have a
`subscription` that might trigger multiple `reducers` (such as a `websocket`
that calls a different `action` based on the incoming data).
In these cases you probably want to have a `model` that doesn't use namespaces,
and has access to the full application state. Try and keep the logic in these
`models` to a minimum, and declare as few `reducers` as possible. That way the
bulk of your logic will be safely shielded, with only a few points touching every
part of your application.
### Effects
`effects` are similar to `reducers` except instead of modifying the state they
cause side `effects` by interacting servers, databases, DOM APIs, etc. Often
they'll call a reducer when they're done to update the state. For instance, you
may have an effect called getUsers that fetches a list of users from a server
API using AJAX. Assuming the AJAX request completes successfully, the effect
can pass off the list of users to a reducer called receiveUsers which simply
updates the state with that list, separating the concerns of interacting with
an API from updating the application's state.
This is an example `effect` that is called once when the application loads and
calls the `'todos:add'` `reducer` when it receives data from the server:
```js ```js
var choo = require('choo') var el = html`<div>node</div>`
var http = require('xhr')
var app = choo()
app.model({ // tell nanomorph to not compare the DOM tree if they're both divs
namespace: 'todos', el.isSameNode = function (target) {
state: { values: [] }, return (target && target.nodeName && target.nodeName === 'DIV')
reducers: {
add: function (state, data) {
return { todos: data }
}
},
effects: {
addAndSave: function (state, data, send, done) {
var opts = { body: data.payload, json: true }
http.post('/todo', opts, function (err, res, body) {
if (err) return done(err)
data.payload.id = body.id
send('todos:add', data, function (err, value) {
if (err) return done(err)
done(null, value)
})
})
}
},
subscriptions: {
'called-once-when-the-app-loads': function (send, done) {
send('todos:addAndSave', done)
}
}
})
```
### Subscriptions
Subscriptions are a way of receiving data from a source. For example when
listening for events from a server using `SSE` or `Websockets` for a
chat app, or when catching keyboard input for a videogame.
An example subscription that logs `"dog?"` every second:
```js
var choo = require('choo')
var app = choo()
app.model({
namespace: 'app',
effects: {
print: function (state, data) {
console.log(data.payload)
}
},
subscriptions: {
callDog: function (send, done) {
setInterval(function () {
var data = { payload: 'dog?', myOtherValue: 1000 }
send('app:print', data, function (err) {
if (err) return done(err)
})
}, 1000)
}
}
})
```
If a `subscription` runs into an error, it can call `done(err)` to signal the
error to the error hook.
### Router
The `router` manages which `views` are rendered at any given time. It also
supports rendering a default `view` if no routes match.
```js
var app = choo()
app.router({ default: '/404' }, [
[ '/', require('./views/empty') ],
[ '/404', require('./views/error') ],
[ '/:mailbox', require('./views/mailbox'), [
[ '/:message', require('./views/email') ]
]]
])
```
Routes on the `router` are passed in as a nested array. This means that the
entry point of the application also becomes a site map, making it easier to
figure out how views relate to each other.
Under the hood `choo` uses [sheet-router]. Internally the
currently rendered route is kept in `state.location`. To access the route
`:params` you can use `state.location.params`. If you want to modify
the location programmatically the `reducer` for the location can be called
using `send('location:set', href)`. This will not work
from within namespaced `models`, and usage should preferably be kept to a
minimum. Changing views all over the place tends to lead to messiness.
### Views
Views are pure functions that return a DOM tree for the router to render.
Theyre passed the current state, and any time the state changes theyre run
again with the new state.
Views are also passed the `send` function, which they can use to dispatch
actions that can update the state. For example, the DOM tree can have an
`onclick` handler that dispatches an `add` action.
```js
function view (state, prev, send) {
return html`
<div>
<h1>Total todos: ${state.todos.length}</h1>
<button onclick=${addTodo}>Add</button>
</div>
`
function addTodo (e) {
send('add', { title: 'demo' })
}
} }
``` ```
In this example, when the `Add` button is clicked, the view will dispatch an ### Pruning dependencies
`add` action that the models `add` reducer will receive. [As seen We use the `require('assert')` module from Node core to provide helpful error
above](#models), the reducer will add an item to the states `todos` array. The messages in development. In production you probably want to strip this using
state change will cause this view to be run again with the new state, and the [unassertify][unassertify].
resulting DOM tree will be used to [efficiently patch the
DOM](#does-choo-use-a-virtual-dom).
### Plugins To convert inlined HTML to valid DOM nodes we use `require('bel')`. This has
Sometimes it's necessary to change the way `choo` itself works. For example to overhead during runtime, so for production environments we should unwrap this
report whenever an action is triggered, handle errors globally or perist state using [yo-yoify][yo-yoify].
somewhere. This is done through something called `plugins`. Plugins are objects
that contain `hook` and `wrap` functions and are passed to `app.use()`:
```js Setting up browserify transforms can sometimes be a bit of hassle; to make this
var log = require('choo-log') more convenient we recommend using [bankai][bankai] with `--optimize` to
var choo = require('choo') compile your assets for production.
var app = choo()
app.use(log())
var tree = app.start()
document.body.appendChild(tree)
```
Generally people using `choo` shouldn't be too worried about the specifics of
`plugins`, as the internal API is (unfortunately by necessity) quite complex.
After all they're the most powerful way to modify a `choo` application.
__:warning: Warning :warning:: plugins should only be used as a last resort.
It creates peer dependencies which makes upgrading versions and switching
frameworks a lot harder. Please exhaust all other options before using
plugins.__
If you want to learn more about creating your own `plugins`, and which `hooks`
and `wrappers` are available, head on over to [app.use()](#appusehooks).
## Badges
Using `choo` in a project? Show off which version you've used using a badge:
[![built with choo v4](https://img.shields.io/badge/built%20with%20choo-v4-ffc3e4.svg?style=flat-square)](https://github.com/yoshuawuyts/choo)
```md
[![built with choo v4](https://img.shields.io/badge/built%20with%20choo-v4-ffc3e4.svg?style=flat-square)](https://github.com/yoshuawuyts/choo)
```
## API
This section provides documentation on how each function in `choo` works. It's
intended to be a technical reference. If you're interested in learning choo for
the first time, consider reading through the [handbook] or
[concepts](#concepts) first :sparkles:
### app = choo(opts)
Initialize a new `choo` app. Takes an optional object of handlers that is
passed to [app.use()](#appusehooks).
Opts can also contain the following values:
- __opts.history:__ default: `true`. Enable a `subscription` to the browser
history API. e.g. updates the internal `location.href` state whenever the
browsers "forward" and "backward" buttons are pressed.
- __opts.href:__ default: `true`. Handle all relative `<a
href="<location>"></a>` clicks and update internal `state.location`
accordingly.
- __opts.hash:__ default: `false`. Enable a `subscription` to the hash change
event, updating the internal `state.location` state whenever the URL hash
changes (eg `localhost/#posts/123`). Enabling this option automatically
disables `opts.history` and `opts.href`.
### app.model(obj)
Create a new model. Models modify data and perform IO. Takes the following
arguments:
- __namespace:__ namespace the model so that it cannot access any properties
and handlers in other models
- __state:__ initial values of `state` inside the model
- __reducers:__ synchronous operations that modify state. Triggered by
`actions`. Signature of `(state, data)`.
- __effects:__ asynchronous operations that don't modify state directly.
Triggered by `actions`, can call `actions`. Signature of `(state, data,
send, done)`
- __subscriptions:__ asynchronous read-only operations that don't modify state
directly. Can call `actions`. Signature of `(send, done)`.
#### send(actionName, data?[,callback])
Send a new action to the models with optional data attached. Namespaced models
can be accessed by prefixing the name with the namespace separated with a `:`,
e.g. `namespace:name`.
When sending data from inside a `model` it expects exactly three arguments: the name of the action you're calling, the data you want to send, and finally a callback to handle errors through the global `onError()` hook. So if you want to send two values, you'd have to either send an array or object containing them.
#### done(err?, res?)
When an `effect` or `subscription` is done executing, or encounters an error,
it should call the final `done(err, res)` callback. If an `effect` was called
by another `effect` it will call the callback of the caller. When an error
propegates all the way to the top, the `onError` handler will be called,
registered in `choo(handlers)`. If no callback is registered, errors will
`throw`.
### app.router(defaultRoute?, (route) => [routes])
Creates a new router. Takes a function that exposes a single `route` function,
and that expects a tree of `routes` to be returned. See [sheet-router] for full
documentation. Registered views have a signature of `(state, prev, send)`,
where `state` is the current `state`, `prev` is the last state,
`state.location.params` is URI partials and `send()` can be called to trigger
actions. If `defaultRoute` is passed in, that will be called if no paths match.
If no `defaultRoute` is specified it will throw instead.
### app.use(hooks)
Register an object of hooks on the application. This is useful to extend the
way `choo` works, adding custom behavior and listeners. Generally returning
objects of `hooks` is done by returning them from functions (which we call
`plugins` throughout the documentation).
There are several `hooks` and `wrappers` that are picked up by `choo`:
- __onError(err, state, createSend):__ called when an `effect` or
`subscription` emit an error. If no handler is passed, the default handler
will `throw` on each error.
- __onAction(state, data, name, caller, createSend):__ called when an
`action` is fired.
- __onStateChange(state, data, prev, caller, createSend):__ called after a
reducer changes the `state`.
- __wrapSubscriptions(fn):__ wraps a `subscription` to add custom behavior
- __wrapReducers(fn):__ wraps a `reducer` to add custom behavior
- __wrapEffects(fn):__ wraps an `effect` to add custom behavior
- __wrapInitialState(fn):__ mutate the complete initial `state` to add custom
behavior - useful to mutate the state before starting up
__:warning: Warning :warning:: plugins should only be used as a last resort.
It creates peer dependencies which makes upgrading versions and switching
frameworks a lot harder. Please exhaust all other options before using
plugins.__
`createSend()` is a special function that allows the creation of a new named
`send()` function. The first argument should be a string which is the name, the
second argument is a boolean `callOnError` which can be set to `true` to call
the `onError` hook istead of a provided callback. It then returns a
`send(actionName, data?)` function.
Hooks should be used with care, as they're the most powerful interface into
the state. For application level code it's generally recommended to delegate to
actions inside models using the `send()` call, and only shape the actions
inside the hooks.
### html = app.toString(route, state?)
Render the application to a string of HTML. Useful for rendering on the server.
First argument is a path that's passed to the router. Second argument is an
optional state object. When calling `.toString()` instead of `.start()`, all
calls to `send()` are disabled, and `subscriptions`, `effects` and `reducers`
aren't loaded.
### tree = app.start()
Start the application. Returns a tree of DOM nodes that can be mounted using
`document.body.appendChild()`.
### view = require('choo/html')\`html\`
Tagged template string HTML builder. Built on top of [yo-yo], [bel], and
[hyperx]. To register a view on the `router` it should be wrapped in a function
with the signature of `(state, prev, send)` where `state` is the current
`state`, `prev` is the last state, `state.location.params` is URI partials and
`send()` can be called to trigger actions.
To create listeners for events, create interpolated attributes on elements.
```js
var html = require('choo/html')
html`
<button onclick=${log}>click for bananas</button>
`
function log (e) {
console.log(e)
}
```
Example listeners include: `onclick`, `onsubmit`, `oninput`, `onkeydown`,
`onkeyup`. A full list can be found [at the yo-yo
repo](https://github.com/maxogden/yo-yo/blob/master/update-events.js). When
creating listeners always remember to call `e.preventDefault()` and
`e.stopPropagation()` on the event so it doesn't bubble up and do stuff like
refreshing the full page or the like.
To trigger lifecycle events on any part of a view, set the `onload=${(el) =>
{}}` and `onunload=${(el) => {}}` attributes. These parameters are useful when
creating self-contained widgets that take care of their own state and lifecycle
(e.g. a maps widget) or to trigger animations. Most elements shouldn't have a
need for these hooks though.
### mount('selector', html)
Use `choo/mount` to mount a tree of DOMNodes at a given selector. This is
especially useful to mount a `<body>` tag on the document body after rendering
from the server. It makes sure all `<script>` tags and similar are persisted so
no duplicate download calls are triggered.
## FAQ ## FAQ
### Why is it called choo? ### Why is it called choo?
@@ -590,141 +207,73 @@ _"performant"_, _"rigid"_, _"robust"_ - I like programming to be light, fun and
non-scary. `choo` embraces that. non-scary. `choo` embraces that.
Also imagine telling some business people you chose to rewrite something Also imagine telling some business people you chose to rewrite something
critical to the company using `choo`. critical for serious bizcorp using a train themed framework.
:steam_locomotive::train::train::train: :steam_locomotive::train::train::train:
### Why is it a framework, and not a library?
I love small libraries that do one thing well, but when working in a team,
having an undocumented combination of packages often isn't great. `choo()` is a
small set of packages that work well together, wrapped in an an architectural
pattern. This means you get all the benefits of small packages, but get to be
productive right from the start without needing to plough through layers of
boilerplate.
### Is it called choo, choo.js or...? ### Is it called choo, choo.js or...?
It's called "choo", though we're fine if you call it "choo-choo" or It's called "choo", though we're fine if you call it "choo-choo" or
"chugga-chugga-choo-choo" too. The only time "choo.js" is tolerated is if / "chugga-chugga-choo-choo" too. The only time "choo.js" is tolerated is if /
when you shimmy like you're a locomotive. when you shimmy like you're a locomotive.
### How does choo compare to X?
Ah, so this is where I get to rant. `choo` (_chugga-chugga-chugga-choo-choo!_)
was built because other options didn't quite cut it for me, so instead of
presenting some faux-objective chart with skewed benchmarks and checklists I'll
give you my opinions directly. Ready? Here goes:
- __react:__ despite being at the root of a giant paradigm shift for frontend
(thank you forever!), `react` is kind of big (`155kb` was it?). They also
like classes a lot, and enforce a _lot_ of abstractions. It also encourages
the use of `JSX` and `babel` which break _JavaScript, The Language™_. And all
that without making clear how code should flow, which is crucial in a team
setting. I don't like complicated things and in my view `react` is one of
them. `react` is not for me.
- __mithril:__ never used it, never will. I didn't like the API, but if you
like it maybe it's worth a shot - the API seems small enough. I wouldn't know
how pleasant it is past face value.
- __preact:__ a pretty cool idea; seems to fix most of what is wrong with
`react`. However it doesn't fix the large dependencies `react` seems to use
(e.g. `react-router` and friends) and doesn't help at all with architecture.
If `react` is your jam, and you will not budge, sitting at `3kb` this is
probably a welcome gift.
- __angular:__ definitely not for me. I like small things with a clear mental
model; `angular` doesn't tick any box in my book of nice things.
- __angular2:__ I'm not sure what's exactly changed, but I know the addition of
`TypeScript` and `RxJS` definitely hasn't made things simpler. Last I checked
it was `~200kb` in size before including some monstrous extra deps. I guess
`angular` and I will just never get along.
- __mercury:__ ah, `mercury` is an interesting one. It seemed like a brilliant
idea until I started using it - the abstractions felt heavy, and it took team
members a long time to pick up. In the end I think using `mercury` helped
shaped `choo` greatly, despite not working out for me.
- __deku:__ `deku` is fun. I even contributed a bit in the early days. It could
probably best be described as "a functional version of `react`". The
dependence on `JSX` isn't great, but give it a shot if you think it looks
neat.
- __cycle:__ `cycle`'s pretty good - unlike most frameworks it lays out a clear
architecture which helps with reasoning about it. That said, it's built on
`virtual-dom` and `xstream` which are a bit heavy for my taste. `choo` works
pretty well for FRP style programming, but something like [inu] might be
an interesting alternative.
- __vue:__ like `cycle`, `vue` is pretty good. But it also uses tech that
provides framework lock in, and additionally doesn't have a clean enough
architecture. I appreciate what it does, but don't think it's the answer.
### Why can't send() be called on the server?
In Node, `reducers`, `effects` and `subscriptions` are disabled for performance
reasons, so if `send()` was called to trigger an action it wouldn't work. Try
finding where in the DOM tree `send()` is called, and disable it when called
from within Node.
### Which packages was choo built on?
- __views:__ [`yo-yo`][yo-yo], [`bel`][bel]
- __models:__ [`barracks`](https://github.com/yoshuawuyts/barracks),
[`xtend`](https://github.com/raynos/xtend)
- __routes:__ [`sheet-router`][sheet-router]
- __http:__ [`xhr`](https://github.com/Raynos/xhr)
### Does choo use a virtual-dom? ### Does choo use a virtual-dom?
`choo` uses [morphdom][morphdom], which diffs real DOM nodes instead of virtual `choo` uses [nanomorph][nanomorph], which diffs real DOM nodes instead of
nodes. It turns out that [browsers are actually ridiculously good at dealing virtual nodes. It turns out that [browsers are actually ridiculously good at
with DOM nodes][morphdom-bench], and it has the added benefit of working with dealing with DOM nodes][morphdom-bench], and it has the added benefit of
_any_ library that produces valid DOM nodes. So to put a long answer short: working with _any_ library that produces valid DOM nodes. So to put a long
we're using something even better. answer short: we're using something even better.
### How can I optimize choo? ### How can I support older browsers?
`choo` really shines when coupled with `browserify` transforms. They can do Template strings aren't supported in all browsers, and parsing them creates
things like reduce file size, prune dependencies and clean up boilerplate code. significant overhead. To optimize we recommend running `browserify` with
Consider running some of the following: [yo-yoify][yo-yoify] as a global transform or using [bankai][bankai] directly.
- [unassertify](https://github.com/twada/unassertify) - remove `assert()`
statements which reduces file size. Use as a `--global` transform
- [es2020](https://github.com/yoshuawuyts/es2020) - backport `const`,
`arrow functions` and `template strings` to older browsers. Should be run as
a `--global` transform
- [yo-yoify](https://github.com/shama/yo-yoify) - replace the internal `hyperx`
dependency with `document.createElement` calls; greatly speeds up performance
too
- [uglifyify](https://github.com/hughsk/uglifyify) - minify your code using
UglifyJS2. Use as a `--global` transform
- [bulkify](https://www.npmjs.com/package/bulkify) - transform inline
[bulk-require](https://www.npmjs.com/package/bulk-require) calls into
statically resolvable require maps
- [envify](https://github.com/hughsk/envify) - replace `process.env` values
with plain strings
### Choo + Internet Explorer &amp; Safari
Out of the box `choo` only supports runtimes which support:
* `const`
* `arrow functions` (e.g. `() => {}`)
* `template strings`
This does not include Safari 9 or any version of IE. If support for these
platforms is required you will have to provide some sort of transform that
makes this functionality available in older browsers. The test suite uses
[es2020](https://github.com/yoshuawuyts/es2020) as a global transform, but
anything else which might satisfy this requirement is fair game.
Generally for production builds you'll want to run:
```sh ```sh
$ NODE_ENV=production browserify \ $ browserify -g yo-yoify
-t envify \
-g yo-yoify \
-g unassertify \
-g es2020 \
-g uglifyify \
| uglifyjs
``` ```
## Hey, doesn't this look a lot like Elm? ### Is choo production ready?
Yup, it's greatly inspired by the `elm` architecture. But contrary to `elm`,
`choo` doesn't introduce a completely new language to build web applications.
### Is it production ready?
Sure. Sure.
## Browser Test Status ## API
<a href="https://saucelabs.com/u/yoshuawuyts"> This section provides documentation on how each function in `choo` works. It's
<img intended to be a technical reference. If you're interested in learning choo for
src="https://saucelabs.com/browser-matrix/yoshuawuyts.svg" the first time, consider reading through the [handbook][handbook] first
alt="Sauce Test Status"/> :sparkles:
</a>
### `app = choo([opts])`
Initialize a new `choo` instance. `opts` can also contain the following values:
- __opts.history:__ default: `true`. LIsten for url changes through the
history API.
- __opts.href:__ default: `true`. Handle all relative `<a
href="<location>"></a>` clicks and call `emit('render')`
### `app.use(callback(state, emitter))`
Call a function and pass it a `state` and `emitter`. `emitter` is an instance
of [nanobus](https://github.com/yoshuawuyts/nanobus/). You can listen to
messages by calling `emitter.on()` and emit messages by calling `bus.emit()`.
Choo fires messages when certain events happen:
- __`.on('DOMContentLoaded')`__: when the DOM has succesfully finished loading
- __`.on('render')`__: when the DOM re-renders
- __`.on('pushState')`__: when the history API is triggered
### `app.route(routeName, handler)`
Register a route on the router
### `app.mount(selector)`
Start the application and mount it on the given `querySelector`
### `tree = app.start()`
Start the application. Returns a tree of DOM nodes that can be mounted using
`document.body.appendChild()`.
### `app.toString(location, [state])`
Render the application to a string. Useful for rendering on the server.
`.use()` calls w
### `framework/html`
Create DOM nodes from template string literals. Exposes
[bel](https://github.com/shama/bel). Can be optimized using
[yo-yoify][yo-yoify].
## Installation ## Installation
```sh ```sh
@@ -732,13 +281,7 @@ $ npm install choo
``` ```
## See Also ## See Also
- [choo-handbook](https://github.com/yoshuawuyts/choo-handbook) - the little - [bankai](https://github.com/yoshuawuyts/bankai) - streaming asset compiler
`choo` guide
- [awesome-choo](https://github.com/YerkoPalma/awesome-choo) - Awesome things
related with choo framework
- [kaktus](https://github.com/kaktus/kaktus) - A new minimalistic web browser
- [budo](https://github.com/mattdesl/budo) - quick prototyping tool for
`browserify`
- [stack.gl](http://stack.gl/) - open software ecosystem for WebGL - [stack.gl](http://stack.gl/) - open software ecosystem for WebGL
- [yo-yo](https://github.com/maxogden/yo-yo) - tiny library for modular UI - [yo-yo](https://github.com/maxogden/yo-yo) - tiny library for modular UI
- [bel](https://github.com/shama/bel) - composable DOM elements using template - [bel](https://github.com/shama/bel) - composable DOM elements using template
@@ -747,9 +290,6 @@ $ npm install choo
humans humans
- [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for - [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for
`browserify` `browserify`
- [pull-stream](https://github.com/pull-stream/pull-stream) - minimal streams
- [es2020](https://github.com/yoshuawuyts/es2020) - because in hindsight we
don't need most of ES6
## Support ## Support
Creating a quality framework takes a lot of time. Unlike others frameworks, Creating a quality framework takes a lot of time. Unlike others frameworks,
@@ -840,8 +380,9 @@ Become a backer, and buy us a coffee (or perhaps lunch?) every month or so.
[es2020]: https://github.com/yoshuawuyts/es2020 [es2020]: https://github.com/yoshuawuyts/es2020
[handbook]: https://github.com/yoshuawuyts/choo-handbook [handbook]: https://github.com/yoshuawuyts/choo-handbook
[hyperx]: https://github.com/substack/hyperx [hyperx]: https://github.com/substack/hyperx
[inu]: https://github.com/ahdinosaur/inu
[morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks [morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks
[morphdom]: https://github.com/patrick-steele-idem/morphdom [nanomorph]: https://github.com/yoshuawuyts/nanomorph
[sheet-router]: https://github.com/yoshuawuyts/sheet-router [nanorouter]: https://github.com/yoshuawuyts/nanorouter
[yo-yo]: https://github.com/maxogden/yo-yo [yo-yo]: https://github.com/maxogden/yo-yo
[yo-yoify]: https://github.com/shama/yo-yoify
[unassertify]: https://github.com/unassert-js/unassertify
+350
View File
@@ -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')
}
}
+23
View File
@@ -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
View File
@@ -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')
-42
View File
@@ -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)
-16
View File
@@ -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"
}
}
-33
View File
@@ -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)
-31
View File
@@ -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)
})
}
-52
View File
@@ -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)
})
}
}
}
-13
View File
@@ -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"
}
}
-45
View File
@@ -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
}
-14
View File
@@ -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>
`
}
-23
View File
@@ -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)
-36
View File
@@ -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>
`
}
-34
View File
@@ -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>
`
}
-83
View File
@@ -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>
`
}
-30
View File
@@ -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>
`
}
-11
View File
@@ -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>
`
}
-23
View File
@@ -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: { }
}
-16
View File
@@ -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: { }
}
-16
View File
@@ -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: { }
}
-25
View File
@@ -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"
}
}
-30
View File
@@ -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
}
-19
View File
@@ -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>
`
}
-15
View File
@@ -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>
`
}
-22
View File
@@ -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: '/' })
}
}
-23
View File
@@ -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)
}
-15
View File
@@ -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"
}
}
-45
View File
@@ -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)
}
-25
View File
@@ -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>
`
}
-51
View File
@@ -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()
}
}
}
}
-14
View File
@@ -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"
}
}
-55
View File
@@ -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
}
-14
View File
@@ -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>
`
}
-13
View File
@@ -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)
-33
View File
@@ -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)
})
}
]
}
-19
View File
@@ -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"
}
}
-15
View File
@@ -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}`
}
-36
View File
@@ -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>
`
}
-36
View File
@@ -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)
-16
View File
@@ -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"
}
}
-38
View File
@@ -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
View File
@@ -1 +1 @@
module.exports = require('yo-yo') module.exports = require('bel')
+95 -191
View File
@@ -1,216 +1,120 @@
const createLocation = require('sheet-router/create-location') var documentReady = require('document-ready')
const onHistoryChange = require('sheet-router/history') var nanorouter = require('nanorouter')
const sheetRouter = require('sheet-router') var nanomount = require('nanomount')
const onHref = require('sheet-router/href') var nanomorph = require('nanomorph')
const walk = require('sheet-router/walk') var nanoraf = require('nanoraf')
const mutate = require('xtend/mutable') var nanobus = require('nanobus')
const barracks = require('barracks') var assert = require('assert')
const nanoraf = require('nanoraf')
const assert = require('assert')
const xtend = require('xtend')
const yo = require('yo-yo')
module.exports = choo var onHistoryChange = require('./lib/history')
var onHref = require('./lib/href')
// framework for creating sturdy web applications module.exports = Framework
// null -> fn
function choo (opts) { function Framework (opts) {
opts = opts || {} opts = opts || {}
const _store = start._store = barracks() var routerOpts = {
var _router = start._router = null default: opts.defaultRoute || '/404',
var _routerOpts = null curry: true
var _rootNode = null
var _routes = null
var _frame = null
if (typeof window !== 'undefined') {
_store.use({ onStateChange: render })
}
_store.use(opts)
start.toString = toString
start.router = router
start.model = model
start.start = start
start.stop = _store.stop
start.use = use
return start
// render the application to a string
// (str, obj) -> str
function toString (route, serverState) {
serverState = serverState || {}
assert.equal(typeof route, 'string', 'choo.app.toString: route must be a string')
assert.equal(typeof serverState, 'object', 'choo.app.toString: serverState must be an object')
_store.start({ subscriptions: false, reducers: false, effects: false })
const state = _store.state({ state: serverState })
const router = createRouter(_routerOpts, _routes, createSend)
const tree = router(route, state)
return tree.outerHTML || tree.toString()
function createSend () {
return function send () {
assert.ok(false, 'choo: send() cannot be called from Node')
}
}
} }
// start the application var router = nanorouter(routerOpts)
// (str?, obj?) -> DOMNode var bus = nanobus()
function start () { var rerender = null
_store.model(createLocationModel(opts)) var tree = null
const createSend = _store.start(opts) var state = {}
_router = start._router = createRouter(_routerOpts, _routes, createSend)
const state = _store.state({state: {}})
const tree = _router(state.location.href, state) return {
assert.ok(tree, 'choo.start: the router should always return a valid DOM node') toString: toString,
assert.equal(typeof tree, 'object', 'choo.start: the router should always return a valid DOM node') use: register,
_rootNode = tree mount: mount,
tree.done = done route: route,
start: start
return tree
// allow a 'mount' function to return the new node
// html -> null
function done (newNode) {
_rootNode = newNode
}
} }
// update the DOM after every state mutation function route (route, handler) {
// (obj, obj, obj, str, fn) -> null router.on(route, function (params) {
function render (state, data, prev, name, createSend) { return function () {
if (!_frame) { state.params = params
_frame = nanoraf(function (state, prev) { return handler(state, emit)
const newTree = _router(state.location.href, state, prev) }
_rootNode = yo.update(_rootNode, newTree)
}) })
} }
_frame(state, prev)
function register (cb) {
cb(state, bus)
} }
// register all routes on the router function start () {
// (str?, [fn|[fn]]) -> obj tree = router(createLocation(), state, emit)
function router (defaultRoute, routes) { rerender = nanoraf(function () {
_routerOpts = defaultRoute var newTree = router(createLocation(), state, emit)
_routes = routes tree = nanomorph(tree, newTree)
} })
// create a new model bus.on('render', rerender)
// (str?, obj) -> null
function model (model) {
_store.model(model)
}
// register a plugin
// (obj) -> null
function use (hooks) {
assert.equal(typeof hooks, 'object', 'choo.use: hooks should be an object')
_store.use(hooks)
}
// create a new router with a custom `createRoute()` function
// (str?, obj) -> null
function createRouter (routerOpts, routes, createSend) {
var prev = null
if (!routes) {
routes = routerOpts
routerOpts = {}
}
routerOpts = mutate({ thunk: 'match' }, routerOpts)
const router = sheetRouter(routerOpts, routes)
walk(router, wrap)
return router
function wrap (route, handler) {
const send = createSend('view: ' + route, true)
return function chooWrap (params) {
return function (state) {
// TODO(yw): find a way to wrap handlers so params shows up in state
const nwState = xtend(state)
nwState.location = xtend(nwState.location, { params: params })
const nwPrev = prev
prev = nwState // save for next time
if (opts.freeze !== false) Object.freeze(nwState)
return handler(nwState, nwPrev, send)
}
}
}
}
}
// application location model
// obj -> obj
function createLocationModel (opts) {
return {
namespace: 'location',
state: mutate(createLocation(), { params: {} }),
subscriptions: createSubscriptions(opts),
effects: { set: setLocation, touch: touchLocation },
reducers: { update: updateLocation }
}
// update the location on the state
// try and jump to an anchor on the page if it exists
// (obj, obj) -> obj
function updateLocation (state, data) {
if (opts.history !== false && data.hash && data.hash !== state.hash) {
try {
const el = document.querySelector(data.hash)
if (el) el.scrollIntoView(true)
} catch (e) {
return data
}
}
return data
}
// update internal location only
// (str, obj, fn, fn) -> null
function touchLocation (state, data, send, done) {
const newLocation = createLocation(state, data)
send('location:update', newLocation, done)
}
// set a new location e.g. "/foo/bar#baz?beep=boop"
// (str, obj, fn, fn) -> null
function setLocation (state, data, send, done) {
const newLocation = createLocation(state, data)
// update url bar if it changed
if (opts.history !== false && newLocation.href !== state.href) {
window.history.pushState({}, null, newLocation.href)
}
send('location:update', newLocation, done)
}
function createSubscriptions (opts) {
const subs = {}
if (opts.history !== false) { if (opts.history !== false) {
subs.handleHistory = function (send, done) { onHistoryChange(function (href) {
onHistoryChange(function navigate (href) { bus.emit('pushState', window.location.href)
send('location:touch', href, done) scrollIntoView()
}) })
}
}
if (opts.href !== false) { if (opts.href !== false) {
subs.handleHref = function (send, done) { onHref(function (location) {
onHref(function navigate (location) { var href = location.href
send('location:set', location, done) var currHref = window.location.href
if (href === currHref) return
window.history.pushState({}, null, href)
bus.emit('pushState', window.location.href)
bus.emit('render')
scrollIntoView()
}) })
} }
} }
return subs documentReady(function () {
bus.emit('DOMContentLoaded')
})
return tree
}
function emit (eventName, data) {
bus.emit(eventName, data)
}
function mount (selector) {
var newTree = start()
documentReady(function () {
var root = document.querySelector(selector)
assert.ok(root, 'could not query selector: ' + selector)
nanomount(root, newTree)
tree = root
})
}
function toString (location, _state) {
state = _state || {}
var html = router(location)
assert.equal()
return html.toString()
} }
} }
function scrollIntoView () {
var hash = window.location.hash
if (hash) {
try {
var el = document.querySelector(hash)
if (el) el.scrollIntoView(true)
} catch (e) {}
}
}
function createLocation () {
var pathname = window.location.pathname.replace(/\/$/, '')
var hash = window.location.hash.replace(/^#/, '/')
return pathname + hash
}
+20
View File
@@ -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
View File
@@ -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)
}
}
-40
View File
@@ -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
View File
@@ -1,23 +1,15 @@
{ {
"name": "choo", "name": "choo",
"version": "4.1.0", "version": "5.0.0-5",
"description": "A 5kb framework for creating sturdy frontend applications", "description": "A 5kb framework for creating sturdy frontend applications",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"deps": "./scripts/test deps", "deps": "dependency-check --entry ./html.js . && dependency-check . --extra --no-dev --entry ./html.js",
"test:electron": "./scripts/test electron", "start": "bankai start example --open",
"test:cov": "./scripts/test cov", "test": "standard && npm run deps && node test.js",
"test:server": "./scripts/test server", "build:dev": "echo 'to be implemented'",
"test:browser": "./scripts/test browser", "build:min": "echo 'to be implemented'",
"test:browser-local": "./scripts/test browser-local", "prepublish": "npm run build:dev && npm run build:min"
"preversion": "if [ ! -z $SKIP_TEST ]; then npm run test:browser; fi",
"test": "npm run test:electron",
"build:dev": "./scripts/build dev",
"build:min": "./scripts/build min",
"prepublish": "npm run build:dev && npm run build:min",
"instrument:discify": "./scripts/instrument discify",
"instrument:minified": "./scripts/instrument minified",
"instrument:gzip": "./scripts/instrument gzip"
}, },
"repository": "yoshuawuyts/choo", "repository": "yoshuawuyts/choo",
"keywords": [ "keywords": [
@@ -28,53 +20,21 @@
"composable", "composable",
"tiny" "tiny"
], ],
"files": [
"index.js",
"mount.js",
"html.js",
"dist/"
],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"barracks": "^9.1.0", "bel": "^4.5.1",
"document-ready": "~1.0.2", "document-ready": "^1.0.3",
"global": "^4.3.0", "global": "^4.3.1",
"nanoraf": "^2.1.1", "nanobus": "^2.1.0",
"sheet-router": "^4.0.1", "nanomorph": "^4.0.0",
"xtend": "^4.0.1", "nanomount": "^1.0.0",
"yo-yo": "^1.2.2" "nanoraf": "^3.0.0",
"nanorouter": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"append-child": "~1.0.0", "dependency-check": "^2.8.0",
"bankai": "^3.1.0", "spok": "^0.7.0",
"browserify": "^13.0.1", "standard": "^9.0.1",
"browserify-istanbul": "^2.0.0", "tape": "^4.6.3"
"bundle-collapser": "^1.2.1",
"dependency-check": "^2.5.1",
"disc": "^1.3.2",
"envify": "^3.4.1",
"es2020": "^1.0.1",
"geval": "~2.1.1",
"gzip-size-cli": "^1.0.0",
"insert-css": "^1.0.0",
"istanbul": "^0.4.4",
"min-document": "~2.19.0",
"pretty-bytes-cli": "^2.0.0",
"proxyquire": "~1.7.10",
"proxyquire-universal": "~1.0.8",
"proxyquireify": "~3.2.0",
"server-router": "^3.0.0",
"sheetify": "^5.1.0",
"standard": "^8.0.0",
"standard-markdown": "^2.2.0",
"tachyons": "^4.0.0-beta.19",
"tape": "^4.5.1",
"tape-istanbul": "~1.0.2",
"tape-run": "~2.1.4",
"uglifyify": "^3.0.2",
"uglifyjs": "^2.4.10",
"unassertify": "^2.0.3",
"yo-yoify": "^3.1.0",
"zuul": "toddself/zuul"
} }
} }
-68
View File
@@ -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
-83
View File
@@ -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
-78
View File
@@ -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
View File
@@ -0,0 +1 @@
// var tape = require('tape')
-63
View File
@@ -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))
})
-52
View File
@@ -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()
})
-54
View File
@@ -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))
})
-38
View File
@@ -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')
})
})
-171
View File
@@ -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, '/')
}
-98
View File
@@ -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)
})
})