- Notifications
You must be signed in to change notification settings - Fork596
🚂🚋 - sturdy 4kb frontend framework
License
choojs/choo
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
4kb
framework for creating sturdy frontend applications- Features
- Example
- Philosophy
- Events
- State
- Routing
- Server Rendering
- Components
- Optimizations
- FAQ
- API
- Installation
- See Also
- Support
- 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!
varhtml=require('choo/html')vardevtools=require('choo-devtools')varchoo=require('choo')varapp=choo()app.use(devtools())app.use(countStore)app.route('/',mainView)app.mount('body')functionmainView(state,emit){returnhtml`<body><h1>count is${state.count}</h1><buttononclick=${onclick}>Increment</button></body> `functiononclick(){emit('increment',1)}}functioncountStore(state,emitter){state.count=0emitter.on('increment',function(count){state.count+=countemitter.emit('render')})}
Want to see more examples? Check out theChoo handbook.
We believe programming should be fun and light, not stern and stressful. It'scool to be cute; using serious words without explaining them doesn't make forbetter results - if anything it scares people off. We don't want to be scary,we want to be nice and fun, and thencasually be the best choice around.Real casually.
We believe frameworks should be disposable, and components recyclable. We don'twant a web where walled gardens jealously compete with one another. By makingthe DOM the lowest common denominator, switching from one framework to anotherbecomes frictionless. Choo is modest in its design; we don't believe it willbe top of the class forever, so we've made it as easy to toss out as it is topick up.
We don't believe that bigger is better. Big APIs, large complexities, longfiles - we see them as omens of impending userland complexity. We want everyoneon a team, no matter the size, to fully understand how an application is laidout. And once an application is built, we want it to be small, performant andeasy to reason about. All of which makes for easy to debug code, better resultsand super smiley faces.
At the core of Choo is an event emitter, which is used for both applicationlogic but also to interface with the framework itself. The package we use forthis isnanobus.
You can access the emitter throughapp.use(state, emitter, app)
,app.route(route, view(state, emit))
orapp.emitter
. Routes only have access to theemitter.emit
method to encourage people to separate business logic fromrender logic.
The purpose of the emitter is two-fold: it allows wiring up application codetogether, and splitting it off nicely - but it also allows communicating withthe Choo framework itself. All events can be read as constants fromstate.events
. Choo ships with the following events built in:
Choo emits this when the DOM is ready. Similar to the DOM's'DOMContentLoaded'
event, except it will be emitted even if the listener isaddedafter the DOM became ready. Usesdocument-ready under the hood.
This event should be emitted to re-render the DOM. A common pattern is toupdate thestate
object, and then emit the'render'
event straight after.Note that'render'
will only have an effect once theDOMContentLoaded
eventhas been fired.
Choo emits this event whenever routes change. This is triggered by either'pushState'
,'replaceState'
or'popState'
.
This event should be emitted to navigate to a new route. The new route is addedto the browser's history stack, and will emit'navigate'
and'render'
.Similar tohistory.pushState.
This event should be emitted to navigate to a new route. The new route replacesthe current entry in the browser's history stack, and will emit'navigate'
and'render'
. Similar tohistory.replaceState.
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, andimmediately afterward the'navigate'
and'render'
events will be emitted.Similar tohistory.popState. (Notethatemit('popState')
willnot cause a popState action - usehistory.go(-1)
for that - this is different from the behaviour ofpushState
andreplaceState
!)
This event should be emitted whenever thedocument.title
needs to be updated.It will set bothdocument.title
andstate.title
. This value can be usedwhen server rendering to accurately include a<title>
tag in the header.This is derived from theDOMTitleChanged event.
Choo comes with a shared state object. This object can be mutated freely, andis passed into the view functions whenever'render'
is emitted. The stateobject comes with a few properties set.
When initializing the application,window.initialState
is used to provisionthe initial state. This is especially useful when combined with serverrendering. Seeserver rendering for more details.
A mapping of Choo's built in events. It's recommended to extend this objectwith your application's events. By defining your event names once and settingthem onstate.events
, it reduces the chance of typos, generally autocompletesbetter, makes refactoring easier and compresses better.
The current params taken from the route. E.g./foo/:bar
becomes available asstate.params.bar
If a wildcard route is used (/foo/*
) it's available asstate.params.wildcard
.
An object containing the current queryString./foo?bin=baz
becomes{ bin: 'baz' }
.
An object containing the current href./foo?bin=baz
becomes/foo
.
The current name of the route used in the router (e.g./foo/:bar
).
The current page title. Can be set using theDOMTitleChange
event.
An objectrecommended to use for local component state.
Generic class cache. Will lookup Component instance by id and create one if notfound. Useful for working with statefulcomponents.
Choo is an application level framework. This means that it takes care ofeverything 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 onstate.params
(e.g.state.params.bar
). Wildcard routes can be registered with*
, e.g./foo/*
.The value of the wildcard will be saved understate.params.wildcard
.
Sometimes a route doesn't match, and you want to display a page to handle it.You can do this by declaringapp.route('*', handler)
to handle all routesthat didn't match anything else.
Querystrings (e.g.?foo=bar
) are ignored when matching routes. An objectcontaining the key-value mappings exists asstate.query
.
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 atthe website root. Unless hash routing is enabled, if a hash is found we check ifthere's an anchor on the same page, and will scroll the element into view. Usingboth 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 thenanohref module. This can bedisabled application-wide by passing{ href: false }
to the applicationconstructor. 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 handletarget=_blank
if they also haverel="noopener noreferrer"
on them. This is needed toproperly sandbox webpages.
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 yourchoo
instance.
varhtml=require('choo/html')varchoo=require('choo')varapp=choo()app.route('/',function(state,emit){returnhtml`<div>Hello${state.name}</div>`})varstate={name:'Node'}varstring=app.toString('/',state)console.log(string)// => '<div>Hello Node</div>'
When starting an application in the browser, it's recommended to provide thesamestate
object available aswindow.initialState
. When the application isstarted, it'll be used to initialize the application state. The process ofserver rendering, and providing an initial state on the client to create theexact same document is also known as "rehydration".
For security purposes, afterwindow.initialState
is used it is deleted fromthewindow
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 applicationhold a self-contained state or to not rerender when the application does. Thisis common when using 3rd party libraries to e.g. display an interactive map or agraph 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. Seenanocomponent for documentation on the component class.
// map.jsvarhtml=require('choo/html')varmapboxgl=require('mapbox-gl')varComponent=require('choo/component')module.exports=classMapextendsComponent{constructor(id,state,emit){super(id)this.local=state.components[id]={}}load(element){this.map=newmapboxgl.Map({container:element,center:this.local.center})}update(center){if(center.join()!==this.local.center.join()){this.map.setCenter(center)}returnfalse}createElement(center){this.local.center=centerreturnhtml`<div></div>`}}
// index.jsvarchoo=require('choo')varhtml=require('choo/html')varMap=require('./map.js')varapp=choo()app.route('/',mainView)app.mount('body')functionmainView(state,emit){returnhtml`<body><buttononclick=${onclick}>Where am i?</button>${state.cache(Map,'my-map').render(state.center)}</body> `functiononclick(){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')})})})
When working with stateful components, one will need to keep track of componentinstances –state.cache
does just that. The component cache is a functionwhich takes a component class and a unique id (string
) as its first twoarguments. Any following arguments will be forwarded to the component constructortogether withstate
andemit
.
The default class cache is an LRU cache (usingnanolru), meaning itwill only hold on to a fixed amount of class instances (100
by default) beforestarting to evict the least-recently-used instances. This behavior can beoverriden withoptions.
Choo is reasonably fast out of the box. But sometimes you might hit a scenariowhere a particular part of the UI slows down the application, and you want tospeed it up. Here are some optimizations that are possible.
Sometimes we want to tell the algorithm to not evaluate certain nodes (and itschildren). This can be because we're sure they haven't changed, or perhapsbecause another piece of code is managing that part of the DOM tree. To achievethisnanomorph
evaluates the.isSameNode()
method on nodes to determine ifthey should be updated or not.
varel=html`<div>node</div>`// tell nanomorph to not compare the DOM tree if they're both divsel.isSameNode=function(target){return(target&&target.nodeName&&target.nodeName==='DIV')}
It's common to work with lists of elements on the DOM. Adding, removing orreordering elements in a list can be rather expensive. To optimize this you canadd anid
attribute to a DOM node. When reordering nodes it will comparenodes with the same ID against each other, resulting in far fewer re-renders.This is especially potent when coupled with DOM node caching.
varel=html`<section><divid="first">hello</div><divid="second">world</div></section>`
We use therequire('assert')
module from Node core to provide helpful errormessages in development. In production you probably want to strip this usingunassertify.
To convert inlined HTML to valid DOM nodes we userequire('nanohtml')
. This hasoverhead during runtime, so for production environments we should unwrap thisusing thenanohtml transform.
Setting up browserify transforms can sometimes be a bit of hassle; to make thismore convenient we recommend usingbankai build to build your assets for production.
Because I thought it sounded cute. All these programs talk about being"performant","rigid","robust" - I like programming to be light, fun andnon-scary. Choo embraces that.
Also imagine telling some business people you chose to rewrite somethingcritical for serious bizcorp using a train themed framework.:steam_locomotive::train::train::train:
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.
Choo usesnanomorph, which diffs real DOM nodes instead ofvirtual nodes. It turns out thatbrowsers are actually ridiculously good atdealing with DOM nodes, and it has the added benefit ofworking withany library that produces valid DOM nodes. So to put a longanswer short: we're using something even better.
Template strings aren't supported in all browsers, and parsing them createssignificant overhead. To optimize we recommend runningbrowserify
withnanohtml as a global transform or usingbankai directly.
$ browserify -g nanohtml
Sure.
This section provides documentation on how each function in Choo works. It'sintended to be a technical reference. If you're interested in learning choo forthe first time, consider reading through thehandbook first:sparkles:
Initialize a newchoo
instance.opts
can also contain the following values:
- opts.history: default:
true
. Listen for url changes through thehistory API. - opts.href: default:
true
. Handle all relative<a href="<location>"></a>
clicks and callemit('render')
- opts.cache: default:
undefined
. Override default class cache used bystate.cache
. Can be a anumber
(maximum number of instances in cache,default100
) or anobject
with ananolru-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 isnot mounted at the website root.
Call a function and pass it astate
,emitter
andapp
.emitter
is an instanceofnanobus. You can listen tomessages by callingemitter.on()
and emit messages by callingemitter.emit()
.app
is the same Choo instance. Callbacks passed toapp.use()
are commonly referred to as'stores'
.
If the callback has a.storeName
property on it, it will be used to identifythe callback during tracing.
See#events for an overview of all events.
Register a route on the router. The handler function is passedapp.state
andapp.emitter.emit
as arguments. Usesnanorouter under thehood.
See#routing for an overview of how to use routing efficiently.
Start the application and mount it on the givenquerySelector
,the given selector can be a String or a DOM element.
In the browser, this willreplace the selector provided with the tree returned fromapp.start()
.If you want to add the app as a child to an element, useapp.start()
to obtain the tree and manually append it.
On the server, this will save theselector
on the app instance.When doing server side rendering, you can then check theapp.selector
property to see where the render result should be inserted.
Returnsthis
, so you can easily export the application for server side rendering:
module.exports=app.mount('body')
Start the application. Returns a tree of DOM nodes that can be mounted usingdocument.body.appendChild()
.
Render the application to a string. Useful for rendering on the server.
Create DOM nodes from template string literals. Exposesnanohtml. Can be optimized usingnanohtml.
Exposesnanohtml/raw helper for rendering raw HTML content.
$ npm install choo
- bankai - streaming asset compiler
- stack.gl - open software ecosystem for WebGL
- yo-yo - tiny library for modular UI
- tachyons - functional CSS forhumans
- sheetify - modular CSS bundler for
browserify
Creating a quality framework takes a lot of time. Unlike others frameworks,Choo is completely independently funded. We fight for our users. This does meanhowever 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 spentimproving Choo rather than dealing with distractions.
Become a sponsor and help ensure the development of independent qualitysoftware. You can help us keep the lights on, bellies full and work days sharpand focused on improving the state of the web.Become asponsor
Become a backer, and buy us a coffee (or perhaps lunch?) every month or so.Become a backer
About
🚂🚋 - sturdy 4kb frontend framework