hyperchoo
v0.1.3
Published
A 4kb framework for creating sturdy frontend applications
Downloads
1
Maintainers
Readme
Table of Content
- Features
- Example
- Philosophy
- Events
- State
- Routing
- Server Rendering
- Optimizations
- FAQ
- API
- Installation
- See Also
- Support
Features
- 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 - isomorphic: renders seamlessly in both Node and browsers
- very cute: choo choo!
Example
var html = require('choo/html')
var log = require('choo-log')
var choo = require('choo')
var app = choo()
app.use(log())
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.
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.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.DOMCONTENTLOADED
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.
'render'
|state.events.RENDER
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.
'navigate'
|state.events.NAVIGATE
Choo emits this event whenever routes change. This is triggered by either
'pushState'
, 'replaceState'
or 'popState'
.
'pushState'
|state.events.PUSHSTATE
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.
'replaceState'
|state.events.REPLACESTATE
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.
'popState'
|state.events.POPSTATE
This event should be emitted to navigate to a previous route. The new route
will be a previous entry in the browser's history stack, and will emit
'navigate'
and 'render'
. Similar to
history.popState.
'DOMTitleChange'
|state.events.DOMTITLECHANGE
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 <title>
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.
state.events
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.
state.params
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
.
state.query
An object containing the current queryString. /foo?bin=baz
becomes { bin:
'baz' }
.
state.href
An object containing the current href. /foo?bin=baz
becomes foo
.
state.route
The current name of the route used in the router (e.g. /foo/:bar
).
state.title
The current page title. Can be set using the DOMTitleChange
event.
Routing
Choo is an application level framework. This means that it takes care of everything related to routing and pathnames for you.
Params
Params can be registered by prepending the routename 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
.
Default routes
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
Querystrings (e.g. ?foo=bar
) are ignored when matching routes. An object
containing the key-value mappings exists as state.query
.
Hash routing
Using hashes to delimit routes is supported out of the box (e.g. /foo#bar
).
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.
Following links
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 withrel="noopener noreferrer"
- a modifier key is enabled (e.g.
ctrl
,alt
,shift
ormeta
) - the link's href starts with protocol handler such as
mailto:
ordat:
- 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.
Navigating programmatically
To can 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()
on
your application.
var html = require('choo/html')
var choo = require('choo')
var app = choo()
app.route('/', function (state, emit) {
return html`<div>Hello ${state.name}</div>`
})
var state = { name: 'Node' }
var string = app.toString('/', state)
console.log(string)
// => '<div>Hello Node</div>'
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.
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.
var el = html`<div>node</div>`
// tell nanomorph to not compare the DOM tree if they're both divs
el.isSameNode = function (target) {
return (target && target.nodeName && target.nodeName === 'DIV')
}
Reordering lists
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.
var el = html`
<section>
<div id="first">hello</div>
<div id="second">world</div>
</section>
`
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.
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.
Setting up browserify transforms can sometimes be a bit of hassle; to make this
more convenient we recommend using bankai with --optimize
to
compile your assets for production.
FAQ
Why is it called choo?
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:
Is it called choo, choo.js or...?
It's called "choo", though we're fine if you call it "choo-choo" or "chugga-chugga-choo-choo" too. The only time "choo.js" is tolerated is if / when you shimmy like you're a locomotive.
Does choo use a virtual-dom?
choo
uses nanomorph, which diffs real DOM nodes instead of
virtual nodes. It turns out that browsers are actually ridiculously good at
dealing with DOM nodes, 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.
How can I support older browsers?
Template strings aren't supported in all browsers, and parsing them creates
significant overhead. To optimize we recommend running browserify
with
yo-yoify as a global transform or using bankai directly.
$ browserify -g yo-yoify
Is choo production ready?
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 first
:sparkles:
app = choo([opts])
Initialize a new choo
instance. opts
can also contain the following values:
- opts.history: default:
true
. Listen for url changes through the history API. - opts.href: default:
true
. Handle all relative<a href="<location>"></a>
clicks and callemit('render')
app.use(callback(state, emitter))
Call a function and pass it a state
and emitter
. emitter
is an instance
of nanobus. You can listen to
messages by calling emitter.on()
and emit messages by calling
emitter.emit()
.
See #events for an overview of all events.
app.route(routeName, handler(state, emit))
Register a route on the router. The handler function is passed app.state
and app.emitter.emit
as arguments. Uses nanorouter under the
hood.
See #routing for an overview of how to use routing efficiently.
app.mount(selector)
Start the application and mount it on the given querySelector
. Uses
nanomount under the hood. 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.
tree = app.start()
Start the application. Returns a tree of DOM nodes that can be mounted using
document.body.appendChild()
.
app.toString(location, [state])
Render the application to a string. Useful for rendering on the server.
choo/html
Create DOM nodes from template string literals. Exposes bel. Can be optimized using yo-yoify.
choo/html/raw
Exposes bel/raw helper for rendering raw HTML content.
Installation
$ npm install choo
See Also
- bankai - streaming asset compiler
- stack.gl - open software ecosystem for WebGL
- yo-yo - tiny library for modular UI
- bel - composable DOM elements using template strings
- tachyons - functional CSS for humans
- sheetify - modular CSS bundler for
browserify
Support
Creating a quality framework takes a lot of time. Unlike others frameworks, Choo is completely independently funded. We fight for our users. This does mean however that we also have to spend time working contracts to pay the bills. This is where you can help: by chipping in you can ensure more time is spent improving Choo rather than dealing with distractions.
Sponsors
Become a sponsor and help ensure the development of independent quality software. You can help us keep the lights on, bellies full and work days sharp and focused on improving the state of the web. Become a sponsor
Backers
Become a backer, and buy us a coffee (or perhaps lunch?) every month or so. Become a backer