A 4kb framework for creating sturdy frontend applications
npm install choo4kb framework for creating sturdy frontend applications4kb, Choo is a tiny little frameworkbrowserify compilerjs
var html = require('choo/html')
var devtools = require('choo-devtools')
var choo = require('choo')var app = choo()
app.use(devtools())
app.use(countStore)
app.route('/', mainView)
app.mount('body')
function mainView (state, emit) {
return html
function onclick () {
emit('increment', 1)
}
}
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
cool to be cute; using serious words without explaining them doesn't make for
better results - if anything it scares people off. We don't want to be scary,
we want to be nice and fun, and then _casually_ be the best choice around.
_Real casually._We believe frameworks should be disposable, and components recyclable. We don't
want a web where walled gardens jealously compete with one another. By making
the DOM the lowest common denominator, switching from one framework to another
becomes frictionless. Choo is modest in its design; we don't believe it will
be top of the class forever, so we've made it as easy to toss out as it is to
pick up.
We don't believe that bigger is better. Big APIs, large complexities, long
files - we see them as omens of impending userland complexity. We want everyone
on a team, no matter the size, to fully understand how an application 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.
Events
At the core of Choo is an event emitter, which is used for both application
logic but also to interface with the framework itself. The package we use for
this is nanobus.You can access the emitter through
app.use(state, emitter, app), app.route(route, or app.emitter. Routes only have access to the
emitter.emit method to encourage people to separate business logic from
render logic.The purpose of the emitter is two-fold: it allows wiring up application code
together, and splitting it off nicely - but it also allows communicating with
the Choo framework itself. All events can be read as constants from
state.events. Choo ships with the following events built in:$3
Choo emits this when the DOM is ready. Similar to the DOM's
'DOMContentLoaded' event, except it will be emitted even if the listener is
added _after_ the DOM became ready. Uses
document-ready under the hood.$3
This event should be emitted to re-render the DOM. A common pattern is to
update the state object, and then emit the 'render' event straight after.
Note that 'render' will only have an effect once the DOMContentLoaded event
has been fired.$3
Choo emits this event whenever routes change. This is triggered by either
'pushState', 'replaceState' or 'popState'.$3
This event should be emitted to navigate to a new route. The new route is added
to the browser's history stack, and will emit 'navigate' and 'render'.
Similar to
history.pushState.$3
This event should be emitted to navigate to a new route. The new route replaces
the current entry in the browser's history stack, and will emit 'navigate'
and 'render'. Similar to
history.replaceState.$3
This event is emitted when the user hits the 'back' button in their browser.
The new route will be a previous entry in the browser's history stack, and
immediately afterward the'navigate' and 'render'events will be emitted.
Similar to history.popState. (Note
that emit('popState') will _not_ cause a popState action - use
history.go(-1) for that - this is different from the behaviour of pushState
and replaceState!)$3
This event should be emitted whenever the document.title needs to be updated.
It will set both document.title and state.title. This value can be used
when server rendering to accurately include a tag in the header.
This is derived from the
DOMTitleChanged event.State
Choo comes with a shared state object. This object can be mutated freely, and
is passed into the view functions whenever 'render' is emitted. The state
object comes with a few properties set.When initializing the application,
window.initialState is used to provision
the initial state. This is especially useful when combined with server
rendering. See server rendering for more details.$3
A mapping of Choo's built in events. It's recommended to extend this object
with your application's events. By defining your event names once and setting
them on state.events, it reduces the chance of typos, generally autocompletes
better, makes refactoring easier and compresses better.$3
The current params taken from the route. E.g. /foo/:bar becomes available as
state.params.bar If a wildcard route is used (/foo/*) it's available as
state.params.wildcard.$3
An object containing the current queryString. /foo?bin=baz becomes { bin:.$3
An object containing the current href. /foo?bin=baz becomes /foo.$3
The current name of the route used in the router (e.g. /foo/:bar).$3
The current page title. Can be set using the DOMTitleChange event.$3
An object _recommended_ to use for local component state.$3
Generic class cache. Will lookup Component instance by id and create one if not
found. Useful for working with stateful components.Routing
Choo is an application level framework. This means that it takes care of
everything related to routing and pathnames for you.$3
Params can be registered by prepending the route name with :routename, e.g.
/foo/:bar/:baz. The value of the param will be saved on state.params (e.g.
state.params.bar). Wildcard routes can be registered with , e.g. /foo/.
The value of the wildcard will be saved under state.params.wildcard.$3
Sometimes a route doesn't match, and you want to display a page to handle it.
You can do this by declaring app.route('*', handler) to handle all routes
that didn't match anything else.$3
Querystrings (e.g. ?foo=bar) are ignored when matching routes. An object
containing the key-value mappings exists as state.query.$3
By default, hashes are ignored when routing. When enabling hash routing
(choo({ hash: true })) hashes will be treated as part of the url, converting
/foo#bar to /foo/bar. This is useful if the application is not mounted at
the website root. Unless hash routing is enabled, if a hash is found we check if
there's an anchor on the same page, and will scroll the element into view. Using
both hashes in URLs and anchor links on the page is generally not recommended.$3
By default all clicks on tags are handled by the router through the
nanohref module. This can be
disabled application-wide by passing { href: false } to the application
constructor. The event is not handled under the following conditions:
- the click event had .preventDefault() called on it
- the link has a target="_blank" attribute with rel="noopener noreferrer"
- a modifier key is enabled (e.g. ctrl, alt, shift or meta)
- the link's href starts with protocol handler such as mailto: or dat:
- the link points to a different host
- the link has a download attribute:warn: Note that we only handle
target=_blank if they also have
rel="noopener noreferrer" on them. This is needed to properly sandbox web
pages.$3
To navigate routes you can emit 'pushState', 'popState' or
'replaceState'. See #events for more details about these events.Server Rendering
Choo was built with Node in mind. To render on the server call
.toString(route, [state]) on your choo instance.`js
var html = require('choo/html')
var choo = require('choo')var app = choo()
app.route('/', function (state, emit) {
return html
})var state = { name: 'Node' }
var string = app.toString('/', state)
console.log(string)
// => '
Hello Node'
`When starting an application in the browser, it's recommended to provide the
same
state object available as window.initialState. When the application is
started, it'll be used to initialize the application state. The process of
server rendering, and providing an initial state on the client to create the
exact same document is also known as "rehydration".For security purposes, after
window.initialState is used it is deleted from
the window object.`html
`Components
From time to time there will arise a need to have an element in an application
hold a self-contained state or to not rerender when the application does. This
is common when using 3rd party libraries to e.g. display an interactive map or a
graph and you rely on this 3rd party library to handle modifications to the DOM.
Components come baked in to Choo for these kinds of situations. See
[nanocomponent][nanocomponent] for documentation on the component class.`javascript
// map.js
var html = require('choo/html')
var mapboxgl = require('mapbox-gl')
var Component = require('choo/component')module.exports = class Map extends Component {
constructor (id, state, emit) {
super(id)
this.local = state.components[id] = {}
}
load (element) {
this.map = new mapboxgl.Map({
container: element,
center: this.local.center
})
}
update (center) {
if (center.join() !== this.local.center.join()) {
this.map.setCenter(center)
}
return false
}
createElement (center) {
this.local.center = center
return html
}
}
``javascript
// index.js
var choo = require('choo')
var html = require('choo/html')
var Map = require('./map.js')var app = choo()
app.route('/', mainView)
app.mount('body')
function mainView (state, emit) {
return html
function onclick () {
emit('locate')
}
}
app.use(function (state, emitter) {
state.center = [18.0704503, 59.3244897]
emitter.on('locate', function () {
window.navigator.geolocation.getCurrentPosition(function (position) {
state.center = [position.coords.longitude, position.coords.latitude]
emitter.emit('render')
})
})
})
`$3
When working with stateful components, one will need to keep track of component
instances – state.cache does just that. The component cache is a function
which takes a component class and a unique id (string) as its first two
arguments. Any following arguments will be forwarded to the component constructor
together with state and emit.The default class cache is an LRU cache (using [nanolru][nanolru]), meaning it
will only hold on to a fixed amount of class instances (
100 by default) before
starting to evict the least-recently-used instances. This behavior can be
overriden with options.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.$3
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 el = html// tell nanomorph to not compare the DOM tree if they're both divs
el.isSameNode = function (target) {
return (target && target.nodeName && target.nodeName === 'DIV')
}
`$3
It's common to work with lists of elements on the DOM. Adding, removing or
reordering elements in a list can be rather expensive. To optimize this you can
add an id attribute to a DOM node. When reordering nodes it will compare
nodes with the same ID against each other, resulting in far fewer re-renders.
This is especially potent when coupled with DOM node caching.`js
var el = html
`$3
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].To convert inlined HTML to valid DOM nodes we use
require('nanohtml'). This has
overhead during runtime, so for production environments we should unwrap this
using the [nanohtml transform][nanohtml].Setting up browserify transforms can sometimes be a bit of hassle; to make this
more convenient we recommend using [bankai build][bankai] to build your assets for production.
FAQ
$3
Because I thought it sounded cute. All these programs talk about being
_"performant"_, _"rigid"_, _"robust"_ - I like programming to be light, fun and
non-scary. Choo embraces that.Also imagine telling some business people you chose to rewrite something
critical for serious bizcorp using a train themed framework.
:steam_locomotive::train::train::train:
$3
It's called "Choo", though we're fine if you call it "Choo-choo" or
"Chugga-chugga-choo-choo" too. The only time "choo.js" is tolerated is if /
when you shimmy like you're a locomotive.$3
Choo uses [nanomorph][nanomorph], which diffs real DOM nodes instead of
virtual nodes. It turns out that [browsers are actually ridiculously good at
dealing with DOM nodes][morphdom-bench], and it has the added benefit of
working with _any_ library that produces valid DOM nodes. So to put a long
answer short: we're using something even better.$3
Template strings aren't supported in all browsers, and parsing them creates
significant overhead. To optimize we recommend running browserify with
[nanohtml][nanohtml] as a global transform or using [bankai][bankai] directly.
`sh
$ browserify -g nanohtml
`$3
Sure.API
This section provides documentation on how each function in Choo works. It's
intended to be a technical reference. If you're interested in learning choo for
the first time, consider reading through the [handbook][handbook] first
:sparkles:$3
Initialize a new choo instance. opts can also contain the following values:
- __opts.history:__ default: true. Listen for url changes through the
history API.
- __opts.href:__ default: true. Handle all relative href=" clicks and call emit('render')
- __opts.cache:__ default: undefined. Override default class cache used by
state.cache. Can be a a number (maximum number of instances in cache,
default 100) or an object with a [nanolru][nanolru]-compatible API.
- __opts.hash:__ default: false. Treat hashes in URLs as part of the pathname,
transforming /foo#bar to /foo/bar. This is useful if the application is
not mounted at the website root.$3
Call a function and pass it a state, emitter and app. emitter is an instance
of nanobus. You can listen to
messages by calling emitter.on() and emit messages by calling
emitter.emit(). app is the same Choo instance. Callbacks passed to app.use() are commonly referred to as
'stores'.If the callback has a
.storeName property on it, it will be used to identify
the callback during tracing.See #events for an overview of all events.
$3
Register a route on the router. The handler function is passed app.state
and app.emitter.emit as arguments. Uses [nanorouter][nanorouter] under the
hood.See #routing for an overview of how to use routing efficiently.
$3
Start the application and mount it on the given querySelector,
the given selector can be a String or a DOM element.In the browser, this will _replace_ the selector provided with the tree returned from
app.start().
If you want to add the app as a child to an element, use app.start() to obtain the tree and manually append it.On the server, this will save the
selector on the app instance.
When doing server side rendering, you can then check the app.selector property to see where the render result should be inserted.Returns
this, so you can easily export the application for server side rendering:`js
module.exports = app.mount('body')
`$3
Start the application. Returns a tree of DOM nodes that can be mounted using
document.body.appendChild().$3
Render the application to a string. Useful for rendering on the server.$3
Create DOM nodes from template string literals. Exposes
nanohtml. Can be optimized using
[nanohtml][nanohtml].$3
Exposes nanohtml/raw helper for rendering raw HTML content.Installation
`sh
$ npm install choo
`See Also
- bankai - streaming asset compiler
- stack.gl - open software ecosystem for WebGL
- yo-yo - tiny library for modular UI
- tachyons - functional CSS for
humans
- sheetify - modular CSS bundler for
browserify`[nanocomponent]: https://github.com/choojs/nanocomponent
[nanolru]: https://github.com/s3ththompson/nanolru
[bankai]: https://github.com/choojs/bankai
[nanohtml]: https://github.com/choojs/nanohtml
[browserify]: https://github.com/substack/node-browserify
[budo]: https://github.com/mattdesl/budo
[es2020]: https://github.com/yoshuawuyts/es2020
[handbook]: https://github.com/yoshuawuyts/choo-handbook
[hyperx]: https://github.com/substack/hyperx
[morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks
[nanomorph]: https://github.com/choojs/nanomorph
[nanorouter]: https://github.com/choojs/nanorouter
[yo-yo]: https://github.com/maxogden/yo-yo
[unassertify]: https://github.com/unassert-js/unassertify
[window-performance]: https://developer.mozilla.org/en-US/docs/Web/API/Performance