diff --git a/.travis.yml b/.travis.yml index 99ea9e2..8ef5391 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,8 @@ addons: before_install: - export DISPLAY=':99.0' - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & -script: npm run test:cov -after_script: npm i -g codecov.io && cat ./coverage/lcov.info | codecov +script: npm run test +# after_script: npm i -g codecov.io && cat ./coverage/lcov.info | codecov env: global: - secure: t6MYp/rV/HXKQ8yb21fDMAV9fkwZmPFJ/4Za4TeXvVOojFan4Q40j5VZF6wiVBHTemctMRueQ2iuGl8pgNX3jqRHMKvNS5zKCCUHYvmyvMLQiWpkDC3Lz3R9oz2fooY1f0fpnzaE0TmJHoZQcVDYDTZSk/o6fNXJQnI4SUG/nmnb3VAA0H8lRZ1OidkAbBzJNFj8i0ZTuaqwo0uQVhNDSvLLKgM94ryF+DB+c0vKuZr85wn+xnuJ1ZPZM5S+uZrOAY+TnwD1QvstULHnbF6sqH5WuUkUCHpXV84Hz2pr7GL7yCkj65XoNoqvnoR5ETv8yBa5r/OccrCY9NqXSCv/zWiS4vRWybrJq0Mdx67cXHKHjbQMrILHsbRBDxbCJwn+LAQlwRUoktEcI9bNRUhi0Wt/LQmLvyfldt7mMvgl+7srBdlZWPoTZWRZKqu04XpYTaqAnAeNOjfSOOuB29uVTRu7n9pCeKA/gDNYOWCF3CfN++ztbL+E2OpPnpoRWwoSXnXzFQrLofq6zF1bpIrb5UpaqNYGX+PifUYX+hRYKLdOqqAS3JlJBqG+0ArRZgf3sm/uk4ZDesV5Ee9qB7Ty32Vh66wmU8P6wZjgare4TL+hcsgtNRrd6+kZwXfn0mqcqeFovN2yPpQSmF1U5+ki8+o1NcFFYdeucWa7E9ULkVE= diff --git a/.zuul.yml b/.zuul.yml deleted file mode 100644 index bfa88d5..0000000 --- a/.zuul.yml +++ /dev/null @@ -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 diff --git a/README.md b/README.md index 6ed5192..7bdf6c1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Fun functional programming
- A 5kb framework for creating sturdy frontend applications + A 4kb framework for creating sturdy frontend applications

@@ -47,18 +47,22 @@

+ + Website + + | Handbook | - - Ecosystem - - | - - CLI - + + Ecosystem + | + + + + Contributing @@ -77,98 +81,64 @@

-

Table of Contents

-
- Table of Contents -
  • Features
  • -
  • Demos
  • -
  • Example
  • -
  • Philosophy
  • -
  • Concepts
  • -
  • Badges
  • -
  • API
  • -
  • FAQ
  • -
  • Installation
  • -
  • See Also
  • -
  • Support
  • -
    +## Table of Content +- [Features](#features) +- [Example](#example) +- [Philosophy](#philosophy) +- [Optimizations](#optimizations) +- [FAQ](#faq) +- [API](#api) +- [Installation](#installation) +- [See Also](#see-also) +- [Support](#support) ## Features -- __minimal size:__ weighing `5kb`, `choo` is a tiny little framework -- __single state:__ immutable single state helps reason about changes -- __small api:__ with only 6 methods, there's not a lot to learn +- __minimal size:__ weighing `4kb`, `choo` is a tiny little framework +- __event based:__ our performant event system makes writing apps easy +- __small api:__ with only 6 methods there's not much to learn - __minimal tooling:__ built for the cutting edge `browserify` compiler -- __transparent side effects:__ using `effects` and `subscriptions` brings - clarity to IO -- __omakase:__ composed out of a balanced selection of open source packages - __isomorphic:__ renders seamlessly in both Node and browsers - __very cute:__ choo choo! -## Demos -- :truck: [Input example](http://requirebin.com/?gist=229bceda0334cf30e3044d5f5c600960) - ([repo](examples/title/)) -- :water_buffalo: [HTTP effects example](https://fork-fang.hyperdev.space/) - ([repo](https://hyperdev.com/#!/project/fork-fang)) -- :mailbox: [Mailbox routing](examples/mailbox/) -- :ok_hand: [TodoMVC](http://shuheikagawa.com/todomvc-choo/) - ([repo](https://github.com/shuhei/todomvc-choo)) -- :fire: [Choo-firebase](https://github.com/fyrkant/choo-firebase) -- :seedling: [Grow](https://grow.static.land/) - ([repo](https://github.com/sethvincent/grow)) -- :newspaper: [Hacker News Reader](https://hackernews-choo.surge.sh/) ([repo](https://github.com/kvnneff/hackernews-choo)) -- :earth_africa: [Thing Translator](https://oxism.com/thing-translator/) ([repo](https://github.com/dmotz/thing-translator)) - -_note: If you've built something cool using `choo` or are using it in -production, we'd love to hear from you!_ - ## Example -Let's create an input box that changes the content of a textbox in real time. -[Click here to see the app running](http://requirebin.com/?gist=229bceda0334cf30e3044d5f5c600960). ```js -var html = require('choo/html') -var choo = require('choo') +var html = require('./html') +var choo = require('./') + var app = choo() +app.use(logger) +app.use(countStore) +app.route('/', mainView) +app.mount('body') -app.model({ - state: { title: 'Not quite set yet' }, - reducers: { - update: function (state, data) { - return { title: data } - } - } -}) - -function mainView (state, prev, send) { +function mainView (state, emit) { return html` -
    -

    Title: ${state.title}

    - -
    + +

    count is ${state.count}

    + + ` - function update (e) { - send('update', e.target.value) + function onclick () { + emit('increment', 1) } } -app.router(['/', mainView]) +function logger (state, emitter) { + emitter.on('*', function (messageName, data) { + console.log('event', messageName, data) + }) +} -var tree = app.start() -document.body.appendChild(tree) -``` - -To run it, save it as `client.js` and run with [bankai][bankai]. `bankai` is -convenient but any [browserify][browserify] based tool should do: -```sh -# run and reload on port 8080 -$ bankai client.js -p 8080 --open - -# compile to static files in `./dist/` -$ bankai build index.js dist/ - -# deploy to github pages using `tschaub/gh-pages` -$ gh-pages -d dist/ +function countStore (state, emitter) { + state.count = 0 + emitter.on('increment', function (count) { + state.count += count + emitter.emit('render') + }) +} ``` +Want to see more examples? Check out the [Choo handbook][handbook]. ## Philosophy We believe programming should be fun and light, not stern and stressful. It's @@ -193,395 +163,42 @@ is laid out. And once an application is built, we want it to be small, performant and easy to reason about. All of which makes for easy to debug code, better results and super smiley faces. -## Concepts -`choo` cleanly structures internal data flow, so that all pieces of logic can -be combined into a nice, cohesive machine. Roughly speaking there are two parts -to `choo`: the views and the models. Models take care of state and logic, and -views are responsible for displaying the interface and responding to user -interactions. +## Optimizations +Choo is reasonably fast out of the box. But sometimes you might hit a scenario +where a particular part of the UI slows down the application, and you want to +speed it up. Here are some optimizations that are possible. -All of `choo`'s state is contained in a single object and whenever it changes -the views receive a new version of the state which they can use to safely -render a complete new representation of the DOM. The DOM is efficiently updated -using DOM diffing/patching. +### Reordering lists +To be implemented. (See [yoshuawuyts/nanomorph#8](https://github.com/yoshuawuyts/nanomorph/issues/8)) -The logic in choo exist in three different kinds of actions, each with their -own role: `effects`, `subscriptions` and `reducers`. - -- __Effects__ makes an asynchronous operation and calls another action when - it's done. - -- __Subscriptions__ (called once when the DOM loads) listens for external input - like keyboard or WebSocket events and then calls another action. - -- __Reducers__ receives the current state and returns an updated version of the - state which is then sent to the views. - -```txt - ┌─────────────────┐ - │ Subscriptions ─┤ User ───┐ - └─ Effects ◀─────┤ ▼ - ┌─ Reducers ◀─────┴─────────── DOM ◀┐ - │ │ - └▶ Router ─────State ───▶ Views ────┘ -``` - -### Models -`models` are objects that contain initial `state`, `subscriptions`, `effects` -and `reducers`. They're generally grouped around a theme (or domain, if you -like). To provide some sturdiness to your `models`, they can either be -namespaced or not. Namespacing means that only state within the model can be -accessed. Models can still trigger actions on other models, though it's -recommended to keep that to a minimum. - -So say we have a `todos` namespace, an `add` reducer and a `todos` model. -Outside the model they're called by `send('todos:add')` and -`state.todos.items`. Inside the namespaced model they're called by -`send('todos:add')` and `state.items`. An example namespaced model: -```js -var app = choo() -app.model({ - namespace: 'todos', - state: { items: [] }, - reducers: { - add: function (state, data) { - return { items: state.items.concat(data.payload) } - } - } -}) -``` - -In most cases using namespaces is beneficial, as having clear boundaries makes -it easier to follow logic. But sometimes you need to call `actions` that -operate over multiple domains (such as a "logout" `action`), or have a -`subscription` that might trigger multiple `reducers` (such as a `websocket` -that calls a different `action` based on the incoming data). - -In these cases you probably want to have a `model` that doesn't use namespaces, -and has access to the full application state. Try and keep the logic in these -`models` to a minimum, and declare as few `reducers` as possible. That way the -bulk of your logic will be safely shielded, with only a few points touching every -part of your application. - -### Effects -`effects` are similar to `reducers` except instead of modifying the state they -cause side `effects` by interacting servers, databases, DOM APIs, etc. Often -they'll call a reducer when they're done to update the state. For instance, you -may have an effect called getUsers that fetches a list of users from a server -API using AJAX. Assuming the AJAX request completes successfully, the effect -can pass off the list of users to a reducer called receiveUsers which simply -updates the state with that list, separating the concerns of interacting with -an API from updating the application's state. - -This is an example `effect` that is called once when the application loads and -calls the `'todos:add'` `reducer` when it receives data from the server: +### Caching DOM elements +Sometimes we want to tell the algorithm to not evaluate certain nodes (and its +children). This can be because we're sure they haven't changed, or perhaps +because another piece of code is managing that part of the DOM tree. To achieve +this `nanomorph` evaluates the `.isSameNode()` method on nodes to determine if +they should be updated or not. ```js -var choo = require('choo') -var http = require('xhr') -var app = choo() +var el = html`
    node
    ` -app.model({ - namespace: 'todos', - state: { values: [] }, - reducers: { - add: function (state, data) { - return { todos: data } - } - }, - effects: { - addAndSave: function (state, data, send, done) { - var opts = { body: data.payload, json: true } - http.post('/todo', opts, function (err, res, body) { - if (err) return done(err) - data.payload.id = body.id - send('todos:add', data, function (err, value) { - if (err) return done(err) - done(null, value) - }) - }) - } - }, - subscriptions: { - 'called-once-when-the-app-loads': function (send, done) { - send('todos:addAndSave', done) - } - } -}) -``` - -### Subscriptions -Subscriptions are a way of receiving data from a source. For example when -listening for events from a server using `SSE` or `Websockets` for a -chat app, or when catching keyboard input for a videogame. - -An example subscription that logs `"dog?"` every second: -```js -var choo = require('choo') - -var app = choo() -app.model({ - namespace: 'app', - effects: { - print: function (state, data) { - console.log(data.payload) - } - }, - subscriptions: { - callDog: function (send, done) { - setInterval(function () { - var data = { payload: 'dog?', myOtherValue: 1000 } - send('app:print', data, function (err) { - if (err) return done(err) - }) - }, 1000) - } - } -}) -``` - -If a `subscription` runs into an error, it can call `done(err)` to signal the -error to the error hook. - -### Router -The `router` manages which `views` are rendered at any given time. It also -supports rendering a default `view` if no routes match. - -```js -var app = choo() -app.router({ default: '/404' }, [ - [ '/', require('./views/empty') ], - [ '/404', require('./views/error') ], - [ '/:mailbox', require('./views/mailbox'), [ - [ '/:message', require('./views/email') ] - ]] -]) -``` - -Routes on the `router` are passed in as a nested array. This means that the -entry point of the application also becomes a site map, making it easier to -figure out how views relate to each other. - -Under the hood `choo` uses [sheet-router]. Internally the -currently rendered route is kept in `state.location`. To access the route -`:params` you can use `state.location.params`. If you want to modify -the location programmatically the `reducer` for the location can be called -using `send('location:set', href)`. This will not work -from within namespaced `models`, and usage should preferably be kept to a -minimum. Changing views all over the place tends to lead to messiness. - -### Views -Views are pure functions that return a DOM tree for the router to render. -They’re passed the current state, and any time the state changes they’re run -again with the new state. - -Views are also passed the `send` function, which they can use to dispatch -actions that can update the state. For example, the DOM tree can have an -`onclick` handler that dispatches an `add` action. - -```js -function view (state, prev, send) { - return html` -
    -

    Total todos: ${state.todos.length}

    - -
    - ` - - function addTodo (e) { - send('add', { title: 'demo' }) - } +// tell nanomorph to not compare the DOM tree if they're both divs +el.isSameNode = function (target) { + return (target && target.nodeName && target.nodeName === 'DIV') } ``` -In this example, when the `Add` button is clicked, the view will dispatch an -`add` action that the model’s `add` reducer will receive. [As seen -above](#models), the reducer will add an item to the state’s `todos` array. The -state change will cause this view to be run again with the new state, and the -resulting DOM tree will be used to [efficiently patch the -DOM](#does-choo-use-a-virtual-dom). +### Pruning dependencies +We use the `require('assert')` module from Node core to provide helpful error +messages in development. In production you probably want to strip this using +[unassertify][unassertify]. -### Plugins -Sometimes it's necessary to change the way `choo` itself works. For example to -report whenever an action is triggered, handle errors globally or perist state -somewhere. This is done through something called `plugins`. Plugins are objects -that contain `hook` and `wrap` functions and are passed to `app.use()`: +To convert inlined HTML to valid DOM nodes we use `require('bel')`. This has +overhead during runtime, so for production environments we should unwrap this +using [yo-yoify][yo-yoify]. -```js -var log = require('choo-log') -var choo = require('choo') -var app = choo() - -app.use(log()) - -var tree = app.start() -document.body.appendChild(tree) -``` - -Generally people using `choo` shouldn't be too worried about the specifics of -`plugins`, as the internal API is (unfortunately by necessity) quite complex. -After all they're the most powerful way to modify a `choo` application. - -__:warning: Warning :warning:: plugins should only be used as a last resort. -It creates peer dependencies which makes upgrading versions and switching -frameworks a lot harder. Please exhaust all other options before using -plugins.__ - -If you want to learn more about creating your own `plugins`, and which `hooks` -and `wrappers` are available, head on over to [app.use()](#appusehooks). - -## Badges -Using `choo` in a project? Show off which version you've used using a badge: - - -[![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 `` 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` - -` - -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 `` tag on the document body after rendering -from the server. It makes sure all ` - - - - diff --git a/html.js b/html.js index 17336bc..e25543b 100644 --- a/html.js +++ b/html.js @@ -1 +1 @@ -module.exports = require('yo-yo') +module.exports = require('bel') diff --git a/index.js b/index.js index 8e6af53..ab60885 100644 --- a/index.js +++ b/index.js @@ -1,216 +1,120 @@ -const createLocation = require('sheet-router/create-location') -const onHistoryChange = require('sheet-router/history') -const sheetRouter = require('sheet-router') -const onHref = require('sheet-router/href') -const walk = require('sheet-router/walk') -const mutate = require('xtend/mutable') -const barracks = require('barracks') -const nanoraf = require('nanoraf') -const assert = require('assert') -const xtend = require('xtend') -const yo = require('yo-yo') +var documentReady = require('document-ready') +var nanorouter = require('nanorouter') +var nanomount = require('nanomount') +var nanomorph = require('nanomorph') +var nanoraf = require('nanoraf') +var nanobus = require('nanobus') +var assert = require('assert') -module.exports = choo +var onHistoryChange = require('./lib/history') +var onHref = require('./lib/href') -// framework for creating sturdy web applications -// null -> fn -function choo (opts) { +module.exports = Framework + +function Framework (opts) { opts = opts || {} - const _store = start._store = barracks() - var _router = start._router = null - var _routerOpts = null - var _rootNode = null - var _routes = null - var _frame = null - - if (typeof window !== 'undefined') { - _store.use({ onStateChange: render }) - } - _store.use(opts) - - start.toString = toString - start.router = router - start.model = model - start.start = start - start.stop = _store.stop - start.use = use - - return start - - // render the application to a string - // (str, obj) -> str - function toString (route, serverState) { - serverState = serverState || {} - assert.equal(typeof route, 'string', 'choo.app.toString: route must be a string') - assert.equal(typeof serverState, 'object', 'choo.app.toString: serverState must be an object') - _store.start({ subscriptions: false, reducers: false, effects: false }) - - const state = _store.state({ state: serverState }) - const router = createRouter(_routerOpts, _routes, createSend) - const tree = router(route, state) - return tree.outerHTML || tree.toString() - - function createSend () { - return function send () { - assert.ok(false, 'choo: send() cannot be called from Node') - } - } + var routerOpts = { + default: opts.defaultRoute || '/404', + curry: true } - // start the application - // (str?, obj?) -> DOMNode - function start () { - _store.model(createLocationModel(opts)) - const createSend = _store.start(opts) - _router = start._router = createRouter(_routerOpts, _routes, createSend) - const state = _store.state({state: {}}) + var router = nanorouter(routerOpts) + var bus = nanobus() + var rerender = null + var tree = null + var state = {} - const tree = _router(state.location.href, state) - assert.ok(tree, 'choo.start: the router should always return a valid DOM node') - assert.equal(typeof tree, 'object', 'choo.start: the router should always return a valid DOM node') - _rootNode = tree - tree.done = done - - return tree - - // allow a 'mount' function to return the new node - // html -> null - function done (newNode) { - _rootNode = newNode - } - } - - // update the DOM after every state mutation - // (obj, obj, obj, str, fn) -> null - function render (state, data, prev, name, createSend) { - if (!_frame) { - _frame = nanoraf(function (state, prev) { - const newTree = _router(state.location.href, state, prev) - _rootNode = yo.update(_rootNode, newTree) - }) - } - _frame(state, prev) - } - - // register all routes on the router - // (str?, [fn|[fn]]) -> obj - function router (defaultRoute, routes) { - _routerOpts = defaultRoute - _routes = routes - } - - // create a new model - // (str?, obj) -> null - function model (model) { - _store.model(model) - } - - // register a plugin - // (obj) -> null - function use (hooks) { - assert.equal(typeof hooks, 'object', 'choo.use: hooks should be an object') - _store.use(hooks) - } - - // create a new router with a custom `createRoute()` function - // (str?, obj) -> null - function createRouter (routerOpts, routes, createSend) { - var prev = null - if (!routes) { - routes = routerOpts - routerOpts = {} - } - routerOpts = mutate({ thunk: 'match' }, routerOpts) - const router = sheetRouter(routerOpts, routes) - walk(router, wrap) - - return router - - function wrap (route, handler) { - const send = createSend('view: ' + route, true) - return function chooWrap (params) { - return function (state) { - // TODO(yw): find a way to wrap handlers so params shows up in state - const nwState = xtend(state) - nwState.location = xtend(nwState.location, { params: params }) - - const nwPrev = prev - prev = nwState // save for next time - - if (opts.freeze !== false) Object.freeze(nwState) - return handler(nwState, nwPrev, send) - } - } - } - } -} - -// application location model -// obj -> obj -function createLocationModel (opts) { return { - namespace: 'location', - state: mutate(createLocation(), { params: {} }), - subscriptions: createSubscriptions(opts), - effects: { set: setLocation, touch: touchLocation }, - reducers: { update: updateLocation } + toString: toString, + use: register, + mount: mount, + route: route, + start: start } - // update the location on the state - // try and jump to an anchor on the page if it exists - // (obj, obj) -> obj - function updateLocation (state, data) { - if (opts.history !== false && data.hash && data.hash !== state.hash) { - try { - const el = document.querySelector(data.hash) - if (el) el.scrollIntoView(true) - } catch (e) { - return data + function route (route, handler) { + router.on(route, function (params) { + return function () { + state.params = params + return handler(state, emit) } - } - return data + }) } - // update internal location only - // (str, obj, fn, fn) -> null - function touchLocation (state, data, send, done) { - const newLocation = createLocation(state, data) - send('location:update', newLocation, done) + function register (cb) { + cb(state, bus) } - // set a new location e.g. "/foo/bar#baz?beep=boop" - // (str, obj, fn, fn) -> null - function setLocation (state, data, send, done) { - const newLocation = createLocation(state, data) + function start () { + tree = router(createLocation(), state, emit) + rerender = nanoraf(function () { + var newTree = router(createLocation(), state, emit) + tree = nanomorph(tree, newTree) + }) - // update url bar if it changed - if (opts.history !== false && newLocation.href !== state.href) { - window.history.pushState({}, null, newLocation.href) - } - - send('location:update', newLocation, done) - } - - function createSubscriptions (opts) { - const subs = {} + bus.on('render', rerender) if (opts.history !== false) { - subs.handleHistory = function (send, done) { - onHistoryChange(function navigate (href) { - send('location:touch', href, done) + onHistoryChange(function (href) { + bus.emit('pushState', window.location.href) + scrollIntoView() + }) + + if (opts.href !== false) { + onHref(function (location) { + var href = location.href + var currHref = window.location.href + if (href === currHref) return + window.history.pushState({}, null, href) + bus.emit('pushState', window.location.href) + bus.emit('render') + scrollIntoView() }) } } - if (opts.href !== false) { - subs.handleHref = function (send, done) { - onHref(function navigate (location) { - send('location:set', location, done) - }) - } - } + documentReady(function () { + bus.emit('DOMContentLoaded') + }) - return subs + return tree + } + + function emit (eventName, data) { + bus.emit(eventName, data) + } + + function mount (selector) { + var newTree = start() + documentReady(function () { + var root = document.querySelector(selector) + assert.ok(root, 'could not query selector: ' + selector) + nanomount(root, newTree) + tree = root + }) + } + + function toString (location, _state) { + state = _state || {} + var html = router(location) + assert.equal() + return html.toString() } } + +function scrollIntoView () { + var hash = window.location.hash + if (hash) { + try { + var el = document.querySelector(hash) + if (el) el.scrollIntoView(true) + } catch (e) {} + } +} + +function createLocation () { + var pathname = window.location.pathname.replace(/\/$/, '') + var hash = window.location.hash.replace(/^#/, '/') + return pathname + hash +} diff --git a/lib/history.js b/lib/history.js new file mode 100644 index 0000000..762a74c --- /dev/null +++ b/lib/history.js @@ -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 + }) + } +} diff --git a/lib/href.js b/lib/href.js new file mode 100644 index 0000000..48cad16 --- /dev/null +++ b/lib/href.js @@ -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) + } +} diff --git a/mount.js b/mount.js deleted file mode 100644 index 580a602..0000000 --- a/mount.js +++ /dev/null @@ -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 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 1137791..7b1c33b 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,15 @@ { "name": "choo", - "version": "4.1.0", + "version": "5.0.0-5", "description": "A 5kb framework for creating sturdy frontend applications", "main": "index.js", "scripts": { - "deps": "./scripts/test deps", - "test:electron": "./scripts/test electron", - "test:cov": "./scripts/test cov", - "test:server": "./scripts/test server", - "test:browser": "./scripts/test browser", - "test:browser-local": "./scripts/test browser-local", - "preversion": "if [ ! -z $SKIP_TEST ]; then npm run test:browser; fi", - "test": "npm run test:electron", - "build:dev": "./scripts/build dev", - "build:min": "./scripts/build min", - "prepublish": "npm run build:dev && npm run build:min", - "instrument:discify": "./scripts/instrument discify", - "instrument:minified": "./scripts/instrument minified", - "instrument:gzip": "./scripts/instrument gzip" + "deps": "dependency-check --entry ./html.js . && dependency-check . --extra --no-dev --entry ./html.js", + "start": "bankai start example --open", + "test": "standard && npm run deps && node test.js", + "build:dev": "echo 'to be implemented'", + "build:min": "echo 'to be implemented'", + "prepublish": "npm run build:dev && npm run build:min" }, "repository": "yoshuawuyts/choo", "keywords": [ @@ -28,53 +20,21 @@ "composable", "tiny" ], - "files": [ - "index.js", - "mount.js", - "html.js", - "dist/" - ], "license": "MIT", "dependencies": { - "barracks": "^9.1.0", - "document-ready": "~1.0.2", - "global": "^4.3.0", - "nanoraf": "^2.1.1", - "sheet-router": "^4.0.1", - "xtend": "^4.0.1", - "yo-yo": "^1.2.2" + "bel": "^4.5.1", + "document-ready": "^1.0.3", + "global": "^4.3.1", + "nanobus": "^2.1.0", + "nanomorph": "^4.0.0", + "nanomount": "^1.0.0", + "nanoraf": "^3.0.0", + "nanorouter": "^2.0.0" }, "devDependencies": { - "append-child": "~1.0.0", - "bankai": "^3.1.0", - "browserify": "^13.0.1", - "browserify-istanbul": "^2.0.0", - "bundle-collapser": "^1.2.1", - "dependency-check": "^2.5.1", - "disc": "^1.3.2", - "envify": "^3.4.1", - "es2020": "^1.0.1", - "geval": "~2.1.1", - "gzip-size-cli": "^1.0.0", - "insert-css": "^1.0.0", - "istanbul": "^0.4.4", - "min-document": "~2.19.0", - "pretty-bytes-cli": "^2.0.0", - "proxyquire": "~1.7.10", - "proxyquire-universal": "~1.0.8", - "proxyquireify": "~3.2.0", - "server-router": "^3.0.0", - "sheetify": "^5.1.0", - "standard": "^8.0.0", - "standard-markdown": "^2.2.0", - "tachyons": "^4.0.0-beta.19", - "tape": "^4.5.1", - "tape-istanbul": "~1.0.2", - "tape-run": "~2.1.4", - "uglifyify": "^3.0.2", - "uglifyjs": "^2.4.10", - "unassertify": "^2.0.3", - "yo-yoify": "^3.1.0", - "zuul": "toddself/zuul" + "dependency-check": "^2.8.0", + "spok": "^0.7.0", + "standard": "^9.0.1", + "tape": "^4.6.3" } } diff --git a/scripts/build b/scripts/build deleted file mode 100755 index e214179..0000000 --- a/scripts/build +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/sh - -usage () { - printf "Usage: build-umd \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 diff --git a/scripts/instrument b/scripts/instrument deleted file mode 100755 index eb077f8..0000000 --- a/scripts/instrument +++ /dev/null @@ -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 diff --git a/scripts/test b/scripts/test deleted file mode 100755 index ee202b6..0000000 --- a/scripts/test +++ /dev/null @@ -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 diff --git a/test.js b/test.js new file mode 100644 index 0000000..1fe0229 --- /dev/null +++ b/test.js @@ -0,0 +1 @@ +// var tape = require('tape') diff --git a/tests/browser/basic.js b/tests/browser/basic.js deleted file mode 100644 index 087310d..0000000 --- a/tests/browser/basic.js +++ /dev/null @@ -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` -
    ${state.foo}:${state.beep}
    - ` - }] - ]) - - const tree = app.start() - t.on('end', append(tree)) -}) diff --git a/tests/browser/freeze.js b/tests/browser/freeze.js deleted file mode 100644 index 2c5717b..0000000 --- a/tests/browser/freeze.js +++ /dev/null @@ -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() -}) diff --git a/tests/browser/hooks.js b/tests/browser/hooks.js deleted file mode 100644 index 80de35b..0000000 --- a/tests/browser/hooks.js +++ /dev/null @@ -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`` - }]) - - const tree = app.start() - t.on('end', append(tree)) -}) diff --git a/tests/browser/rehydration.js b/tests/browser/rehydration.js deleted file mode 100644 index bee4d1a..0000000 --- a/tests/browser/rehydration.js +++ /dev/null @@ -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` -
    -
    Hello squirrel! -
    - ` - - app.router(['/', function (state, prev, send) { - return html` -
    -
    send('test')}>Hello world! -
    - ` - }]) - - 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') - }) -}) diff --git a/tests/browser/routing.js b/tests/browser/routing.js deleted file mode 100644 index 03321c0..0000000 --- a/tests/browser/routing.js +++ /dev/null @@ -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` - - ` - } - - 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({ 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, '/') -} diff --git a/tests/server/index.js b/tests/server/index.js deleted file mode 100644 index fe28e3b..0000000 --- a/tests/server/index.js +++ /dev/null @@ -1,98 +0,0 @@ -const test = require('tape') -const minDocument = require('min-document') -const choo = require('../../') -const view = require('../../html') - -test('server', function (t) { - t.test('renders a static html response', function (t) { - t.plan(1) - - const app = choo() - app.router(['/', () => view`

    Hello Tokyo!

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

    Hello Tokyo!

    ' - 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 = '
    ' - 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`

    meow meow ${state.message}

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

    meow meow nyan!

    ' - 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`

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

    ` - }]) - - const state = { foo: 'bar!', beep: 'beep' } - const html = app.toString('/', state) - const expected = '

    bar! baz beep

    ' - 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` -

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

    - ` - }]) - - const state = { - hello: { - foo: 'bar!', - beep: 'beep' - } - } - const html = app.toString('/', state) - const expected = '

    bar! baz beep

    ' - 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) - }) -})