From a7916ec3f96e82bc793ba48bf1da85f3e129ce74 Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Sun, 11 Dec 2016 19:35:29 +0100 Subject: [PATCH] [major] choo v4 (#352) * location: change url on location:setLocation - [ ] don't break hashing - [ ] allow not changing the url fixup! move fns around fixup! add search string * app.start: clean * walk: add * location: update arg calls * fixup! location: update * uri-wrap: fix thunking * router: update to latest sheet-router (#239) * router: update to latest sheet-router * tests: fix for latest version * tests: fix SSR * tests: fix history * tests: spruce up * docs: update example * examples: update * deps: bump sheet-router * 4.0.0-0 * chore(changelog): 4.0.0 (#211) * feat(api:) arg order (#268) * s/data, state/state, data/ * feat(api): swap arguments * fix(href): fix routing (#271) * feat(http): remove (#269) * feat(router): enable hash routing (#273) * deps: fix mount * 4.0.0-1 * feat(mount): copy {script,link} tags * 4.0.0-2 * fix(mount): forEach -> for Lol can't use forEach * fix(router): use state.location.href (#282) * fix(mount): use deep node clone * 4.0.0-3 * fix(deps): remove hash-match * 4.0.0-4 * fix(mount): return node * 4.0.0-5 * fix(router): check if a hash is a valid selector (#339) * 4.0.0-6 * fix(router): pass params on newstate (#343) * 4.0.0-7 * feat(docs): update for 4.0.0 (#320) * feat(docs): update for 4.0.0 * docs: update router example in readme (#337) * chore(changelog): update for v4 (#351) --- CHANGELOG.md | 29 ++ README.md | 308 +++++++++--------- examples/async-counter/client.js | 14 +- examples/async-counter/package.json | 4 +- examples/http/client.js | 9 +- examples/http/models/api.js | 6 +- examples/http/models/error.js | 4 +- examples/mailbox/client.js | 15 +- examples/mailbox/package.json | 1 + examples/mailbox/views/mailbox.js | 5 + examples/server-rendering/client.js | 10 +- .../{views/main.js => view.js} | 2 +- examples/sse/client.js | 18 +- examples/stopwatch/client.js | 4 +- examples/stopwatch/models/stopwatch.js | 12 +- examples/stopwatch/package.json | 4 +- examples/title/client.js | 9 +- examples/title/package.json | 7 +- examples/vanilla/index.html | 71 ++-- http.js | 1 - index.js | 185 ++++++----- mount.js | 40 +++ package.json | 11 +- scripts/test | 7 +- tests/browser/basic.js | 21 +- tests/browser/freeze.js | 28 +- tests/browser/hooks.js | 28 +- tests/browser/rehydration.js | 35 +- tests/browser/routing.js | 156 ++++----- tests/server/index.js | 50 +-- 30 files changed, 580 insertions(+), 514 deletions(-) rename examples/server-rendering/{views/main.js => view.js} (96%) delete mode 100644 http.js create mode 100644 mount.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 322a7d29..39adb97c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +## `4.0.0` The routing patch +This patch changes the way we handle routes. It introduces query string +support (!), and changes the router to use a lisp-like syntax. It also inverts +the argument order of effects and reducers to be more intuitive. We also +managed to sneak in some performance upgrades :sparkles: - We hope you enjoy +it! + +### changes +- :exclamation: slim down server side rendering API | + [issue](https://github.com/yoshuawuyts/choo/issues/191) | + [pull-request](https://github.com/yoshuawuyts/choo/pull/203) +- :exclamation: update router API to be lisp-like +- :exclamation: swap `state` and `data` argument order | + [issue](https://github.com/yoshuawuyts/choo/issues/179) +- :exclamation: remove `choo/http`. Use [xhr](https://github.com/naugtur/xhr) + instead | [pull-request](https://github.com/yoshuawuyts/choo/pull/269) +- update `router` to use memoization | + [issue](https://github.com/yoshuawuyts/sheet-router/issues/17) | + [pull-request](https://github.com/yoshuawuyts/sheet-router/pull/34) +- support inline anchor links | + [issue](https://github.com/yoshuawuyts/choo/issues/65) +- allow bypassing of link clicks in `sheet-router` | + [issue](https://github.com/yoshuawuyts/sheet-router/issues/15) | + [pull-request](https://github.com/yoshuawuyts/sheet-router/pull/27) +- update router API to handle hashes by default +- update router to provide out of the box support for Electron +- update `location` state to expose `search` parameters (query strings) | + [issue](https://github.com/yoshuawuyts/sheet-router/issues/31) + ## `3.3.0` Yay, `plugins` now support `wrappers` which is a segway onto HMR, time travel and other cool plugins. These changes have come through in barracks `v8.3.0` diff --git a/README.md b/README.md index 7054fed7..dd259581 100644 --- a/README.md +++ b/README.md @@ -125,49 +125,50 @@ production, we'd love to hear from you!_ Let's create an input box that changes the content of a textbox in real time. [Click here to see the app running](http://requirebin.com/?gist=229bceda0334cf30e3044d5f5c600960). ```js -const choo = require('choo') -const html = require('choo/html') -const app = choo() +var html = require('choo/html') +var choo = require('choo') +var app = choo() app.model({ state: { title: 'Not quite set yet' }, reducers: { - update: (data, state) => ({ title: data }) + update: function (state, data) { + return { title: data } + } } }) -const mainView = (state, prev, send) => html` -
-

Title: ${state.title}

- send('update', e.target.value)}/> -
-` +function mainView (state, prev, send) { + return html` +
+

Title: ${state.title}

+ +
+ ` + + function update (e) { + send('update', e.target.value) + } +} -app.router((route) => [ - route('/', mainView) -]) +app.router(['/', mainView]) -const tree = app.start() +var tree = app.start() document.body.appendChild(tree) ``` -To run it, save it as `client.js` and run with [budo] and [es2020]. These tools -are convenient but any [browserify] based tool should do: +To run it, save it as `client.js` and run with [bankai][bankai]. `bankai` is +convenient but any [browserify][browserify] based tool should do: ```sh -$ budo client.js -p 8080 --open -- -t es2020 -``` +# run and reload on port 8080 +$ bankai client.js -p 8080 --open + +# compile to static files in `./dist/` +$ bankai build index.js dist/ -And to save the output to files so it can be deployed, open a new terminal and -do: -```bash -$ mkdir -p 'dist/' -$ curl 'localhost:8080' > 'dist/index.html' -$ curl 'localhost:8080/client.js' > 'dist/client.js' +# deploy to github pages using `tschaub/gh-pages` +$ gh-pages -d dist/ ``` -All using a couple of shell commands and `.js` files, no grandiose boilerplate -needed. ## Philosophy We believe programming should be fun and light, not stern and stressful. It's @@ -194,47 +195,36 @@ better results and super smiley faces. ## Concepts `choo` cleanly structures internal data flow, so that all pieces of logic can -be combined into a nice, cohesive machine. Internally all logic lives within -`models` that contain several properties. `subscriptions` are functions that -are called at startup and have `send()` passed in, so they act as read-only -sources of data. `effects` react to changes, perform an `action` and can then -post the results. `reducers` take data, modify it, and update the internal -`state`. - -Communication of data is done using something called `actions`. Each `action` -consists of a unique `actionName` and an optional payload of `data`, which can -be any value. - -When a `reducer` modifies `state`, the `router` is called, which in turn calls -`views`. `views` take `state` and return [DOM] nodes which are then -efficiently rendered on the screen. - -In turn when the `views` are rendered, the `user` can interact with elements by -clicking on them, triggering `actions` which then flow back into the -application logic. This is the _unidirectional_ architecture of `choo`. +be combined into a nice, cohesive machine. Roughly speaking there are two parts +to `choo`: the views and the models. Models take care of state and logic, and +views are responsible for displaying the interface and responding to user +interactions. + +All of `choo`'s state is contained in a single object and whenever it changes +the views receive a new version of the state which they can use to safely +render a complete new representation of the DOM. The DOM is efficiently updated +using DOM diffing/patching. + +The logic in choo exist in three different kinds of actions, each with their +own role: `effects`, `subscriptions` and `reducers`. + +- __Effects__ makes an asynchronous operation and calls another action when + it's done. + +- __Subscriptions__ (called once when the DOM loads) listens for external input + like keyboard or WebSocket events and then calls another action. + +- __Reducers__ receives the current state and returns an updated version of the + state which is then sent to the views. + ```txt ┌─────────────────┐ │ Subscriptions ─┤ User ───┐ └─ Effects ◀─────┤ ▼ - ┌─ Reducers ◀─────┴──Actions── DOM ◀┐ + ┌─ Reducers ◀─────┴─────────── DOM ◀┐ │ │ └▶ Router ─────State ───▶ Views ────┘ ``` -- __user:__ 🙆 -- __DOM:__ the [Document Object Model][DOM] is what is currently displayed in - your browser -- __actions:__ a named event with optional properties attached. Used to call - `effects` and `reducers` that have been registered in `models` -- __model:__ optionally namespaced object containing `subscriptions`, - `effects`, `reducers` and initial `state` -- __subscriptions:__ read-only data sources that emit `actions` -- __effects:__ asynchronous functions that emit an `action` when done -- __reducers:__ synchronous functions that modify `state` -- __state:__ a single object that contains __all__ the values used in your - application -- __router:__ determines which `view` to render -- __views:__ take `state` and returns a new `DOM tree` that is rendered in the - browser ### Models `models` are objects that contain initial `state`, `subscriptions`, `effects` @@ -249,12 +239,14 @@ Outside the model they're called by `send('todos:add')` and `state.todos.items`. Inside the namespaced model they're called by `send('todos:add')` and `state.items`. An example namespaced model: ```js -const app = choo() +var app = choo() app.model({ namespace: 'todos', state: { items: [] }, reducers: { - add: (data, state) => ({ items: state.items.concat(data.payload) }) + add: function (state, data) { + return { items: state.items.concat(data.payload) } + } } }) ``` @@ -272,52 +264,52 @@ bulk of your logic will be safely shielded, with only a few points touching ever part of your application. ### Effects -Side effects are done through `effects` declared in `app.model()`. Unlike -`reducers` they cannot modify the state by returning objects, but get a -callback passed which is used to emit `actions` to handle results. Use effects -every time you don't need to modify the state object directly, but wish to -respond to an action. - -A typical `effect` flow looks like: - -1. An action is received -2. An effect is triggered -3. The effect performs an async call -4. When the async call is done, either a success or error action is emitted -5. A reducer catches the action and updates the state - -Examples of effects include: performing [xhr] requests (server requests), -calling multiple `reducers`, persisting state to [localstorage]. +`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 -const http = require('choo/http') -const choo = require('choo') -const app = choo() +var choo = require('choo') +var http = require('xhr') +var app = choo() + app.model({ namespace: 'todos', - state: { items: [] }, + state: { values: [] }, + reducers: { + add: function (data, state) { + return { todos: data } + } + }, effects: { - fetch: (data, state, send, done) => { - http('/todos', (err, res, body) => { - send('todos:receive', body, done) + 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) + }) }) } }, - reducers: { - receive: (data, state) => { - return { items: data } + subscriptions: { + 'called-once-when-the-app-loads': function (send, done) { + send('todos:addAndSave', done) } } }) ``` -When an `effect` is done executing, it should call the `done(err, res)` -callback. This callback is used to communicate when an `effect` is done, handle -possible errors, and send values back to the caller. You'll probably notice when -applications become more complex, that composing multiple namespaced models -using higher level effects becomes really powerful - without becoming -complicated. - ### Subscriptions Subscriptions are a way of receiving data from a source. For example when listening for events from a server using `SSE` or `Websockets` for a @@ -325,23 +317,29 @@ chat app, or when catching keyboard input for a videogame. An example subscription that logs `"dog?"` every second: ```js -const app = choo() +var choo = require('choo') + +var app = choo() app.model({ namespace: 'app', - subscriptions: [ - (send, done) => { - setInterval(() => { - send('app:print', { payload: 'dog?', myOtherValue: 1000 }, (err) => { + 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) } - ], - effects: { - print: (data, state) => console.log(data.payload) } }) ``` + If a `subscription` runs into an error, it can call `done(err)` to signal the error to the error hook. @@ -350,13 +348,13 @@ The `router` manages which `views` are rendered at any given time. It also supports rendering a default `view` if no routes match. ```js -const app = choo() -app.router('/404', (route) => [ - route('/', require('./views/empty')), - route('/404', require('./views/error')), - route('/:mailbox', require('./views/mailbox'), [ - route('/:message', require('./views/email')) - ]) +var app = choo() +app.router({ default: '/404' }, [ + [ '/', require('./views/empty') ], + [ '/404', require('./views/error') ], + [ '/:mailbox', require('./views/mailbox'), [ + [ '/:message', require('./views/email') ] + ]] ]) ``` @@ -372,21 +370,29 @@ from within namespaced `models`, and usage should preferably be kept to a minimum. Changing views all over the place tends to lead to messiness. ### Views -Views are pure functions that return a DOM tree for the router to render. They’re passed the current state, and any time the state changes they’re run again with the new state. +Views are pure functions that return a DOM tree for the router to render. +They’re passed the current state, and any time the state changes they’re run +again with the new state. -Views are also passed the `send` function, which they can use to dispatch actions that can update the state. For example, the DOM tree can have an `onclick` handler that dispatches an `add` action. +Views are also passed the `send` function, which they can use to dispatch +actions that can update the state. For example, the DOM tree can have an +`onclick` handler that dispatches an `add` action. -```javascript -const view = (state, prev, send) => { +```js +function view (state, prev, send) { return html`

Total todos: ${state.todos.length}

- -
` + + + ` + + function addTodo (e) { + send('add', { title: 'demo' }) + } } ``` + In this example, when the `Add` button is clicked, the view will dispatch an `add` action that the model’s `add` reducer will receive. [As seen above](#models), the reducer will add an item to the state’s `todos` array. The @@ -401,13 +407,13 @@ somewhere. This is done through something called `plugins`. Plugins are objects that contain `hook` and `wrap` functions and are passed to `app.use()`: ```js -const log = require('choo-log') -const choo = require('choo') -const app = choo() +var log = require('choo-log') +var choo = require('choo') +var app = choo() app.use(log()) -const tree = app.start() +var tree = app.start() document.body.appendChild(tree) ``` @@ -427,9 +433,9 @@ and `wrappers` are available, head on over to [app.use()](#appusehooks). Using `choo` in a project? Show off which version you've used using a badge: -[![built with choo v3](https://img.shields.io/badge/built%20with%20choo-v3-ffc3e4.svg?style=flat-square)](https://github.com/yoshuawuyts/choo) +[![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 v3](https://img.shields.io/badge/built%20with%20choo-v3-ffc3e4.svg?style=flat-square)](https://github.com/yoshuawuyts/choo) +[![built with choo v4](https://img.shields.io/badge/built%20with%20choo-v4-ffc3e4.svg?style=flat-square)](https://github.com/yoshuawuyts/choo) ``` ## API @@ -449,9 +455,9 @@ arguments: and handlers in other models - __state:__ initial values of `state` inside the model - __reducers:__ synchronous operations that modify state. Triggered by - `actions`. Signature of `(data, state)`. + `actions`. Signature of `(state, data)`. - __effects:__ asynchronous operations that don't modify state directly. - Triggered by `actions`, can call `actions`. Signature of `(data, state, + 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)`. @@ -490,9 +496,9 @@ There are several `hooks` and `wrappers` that are picked up by `choo`: - __onError(err, state, createSend):__ called when an `effect` or `subscription` emit an error. If no handler is passed, the default handler will `throw` on each error. -- __onAction(data, state, name, caller, createSend):__ called when an +- __onAction(state, data, name, caller, createSend):__ called when an `action` is fired. -- __onStateChange(data, state, prev, caller, createSend):__ called after a +- __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 @@ -523,12 +529,9 @@ 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(rootId?, opts) +### tree = app.start(opts) Start the application. Returns a tree of DOM nodes that can be mounted using -`document.body.appendChild()`. If a valid `id` selector is passed in as the -first argument, the tree will diff against the selected node rather than be -returned. This is useful for [rehydration](https://github.com/yoshuawuyts/choo-handbook/blob/master/rendering-in-node.md#rehydration). Opts can contain the -following values: +`document.body.appendChild()`. Opts can 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. @@ -549,10 +552,14 @@ current `state`, `prev` is the last state, `state.params` is URI partials and To create listeners for events, create interpolated attributes on elements. ```js -const html = require('choo/html') +var html = require('choo/html') html` - + ` + +function log (e) { + console.log(e) +} ``` Example listeners include: `onclick`, `onsubmit`, `oninput`, `onkeydown`, `onkeyup`. A full list can be found [at the yo-yo @@ -660,8 +667,8 @@ Consider running some of the following: - [unassertify](https://github.com/twada/unassertify) - remove `assert()` statements which reduces file size. Use as a `--global` transform - [es2020](https://github.com/yoshuawuyts/es2020) - backport `const`, - `fat-arrows` and `template strings` to older browsers. Should be run as a - `--global` transform + `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 @@ -676,8 +683,8 @@ Consider running some of the following: ### Choo + Internet Explorer & Safari Out of the box `choo` only supports runtimes which support: * `const` -* `fat-arrow` functions (e.g. `() => {}`) -* `template-strings` +* `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 @@ -814,35 +821,18 @@ Become a backer, and buy us a coffee (or perhaps lunch?) every month or so. - ## License [MIT](https://tldrlegal.com/license/mit-license) +[bankai]: https://github.com/yoshuawuyts/bankai [bel]: https://github.com/shama/bel -[big-o]: https://rob-bell.net/2009/06/a-beginners-guide-to-big-o-notation/ -[bl]: https://github.com/rvagg/bl [browserify]: https://github.com/substack/node-browserify [budo]: https://github.com/mattdesl/budo -[DOM]: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model -[dom]: https://en.wikipedia.org/wiki/Document_Object_Model [es2020]: https://github.com/yoshuawuyts/es2020 [handbook]: https://github.com/yoshuawuyts/choo-handbook -[html-input]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input [hyperx]: https://github.com/substack/hyperx [inu]: https://github.com/ahdinosaur/inu -[isomorphic]: https://en.wikipedia.org/wiki/Isomorphism -[keyboard-support]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Browser_compatibility -[localstorage]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage -[module-parent]: https://nodejs.org/dist/latest-v6.x/docs/api/modules.html#modules_module_parent [morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks [morphdom]: https://github.com/patrick-steele-idem/morphdom -[nginx]: http://nginx.org/ -[qps]: https://en.wikipedia.org/wiki/Queries_per_second [sheet-router]: https://github.com/yoshuawuyts/sheet-router -[sse-reconnect]: http://stackoverflow.com/questions/24564030/is-an-eventsource-sse-supposed-to-try-to-reconnect-indefinitely -[sse]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events -[varnish]: https://varnish-cache.org -[ws-reconnect]: http://stackoverflow.com/questions/13797262/how-to-reconnect-to-websocket-after-close-connection -[ws]: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API -[xhr]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest [yo-yo]: https://github.com/maxogden/yo-yo diff --git a/examples/async-counter/client.js b/examples/async-counter/client.js index b64180ab..71cce36f 100644 --- a/examples/async-counter/client.js +++ b/examples/async-counter/client.js @@ -8,20 +8,20 @@ app.model({ counter: 0 }, reducers: { - increment: (data, state) => ({ counter: state.counter + 1 }), - decrement: (data, state) => ({ counter: state.counter - 1 }) + increment: (state, data) => ({ counter: state.counter + 1 }), + decrement: (state, data) => ({ counter: state.counter - 1 }) }, effects: { - incrementAsync: function (data, state, send, done) { + incrementAsync: function (state, data, send, done) { setTimeout(() => send('increment', done), 1000) }, - decrementAsync: function (data, state, send, done) { + decrementAsync: function (state, data, send, done) { setTimeout(() => send('decrement', done), 1000) } } }) -const mainView = (state, prev, send) => { +function mainView (state, prev, send) { const count = state.counter return html` @@ -36,9 +36,7 @@ const mainView = (state, prev, send) => { ` } -app.router((route) => [ - route('/', mainView) -]) +app.router([ '/', mainView ]) const tree = app.start() document.body.appendChild(tree) diff --git a/examples/async-counter/package.json b/examples/async-counter/package.json index a923e547..45f37833 100644 --- a/examples/async-counter/package.json +++ b/examples/async-counter/package.json @@ -4,13 +4,13 @@ "description": "", "main": "client.js", "scripts": { - "start": "budo client.js -p 8080" + "start": "bankai start --entry=client -p 8080" }, "keywords": [], "author": "Juan Soto ", "license": "ISC", "dependencies": { - "budo": "^8.3.0", + "bankai": "^3.2.0", "plur": "^2.1.2" } } diff --git a/examples/http/client.js b/examples/http/client.js index 7ca0145a..b2c92ed5 100644 --- a/examples/http/client.js +++ b/examples/http/client.js @@ -4,18 +4,19 @@ 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 (data, state, name, caller, createSend) { + onAction: function (state, data, name, caller, createSend) { console.groupCollapsed(`Action: ${caller} -> ${name}`) console.log(data) console.groupEnd() }, - onStateChange: function (data, state, prev, createSend) { + onStateChange: function (state, data, prev, createSend) { console.groupCollapsed('State') console.log(prev) console.log(state) @@ -26,9 +27,7 @@ const app = choo({ app.model(require('./models/error')) app.model(require('./models/api')) -app.router((route) => [ - route('/', mainView) -]) +app.router(['/', mainView]) const tree = app.start() document.body.appendChild(tree) diff --git a/examples/http/models/api.js b/examples/http/models/api.js index 9fd58263..5d9067a3 100644 --- a/examples/http/models/api.js +++ b/examples/http/models/api.js @@ -6,13 +6,13 @@ module.exports = { title: 'Button pushing machine 3000' }, reducers: { - set: (data, state) => ({ 'title': data }) + set: (state, data) => ({ 'title': data }) }, effects: { - good: function (data, state, send, done) { + good: function (state, data, send, done) { request('/good', send, done) }, - bad: (data, state, send, done) => request('/bad', send, done) + bad: (state, data, send, done) => request('/bad', send, done) } } diff --git a/examples/http/models/error.js b/examples/http/models/error.js index bd1620a4..4530791b 100644 --- a/examples/http/models/error.js +++ b/examples/http/models/error.js @@ -15,13 +15,13 @@ module.exports = { triggerTime: null }, reducers: { - setError: function (data, state) { + setError: function (state, data) { return { errors: state.errors.concat(data.message), errorTimeDone: data.errorTimeDone } }, - 'delError': function (data, state) { + 'delError': function (state, data) { state.errors.shift() return { errors: state.errors } } diff --git a/examples/mailbox/client.js b/examples/mailbox/client.js index a5f118fb..aceae2c3 100644 --- a/examples/mailbox/client.js +++ b/examples/mailbox/client.js @@ -1,19 +1,22 @@ -const choo = require('../../') +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((route) => [ - route('/', require('./views/empty')), - route('/:mailbox', require('./views/mailbox'), [ - route('/:message', require('./views/email')) - ]) +app.router([ + ['/', require('./views/empty')], + ['/:mailbox', require('./views/mailbox'), [ + ['/:message', require('./views/email')] + ]] ]) const tree = app.start() diff --git a/examples/mailbox/package.json b/examples/mailbox/package.json index c14dfc1c..290e124c 100644 --- a/examples/mailbox/package.json +++ b/examples/mailbox/package.json @@ -9,6 +9,7 @@ "author": "Yoshua Wuyts ", "license": "ISC", "dependencies": { + "choo-log": "^1.4.1", "css-wipe": "^4.2.1", "dateformat": "^1.0.12", "pathname-match": "^1.1.3", diff --git a/examples/mailbox/views/mailbox.js b/examples/mailbox/views/mailbox.js index 1fb173ea..582a654e 100644 --- a/examples/mailbox/views/mailbox.js +++ b/examples/mailbox/views/mailbox.js @@ -7,6 +7,7 @@ const nav = require('../elements/nav') module.exports = function (state, prev, send) { return html`
+ ${pathname(state, prev, send)} ${nav(state, prev, send)}
@@ -14,4 +15,8 @@ module.exports = function (state, prev, send) {
` + + function goHome () { + send('location:set', { pathname: '/' }) + } } diff --git a/examples/server-rendering/client.js b/examples/server-rendering/client.js index 7e8283cd..3bcd8f67 100644 --- a/examples/server-rendering/client.js +++ b/examples/server-rendering/client.js @@ -1,6 +1,7 @@ +const mount = require('../../mount') const choo = require('../../') -const mainView = require('./views/main') +const mainView = require('./view') const app = choo() @@ -12,12 +13,11 @@ app.model({ } }) -app.router((route) => [ - route('/', mainView) -]) +app.router(['/', mainView]) if (module.parent) { module.exports = app } else { - app.start('#app-root') + const tree = app.start() + mount('#app-root', tree) } diff --git a/examples/server-rendering/views/main.js b/examples/server-rendering/view.js similarity index 96% rename from examples/server-rendering/views/main.js rename to examples/server-rendering/view.js index f59e9abc..5e2510cf 100644 --- a/examples/server-rendering/views/main.js +++ b/examples/server-rendering/view.js @@ -1,5 +1,5 @@ const assert = require('assert') -const html = require('../../../html') +const html = require('../../html') module.exports = function (state, prev, send) { const serverMessage = state.message.server diff --git a/examples/sse/client.js b/examples/sse/client.js index 8ed40cc7..0552fe40 100644 --- a/examples/sse/client.js +++ b/examples/sse/client.js @@ -3,14 +3,12 @@ const html = require('../../html') const app = choo() app.model(createModel()) -app.router((route) => [ - route('/', mainView) -]) +app.router(['/', mainView]) const tree = app.start() document.body.appendChild(tree) -function mainView (params, state, send) { +function mainView (state, prev, send) { return html`
${state.logger.msg}
` @@ -24,27 +22,27 @@ function createModel () { msg: '' }, subscriptions: [ - function (send) { + function (send, done) { stream.onerror = (e) => { - send('logger:error', { payload: JSON.stringify(e) }) + send('logger:error', { payload: JSON.stringify(e) }, done) } stream.onmessage = (e) => { const msg = JSON.parse(e.data).message - send('logger:print', { payload: msg }) + send('logger:print', { payload: msg }, done) } } ], reducers: { - 'print': (data, state) => { + 'print': (state, data) => { return ({ msg: state.msg + ' ' + data.payload }) } }, effects: { - close: (data, state, send, done) => { + close: (state, data, send, done) => { stream.close() done() }, - error: (data, state, send, done) => { + error: (state, data, send, done) => { console.error(`error: ${data.payload}`) done() } diff --git a/examples/stopwatch/client.js b/examples/stopwatch/client.js index abf3d1f1..e69e268f 100644 --- a/examples/stopwatch/client.js +++ b/examples/stopwatch/client.js @@ -7,9 +7,7 @@ const app = choo() app.model(stopwatch) -app.router((route) => [ - route('/', mainView) -]) +app.router(['/', mainView]) const tree = app.start() document.body.appendChild(tree) diff --git a/examples/stopwatch/models/stopwatch.js b/examples/stopwatch/models/stopwatch.js index cb540710..dec45543 100644 --- a/examples/stopwatch/models/stopwatch.js +++ b/examples/stopwatch/models/stopwatch.js @@ -8,14 +8,14 @@ module.exports = { laps: [] }, reducers: { - start: (data, state) => ({ start: true, startTime: Date.now() - state.elapsed }), - stop: (data, state) => ({ start: false }), - update: (data, state) => ({ elapsed: data }), - reset: (data, state) => ({ startTime: Date.now(), elapsed: 0, laps: [] }), - add: (data, state) => ({ laps: state.laps.concat(data) }) + 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: (data, state, send, done) => { + now: (state, data, send, done) => { if (state.start) { let elapsed = data - state.startTime send('update', elapsed, done) diff --git a/examples/stopwatch/package.json b/examples/stopwatch/package.json index 8011f0e4..dc12490a 100644 --- a/examples/stopwatch/package.json +++ b/examples/stopwatch/package.json @@ -4,13 +4,13 @@ "description": "", "main": "client.js", "scripts": { - "start": "budo client.js -p 8080 -- -t es2020" + "start": "bankai start --entry=client.js -p 8080" }, "keywords": [], "author": "traducer ", "license": "ISC", "dependencies": { - "budo": "^8.3.0", + "bankai": "^3.2.0", "raf": "^3.2.0" }, "devDependencies": { diff --git a/examples/title/client.js b/examples/title/client.js index f4f9e486..41bb0ff5 100644 --- a/examples/title/client.js +++ b/examples/title/client.js @@ -8,10 +8,10 @@ app.model({ title: 'my demo app' }, reducers: { - update: (data, state) => ({ title: data.payload }) + update: (state, data) => ({ title: data.payload }) }, effects: { - update: (data, state, send, done) => { + update: (state, data, send, done) => { document.title = data.payload done() } @@ -31,9 +31,6 @@ const mainView = (state, prev, send) => { ` } -app.router((route) => [ - route('/', mainView) -]) - +app.router(['/', mainView]) const tree = app.start() document.body.appendChild(tree) diff --git a/examples/title/package.json b/examples/title/package.json index 731e7c1b..64000b17 100644 --- a/examples/title/package.json +++ b/examples/title/package.json @@ -4,12 +4,13 @@ "description": "", "main": "client.js", "scripts": { - "start": "budo client.js -p 8080" + "start": "bankai start --entry=client.js -p 8080 --open" }, "keywords": [], "author": "Yoshua Wuyts ", "license": "ISC", - "dependencies": { - "budo": "^8.3.0" + "dependencies": {}, + "devDependencies": { + "bankai": "^3.2.0" } } diff --git a/examples/vanilla/index.html b/examples/vanilla/index.html index e6ec9f1e..72dcd718 100644 --- a/examples/vanilla/index.html +++ b/examples/vanilla/index.html @@ -1,43 +1,38 @@ - - Vanilla example - - - - - + + - + app.router(['/', mainView]) + const tree = app.start() + document.body.appendChild(tree) + + diff --git a/http.js b/http.js deleted file mode 100644 index 278c396e..00000000 --- a/http.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('xhr') diff --git a/index.js b/index.js index 20f6a570..87647563 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,9 @@ -const history = require('sheet-router/history') +const createLocation = require('sheet-router/create-location') +const onHistoryChange = require('sheet-router/history') const sheetRouter = require('sheet-router') -const document = require('global/document') -const onReady = require('document-ready') -const href = require('sheet-router/href') -const hash = require('sheet-router/hash') -const hashMatch = require('hash-match') +const onHref = require('sheet-router/href') +const walk = require('sheet-router/walk') +const mutate = require('xtend/mutable') const barracks = require('barracks') const nanoraf = require('nanoraf') const assert = require('assert') @@ -20,7 +19,7 @@ function choo (opts) { const _store = start._store = barracks() var _router = start._router = null - var _defaultRoute = null + var _routerOpts = null var _rootNode = null var _routes = null var _frame = null @@ -45,7 +44,7 @@ function choo (opts) { _store.start({ subscriptions: false, reducers: false, effects: false }) const state = _store.state({ state: serverState }) - const router = createRouter(_defaultRoute, _routes, createSend) + const router = createRouter(_routerOpts, _routes, createSend) const tree = router(route, state) return tree.outerHTML || tree.toString() @@ -58,38 +57,31 @@ function choo (opts) { // start the application // (str?, obj?) -> DOMNode - function start (selector, startOpts) { - if (!startOpts && typeof selector !== 'string') { - startOpts = selector - selector = null - } - startOpts = startOpts || {} - - _store.model(appInit(startOpts)) - const createSend = _store.start(startOpts) - _router = start._router = createRouter(_defaultRoute, _routes, createSend) + function start () { + _store.model(createLocationModel(opts)) + const createSend = _store.start(opts) + _router = start._router = createRouter(_routerOpts, _routes, createSend) const state = _store.state({state: {}}) - if (!selector) { - const tree = _router(state.location.pathname, state) - _rootNode = tree - return tree - } else { - onReady(function onReady () { - const oldTree = document.querySelector(selector) - assert.ok(oldTree, 'could not query selector: ' + selector) - const newTree = _router(state.location.pathname, state) - _rootNode = yo.update(oldTree, newTree) - }) + const tree = _router(state.location.href, state) + _rootNode = tree + tree.done = done + + return tree + + // allow a 'mount' function to return the new node + // html -> null + function done (newNode) { + _rootNode = newNode } } // update the DOM after every state mutation // (obj, obj, obj, str, fn) -> null - function render (data, state, prev, name, createSend) { + function render (state, data, prev, name, createSend) { if (!_frame) { _frame = nanoraf(function (state, prev) { - const newTree = _router(state.location.pathname, state, prev) + const newTree = _router(state.location.href, state, prev) _rootNode = yo.update(_rootNode, newTree) }) } @@ -99,7 +91,7 @@ function choo (opts) { // register all routes on the router // (str?, [fn|[fn]]) -> obj function router (defaultRoute, routes) { - _defaultRoute = defaultRoute + _routerOpts = defaultRoute _routes = routes } @@ -117,70 +109,103 @@ function choo (opts) { } // create a new router with a custom `createRoute()` function - // (str?, obj, fn?) -> null - function createRouter (defaultRoute, routes, createSend) { - var prev = { params: {} } - return sheetRouter(defaultRoute, routes, createRoute) - - function createRoute (routeFn) { - return function (route, inline, child) { - if (typeof inline === 'function') { - inline = wrap(inline, route) - } - return routeFn(route, inline, child) - } + // (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 (child, route) { - const send = createSend('view: ' + route, true) - return function chooWrap (params, state) { + function wrap (route, handler) { + const send = createSend('view: ' + route, true) + return function chooWrap (params) { + return function (state) { const nwPrev = prev - const nwState = prev = xtend(state, { params: params }) + prev = state + + // TODO(yw): find a way to wrap handlers so params shows up in state + const nwState = xtend(state) + nwState.location = xtend(nwState.location, { params: params }) + if (opts.freeze !== false) Object.freeze(nwState) - return child(nwState, nwPrev, send) + return handler(nwState, nwPrev, send) } } } } } -// initial application state model +// application location model // obj -> obj -function appInit (opts) { - const loc = document.location - const state = { pathname: (opts.hash) ? hashMatch(loc.hash) : loc.href } - const reducers = { - setLocation: function setLocation (data, state) { - return { pathname: data.location.replace(/#.*/, '') } +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 } - // if hash routing explicitly enabled, subscribe to it - const subs = {} - if (opts.hash === true) { - pushLocationSub(function (navigate) { - hash(function (fragment) { - navigate(hashMatch(fragment)) - }) - }, 'handleHash', subs) - } else { - if (opts.history !== false) pushLocationSub(history, 'handleHistory', subs) - if (opts.href !== false) pushLocationSub(href, 'handleHref', subs) + + // 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) } - return { - namespace: 'location', - subscriptions: subs, - reducers: reducers, - state: state + // 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) } - // create a new subscription that modifies - // 'app:location' and push it to be loaded - // (fn, obj) -> null - function pushLocationSub (cb, key, model) { - model[key] = function (send, done) { - cb(function navigate (pathname) { - send('location:setLocation', { location: pathname }, done) - }) + function createSubscriptions (opts) { + const subs = {} + + if (opts.history !== false) { + subs.handleHistory = function (send, done) { + onHistoryChange(function navigate (href) { + send('location:touch', href, done) + }) + } } + + if (opts.href !== false) { + subs.handleHref = function (send, done) { + onHref(function navigate (location) { + send('location:set', location, done) + }) + } + } + + return subs } } diff --git a/mount.js b/mount.js new file mode 100644 index 00000000..580a602b --- /dev/null +++ b/mount.js @@ -0,0 +1,40 @@ +// mount.js +const documentReady = require('document-ready') +const assert = require('assert') +const yo = require('yo-yo') + +module.exports = mount + +// (str, html) -> null +function mount (selector, newTree) { + assert.equal(typeof selector, 'string', 'choo/mount: selector should be a string') + assert.equal(typeof newTree, 'object', 'choo/mount: newTree should be an object') + + const done = newTree.done + + documentReady(function onReady () { + const _rootNode = document.querySelector(selector) + assert.ok(_rootNode, 'could not query selector: ' + selector) + + // copy script tags from the old newTree to the new newTree so + // we can pass a 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) + }) +} diff --git a/package.json b/package.json index 53104a3a..728e1d82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "choo", - "version": "3.3.0", + "version": "4.0.0-7", "description": "A 5kb framework for creating sturdy frontend applications", "main": "index.js", "scripts": { @@ -30,19 +30,17 @@ ], "files": [ "index.js", - "http.js", + "mount.js", "html.js", "dist/" ], "license": "MIT", "dependencies": { - "barracks": "^8.3.1", + "barracks": "^9.0.0", "document-ready": "~1.0.2", "global": "^4.3.0", - "hash-match": "^1.0.2", "nanoraf": "^2.1.1", - "sheet-router": "^3.1.0", - "xhr": "^2.2.0", + "sheet-router": "^4.0.1", "xtend": "^4.0.1", "yo-yo": "^1.2.2" }, @@ -68,6 +66,7 @@ "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", diff --git a/scripts/test b/scripts/test index edf44cf3..5dabdf29 100755 --- a/scripts/test +++ b/scripts/test @@ -8,7 +8,8 @@ usage () { } lint () { - standard + standard > /dev/tty + standard-markdown > /dev/tty } test_electron () { @@ -52,7 +53,8 @@ test_browser_local () { check_deps () { dependency-check . --entry 'index.js' dependency-check . --entry 'index.js' --extra --no-dev \ - -i xhr + -i document-ready \ + -i global } # set CLI flags @@ -70,6 +72,7 @@ case "$1" in 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 diff --git a/tests/browser/basic.js b/tests/browser/basic.js index 9633badc..087310d8 100644 --- a/tests/browser/basic.js +++ b/tests/browser/basic.js @@ -1,7 +1,8 @@ -const test = require('tape') const append = require('append-child') +const test = require('tape') + const choo = require('../../') -const view = require('../../html') +const html = require('../../html') test('state is immutable', function (t) { t.plan(4) @@ -16,16 +17,16 @@ test('state is immutable', function (t) { state: state, namespace: 'test', reducers: { - 'no-reducer-mutate': (data, state) => { + 'no-reducer-mutate': (state, data) => { return {} }, - 'mutate-on-return': (data, state) => { + 'mutate-on-return': (state, data) => { delete data.type return data } }, effects: { - 'triggers-reducers': (data, state, send, done) => { + 'triggers-reducers': (state, data, send, done) => { send('test:mutate-on-return', {beep: 'barp'}, done) } } @@ -46,13 +47,15 @@ test('state is immutable', function (t) { (send) => send('test:triggers-reducers') ] - app.router((route) => [ - route('/', function (state, prev, send) { + app.router([ + ['/', function (state, prev, send) { ++loop asserts[loop] && asserts[loop](state.test) setTimeout(() => triggers[loop] && triggers[loop](send), 5) - return view`
${state.foo}:${state.beep}
` - }) + return html` +
${state.foo}:${state.beep}
+ ` + }] ]) const tree = app.start() diff --git a/tests/browser/freeze.js b/tests/browser/freeze.js index 078f3df7..1a2f2fc9 100644 --- a/tests/browser/freeze.js +++ b/tests/browser/freeze.js @@ -11,14 +11,12 @@ test('freeze (default)', function (t) { } }) - app.router((route) => [ - route('/', function (state, prev, send) { - state.foo = '' - t.equal(state.foo, 'bar', 'cannot modify property') - state.bar = 'baz' - t.equal(state.bar, undefined, 'cannot add property') - }) - ]) + app.router(['/', function (state, prev, send) { + state.foo = '' + t.equal(state.foo, 'bar', 'cannot modify property') + state.bar = 'baz' + t.equal(state.bar, undefined, 'cannot add property') + }]) app.start() }) @@ -33,14 +31,12 @@ test('noFreeze', function (t) { } }) - app.router((route) => [ - route('/', function (state, prev, send) { - state.foo = '' - t.equal(state.foo, '', 'can modify property') - state.bar = 'baz' - t.equal(state.bar, 'baz', 'can add property') - }) - ]) + app.router(['/', function (state, prev, send) { + state.foo = '' + t.equal(state.foo, '', 'can modify property') + state.bar = 'baz' + t.equal(state.bar, 'baz', 'can add property') + }]) app.start() }) diff --git a/tests/browser/hooks.js b/tests/browser/hooks.js index 6954c2d7..80de35bf 100644 --- a/tests/browser/hooks.js +++ b/tests/browser/hooks.js @@ -10,15 +10,15 @@ test('hooks', function (t) { onError: function (err) { t.equal(err.message, 'effect error', 'onError: receives err') }, - onAction: function (data, state, name, caller, createSend) { + onAction: function (state, data, name, caller, createSend) { if (name === 'explodes') return t.deepEqual(data, {foo: 'bar'}, 'onAction: action data') t.equal(state.clicks, 0, 'onAction: current state: 0 clicks') - t.equal(name, 'click', 'onAction: action name') + t.equal(name, 'click', 'onAction: data name') t.equal(caller, 'view: /', 'onAction: caller name') t.equal(typeof createSend, 'function', 'onAction: createSend fn') }, - onStateChange: function (data, state, prev, createSend) { + onStateChange: function (state, data, prev, createSend) { t.deepEqual(data, {foo: 'bar'}, 'onState: action data') t.deepEqual(state.clicks, 1, 'onState: new state: 1 clicks') t.deepEqual(prev.clicks, 0, 'onState: prev state: 0 clicks') @@ -30,26 +30,24 @@ test('hooks', function (t) { clicks: 0 }, reducers: { - click: (data, state) => ({clicks: state.clicks + 1}) + click: (state, data) => ({clicks: state.clicks + 1}) }, effects: { - explodes: (data, state, send, done) => { + explodes: (state, data, send, done) => { setTimeout(() => done(new Error('effect error')), 5) } } }) var sent = false - app.router((route) => [ - route('/', function (state, prev, send) { - if (!sent) { - send('click', {foo: 'bar'}) - send('explodes') - } - sent = true - return view`` - }) - ]) + app.router(['/', function (state, prev, send) { + if (!sent) { + send('click', {foo: 'bar'}) + send('explodes') + } + sent = true + return view`` + }]) const tree = app.start() t.on('end', append(tree)) diff --git a/tests/browser/rehydration.js b/tests/browser/rehydration.js index 1b91995b..9eda5277 100644 --- a/tests/browser/rehydration.js +++ b/tests/browser/rehydration.js @@ -1,29 +1,38 @@ const test = require('tape') const onReady = require('document-ready') const append = require('append-child') +const mount = require('../../mount') const choo = require('../../') -const view = require('../../html') +const html = require('../../html') test('rehydration', function (t) { t.plan(2) const app = choo() - app.router((route) => [ - route('/', function (state, prev, send) { - return view`
send('test')}>Hello world!` - }) - ]) + const node = html` +
+
Hello squirrel! +
+ ` - var node = document.createElement('div') - node.innerHTML = app.toString('/') - node = node.childNodes[0] - t.on('end', append(node)) + app.router(['/', function (state, prev, send) { + return html` +
+
send('test')}>Hello world! +
+ ` + }]) - app.start('#app-root') + append(node) + + const tree = app.start() + mount('#app-root', tree) onReady(function () { - t.equal(node.innerHTML, 'Hello world!', 'same content') - t.equal(typeof node.onclick, 'function', 'attaches dom listeners') + 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') }) }) diff --git a/tests/browser/routing.js b/tests/browser/routing.js index c220be20..c829cdeb 100644 --- a/tests/browser/routing.js +++ b/tests/browser/routing.js @@ -6,7 +6,7 @@ const view = require('../../html') test('routing', function (t) { t.test('history', function (t) { - t.plan(3) + t.plan(2) const history = Event() const choo = proxyquire('../..', { @@ -20,23 +20,23 @@ test('routing', function (t) { user: null }, reducers: { - set: (data, state) => ({user: data.id}) + set: (state, data) => ({user: data.id}) }, effects: { - open: function (data, state, send, done) { + open: function (state, data, send, done) { t.deepEqual(data, {id: 1}) send('set', {id: 1}, function (err) { if (err) return done(err) - history.broadcast('https://foo.com/users/1') + history.broadcast('/users/1') }) } } }) - app.router('/users', (route) => [ - route('/users', parentView, [ - route('/:user', childView) - ]) + app.router({ default: '/users' }, [ + ['/users', parentView, [ + ['/:user', childView] + ]] ]) const tree = app.start() @@ -59,74 +59,76 @@ test('routing', function (t) { } }) - t.test('hash', function (t) { - t.plan(1) - - const hash = Event() - const choo = proxyquire('../..', { - 'sheet-router/hash': hash.listen - }) - - const app = choo() - - app.model({ - state: { - user: null - }, - reducers: { - set: (data, state) => ({user: data.id}) - }, - effects: { - open: function (data, state, send, done) { - send('set', {id: 1}, function (err) { - if (err) return done(err) - hash.broadcast('#users/1') - }) - } - } - }) - - app.router('/users', (route) => [ - route('/users', parentView, [ - route('/:user', childView) - ]) - ]) - - const tree = app.start({hash: true}) - t.on('end', append(tree)) - - tree.onclick() - - function parentView (state, prev, send) { - return view` - - ` - } - - function childView (state, prev, send) { - t.equal(state.user, 1) - return view`` - } - }) + // 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` + // + // ` + // } + + // function childView (state, prev, send) { + // t.equal(state.user, 1) + // return view`

${state.user}

` + // } + // }) t.test('disabling history', function (t) { t.plan(1) + resetLocation() const choo = proxyquire('../..', { 'sheet-router/history': () => t.fail('history listener attached') }) - const app = choo() + const app = choo({ history: false }) - app.router('/', (route) => [ - route('/', function () { + app.router('/', [ + ['/', function () { t.pass('rendered') - }) + }] ]) - app.start({history: false}) + app.start() }) t.test('disabling href', function (t) { @@ -136,15 +138,9 @@ test('routing', function (t) { 'sheet-router/href': () => t.fail('href listener attached') }) - const app = choo() - - app.router('/', (route) => [ - route('/', function () { - t.pass('rendered') - }) - ]) - - app.start({href: false}) + const app = choo({ href: false }) + app.router(['/', () => t.pass('rendered')]) + app.start() }) t.test('viewless nesting', function (t) { @@ -153,12 +149,12 @@ test('routing', function (t) { const choo = require('../..') const app = choo() - app.router('/users/123', (route) => [ - route('/users', [ - route('/:user', function (state) { + app.router({ default: '/users/123' }, [ + ['/users', [ + ['/:user', function (state) { t.deepEqual(state.params, {user: '123'}) - }) - ]) + }] + ]] ]) app.start() @@ -181,3 +177,7 @@ test('routing', function (t) { app.start() }) }) + +function resetLocation () { + window.history.pushState({}, null, '/') +} diff --git a/tests/server/index.js b/tests/server/index.js index 871b035b..fe28e3bd 100644 --- a/tests/server/index.js +++ b/tests/server/index.js @@ -8,9 +8,7 @@ test('server', function (t) { t.plan(1) const app = choo() - app.router((route) => [ - route('/', () => view`

Hello Tokyo!

`) - ]) + app.router(['/', () => view`

Hello Tokyo!

`]) const html = app.toString('/') const expected = '

Hello Tokyo!

' @@ -21,9 +19,7 @@ test('server', function (t) { t.plan(1) const app = choo() - app.router((route) => [ - route('/', () => minDocument.createElement('div')) - ]) + app.router(['/', () => minDocument.createElement('div')]) const html = app.toString('/') const expected = '
' @@ -34,11 +30,9 @@ test('server', function (t) { t.plan(1) const app = choo() - app.router((route) => [ - route('/', function (state, prev, send) { - return view`

meow meow ${state.message}

` - }) - ]) + app.router(['/', (state, prev, send) => { + return view`

meow meow ${state.message}

` + }]) const html = app.toString('/', { message: 'nyan!' }) const expected = '

meow meow nyan!

' @@ -50,11 +44,9 @@ test('server', function (t) { const app = choo() app.model({ state: { bin: 'baz', beep: 'boop' } }) - app.router((route) => [ - route('/', function (state, prev, send) { - return view`

${state.foo} ${state.bin} ${state.beep}

` - }) - ]) + app.router(['/', (state, prev, send) => { + return view`

${state.foo} ${state.bin} ${state.beep}

` + }]) const state = { foo: 'bar!', beep: 'beep' } const html = app.toString('/', state) @@ -70,13 +62,11 @@ test('server', function (t) { namespace: 'hello', state: { bin: 'baz', beep: 'boop' } }) - app.router((route) => [ - route('/', function (state, prev, send) { - return view` -

${state.hello.foo} ${state.hello.bin} ${state.hello.beep}

- ` - }) - ]) + app.router(['/', (state, prev, send) => { + return view` +

${state.hello.foo} ${state.hello.bin} ${state.hello.beep}

+ ` + }]) const state = { hello: { @@ -93,12 +83,7 @@ test('server', function (t) { t.plan(1) const app = choo() - app.router((route) => [ - route('/', function (state, prev, send) { - send('hey!') - }) - ]) - + app.router(['/', (state, prev, send) => send('hey!')]) t.throws(app.toString.bind(null), /route must be a string/) }) @@ -106,12 +91,7 @@ test('server', function (t) { t.plan(1) const app = choo() - app.router((route) => [ - route('/', function (state, prev, send) { - send('hey!') - }) - ]) - + app.router(['/', (state, prev, send) => send('hey!')]) const msg = /send\(\) cannot be called/ t.throws(app.toString.bind(null, '/', { message: 'nyan!' }), msg) })