:steam_locomotive::train::train::train::train::train:
Fun functional programming
A 4kb framework for creating sturdy frontend applications
The little framework that could. Built with ❤︎ by Yoshua Wuyts and contributors
4kb, Choo is a tiny little frameworkbrowserify compilervar 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`
<body>
<h1>count is ${state.count}</h1>
<button onclick=${onclick}>Increment</button>
</body>
`
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].
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.
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,
view(state, emit)) 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:
'DOMContentLoaded'|state.events.DOMCONTENTLOADEDChoo 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.
'render'|state.events.RENDERThis 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.
'navigate'|state.events.NAVIGATEChoo emits this event whenever routes change. This is triggered by either
'pushState', 'replaceState' or 'popState'.
'pushState'|state.events.PUSHSTATEThis 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.
'replaceState'|state.events.REPLACESTATEThis 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.
'popState'|state.events.POPSTATEThis 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!)
'DOMTitleChange'|state.events.DOMTITLECHANGEThis 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 <title> tag in the header.
This is derived from the
DOMTitleChanged event.
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.
state.eventsA 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.
state.paramsThe 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.
state.queryAn object containing the current queryString. /foo?bin=baz becomes { bin:
'baz' }.
state.hrefAn object containing the current href. /foo?bin=baz becomes /foo.
state.routeThe current name of the route used in the router (e.g. /foo/:bar).
state.titleThe current page title. Can be set using the DOMTitleChange event.
state.componentsAn object recommended to use for local component state.
state.cache(Component, id, [...args])Generic class cache. Will lookup Component instance by id and create one if not found. Useful for working with stateful components.
Choo is an application level framework. This means that it takes care of everything related to routing and pathnames for you.
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.
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.
Querystrings (e.g. ?foo=bar) are ignored when matching routes. An object
containing the key-value mappings exists as state.query.
By default hashes are treated as part of the url when routing. Using hashes to
delimit routes (e.g. /foo#bar) can be disabled by setting the hash
option to false. Regardless, when a hash is found we also
check if there's an available anchor on the same page, and will scroll the
screen to the position. Using both hashes in URLs and anchor links on the page
is generally not recommended.
By default all clicks on <a> 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.
To navigate routes you can emit 'pushState', 'popState' or
'replaceState'. See #events for more details about these events.
Choo was built with Node in mind. To render on the server call
.toString(route, [state]) on your choo instance.
var html = require('choo/html')
var choo = require('choo')
var app = choo()
app.route('/', function (state, emit) {
return html`
Hello ${state.name}
`
})
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>
<head>
<script>window.initialState = { initial: 'state' }</script>
</head>
<body>
</body>
</html>
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.