Choo is built up out of two parts: stores and views. In order to render a view,
it must be added to the application through app.route()
. This is the router.
In many other frameworks, routing is done by a separate library. We found that routing is common in most applications, so making it part of the framework makes sense.
Routing has a few parts to it. Routing must handle the browser's history API (e.g. going forward & backwards). It must handle anchor tags, and programmatic actions. There's more than a few moving pieces.
To perform routing, Choo uses a Trie data structure. This means that our routing is fast, and the order in which routes are added doesn't matter.
Note: It's recommended to read the views chapter first, as we'll assume that you're already familiar with how views work. This chapter is intended to give an overview of how routing works in Choo.
Every application needs an entry point. Routes in Choo are defined relative to
the host. The route '/'
maps to www.mysite.com
. The route '/foo'
maps to
www.mysite.com/foo
.
var html = require('choo/html')
var choo = require('choo')
var app = choo() // 1.
app.route('/', view) // 2.
app.mount('body') // 3.
function view () { // 4.
return html`
<body>Hello World</body>
`
}
/
route. This means that if people
navigate to oursite.com
, this will be the route that is enabled.There's no point in routing if you can't navigate between routes. The easiest way
to navigate between routes is to use <a>
tags (anchor tags). Choo picks up
whenever a tag was clicked, and figures out which route to trigger on the
router.
var html = require('choo/html')
var choo = require('choo')
var app = choo()
app.route('/', view) // 1.
app.route('/second', second) // 2.
app.mount('body') // 3.
function view () {
return html`
<body>
<a href="/second">
Navigate to the next route.
</a>
</body>
`
}
function second () {
return html`
<body>
<a href="/">
Navigate back.
</a>
</body>
`
}
/
. This is the first route that's loaded
when someone visits our site. It contains a single anchor tag that points to
/second
./second
. This won't be shown unless someone
navigates to /second
. When it's rendered, it contains a single anchor tag
that points to /
.Preparing for things to go wrong is an important part of programming. At some point, someone using your application will land on an unexpected route. It's important to not just crash the page, but to show something helpful to explain what just happened. This is where fallback routes come in.
var html = require('choo/html')
var choo = require('choo')
var app = choo()
app.route('/', view) // 1.
app.route('/404', notFound) // 2.
app.route('/*', notFound) // 3.
app.mount('body') // 4.
function view () {
return html`
<body>
<a href="/uh-oh">
Click Click Click
</a>
</body>
`
}
function notFound () {
return html`
<body>
<a href="/">
Route not found. Navigate back.
</a>
</body>
`
}
/
. This is the first route that's loaded
when someone visits our site. It contains a single anchor tag that points
to /uh-oh
, which is a route that doesn't exist./404
. This
helps with debugging, and is often treated specially when deploying to
production.*
. The asterisk symbol is pronounced
"glob". Our glob route will now handle all routes that didn't match
anything./
, it will be handled by the fallback route.Sometimes you want to encode some meta information in a URL. This is
often done with querystrings. Querystrings look somewhat like this:
?foo=bar&bin=baz
. This querystring contains two pairs: foo=bar
and
bin=baz
.
While using querystrings is great for URLs, when you want to use them
in a project, you'll probably want to convert them to an Object first.
Choo does this for you when URLs are updated, and exposes the
querystring pairs as state.query
.
So ?foo=bar&bin=baz
would be exposed as state.query.foo
and
state.query.bin
.
Sometimes there will be pages that have the same layout but different data, such as user pages or blog entries. This requires dynamic routing. In Choo we have two types of syntax for dynamic routing.
Params are declared with the :
syntax. For example /foo/:bar
. This means
that the /foo
part is static, and :bar
can be any value, up until the next
slash (/
).
The value from a param is exposed in Choo as state.params
. So say we have
the route /foo/:bar
, and we navigate to /foo/beep
, the value of
state.params.bar
will be 'beep'
.
var html = require('choo/html')
var choo = require('choo')
var app = choo()
app.route('/', placeholder)
app.route('/:user', placeholder)
app.route('/:user/:repo', placeholder)
app.route('/:user/settings', placeholder)
app.route('/404', placeholder)
function placeholder (state) {
console.log(state.params)
return html`<body>placeholder</body>`
}
In most cases params are the answer for dynamic routing. They're named, readily available, and easy to traverse. However, they can't cover cases where the amount of slashes in route will be unknown.
Take for example GitHub's code view. To navigate to Choo's html/raw
API, the
route is:
github.com/choojs/choo/blob/master/html/raw.js .
In this case, all parts of the route after master/
are unknown. This means
that params
can't match the route correctly, because there's an unknown
amount of slashes after master/
. This is what wildcards are for.
If we were building GitHub's code view with Choo, we could express the route
as: /:user/:repo/blob/:branch/*
. The value of state.params.wildcard
would
then be: '/html/raw.js'
.
Try and use wildcards sparingly. They're the most powerful tool in the routing toolbox, which means that if you're not careful you might end up reimplementing a router on top of it yourself.
There is one last thing we should touch on with dynamic routing: what to do when a route is not found.
If you're using params or wildcards, there can always be a case where a route
isn't found. For example if we have the route /:users
, if a particular user
does not exist, we might want to show a fallback route instead.
There are generally two approaches to this: hard redirects, and soft redirects.
/foobar
doesn't match a known user, we'll navigate to /404
instead./foobar
doesn't match, we'll require and render the
content of the /404
view instead.It's generally recommended to use soft redirects, as they interfere the least with the browser, and allow users to recover from an error (e.g. fix typos in a url).
Some links in your app will point to other pages. In order to do this safely, we must add some attributes to our link tags. This is needed, so the pages we link to can't hijack our page.
In order to link to an external link, we must do one of the following:
.preventDefault()
called on ittarget="_blank"
attribute with rel="noopener noreferrer"
ctrl
, alt
, shift
or meta
)mailto:
or dat:
download
attributevar html = require('choo/html')
html`
<a href="other.site.com/something" target=_blank rel="noopener noreferrer">
Click to open in another page
</a>
`
It's often necessary to change routes after some event happens. For example, someone logs in, and we need to redirect them to the logged in page. We need programmatic navigation.
Choo comes with several events built-in to allow you to navigate using the Browser's History API. The History API is a thin wrapper around a Stack. As you navigate, history entries are added to the stack. And when you hit the back button, it pops entries off the stack.
These are the events that Choo ships with:
'pushState'
- navigate to a new route.'replaceState'
- replace the current route with a new route. This is useful
for things like redirects.'popState'
- navigate to a previous route.'navigate'
- emitted when any of the above events is emitted.Most of the time you'll be wanting to use the pushState
event. replaceState
and popState
are much less common, although you might need them from time to
time.
note: Anchor tags, buttons, and input submissions have slightly different
use cases. <a>
tags are meant for static links to pages.
<button>
tags are meant for conditional logic that doesn't always link to
another page. And <input type="submit">
tags are meant to be used to submit
forms.
var html = require('choo/html')
var choo = require('choo')
var app = choo()
var app = choo()
app.route('/', view) // 1.
app.route('/other', other) // 2.
app.mount('body')
function view () { // 3.
return html`
<body>
<a href="/">Navigate</a>
</body>
`
}
function other (state, emit) { // 4.
return html`
<body>
<button onclick=${onclick}>
Go Back
</button>
</body>
`
function onClick () {
emit('popState')
}
}
/
./other
./other
.As we briefly mentioned in the previous section, we can listen to route
changes using the 'navigate'
event.
var html = require('choo/html')
var choo = require('choo')
var app = choo()
var app = choo()
app.use((state, emitter) => { // 1.
emitter.on('navigate', () => { // 2.
console.log(`Navigated to ${state.route}`) // 3.
})
})
route
is a string here.Sometimes when you deploy a static app, you can't control the server part of it. Changing the route might mean the server interprets it differently, causing problems to occur when reloading the page.
To work around this, Choo supports hash routing. Instead of writing a
route as foobar.com/bin/baz
, you can write it as foobar.com#bin/baz
. To
enable hash routing, initialize your Choo app with the hash
option set to
true
.
var choo = require('choo')
var app = choo({ hash: true }) // 1.
app.route('/', view)
app.route('#hi', view)
app.route('#hi/hello', view)
function view (state, emit) {
return html`
<body>${state.route}</body>
`
}
Another use of hashes in urls is to map to anchors on the page. This is commonly used for headings in articles. So when a link is shared, they're navigated to the right heading in the page.
Choo supports page anchors out of the box. Unless hash routing is enabled, it will first try to match anchors on the page. If no matching anchor is found, or if hash routing is enabled, it will try to find a matching route in the router. After the new route has been rendered, Choo will once again check to see if a matching anchor has appeared on the page.
note: Using both hash routing and anchor links on the page is generally not recommended.
There are cases where you might not need routing at all, for example when using Choo to create iframe widgets. Choo accepts options in the contructor to disable either hrefs or the entire history API integration (including hrefs).
Because there is no more routing events available, you'll need to handle
it yourself. You can manually change the value of state.href
, before
calling the 'render'
event. Or perhaps even simpler would be to only
use a single route. It's up to you at this point.
var choo = require('choo')
var app = choo({ href: false }) // 1.
<a href="">
links.var choo = require('choo')
var app = choo({ history: false }) // 1.
<a href="">
links, and all built-in history
events.Browsers are generally quite good at scrolling pages. When using the history API, previous locations on the page are restored. But this is not great for every app.
If for example you want to always scroll to the top of the page whenever we navigate, you'll notice the page jumping around. This is not great user experience.
In order to manually take control of scrolling whenever page navigation
occurs, we can make use of
history.scrollRestoration.
Setting the value to 'manual'
disables all default scroll behavior, allowing us to take control
instead.
if (typeof window !== 'undefined' && window.history.scrollRestoration) {
window.history.scrollRestoration = 'manual'
}
Websites generally consist of 3 main elements: paragraph text, lists and forms. While paragraph text is generally straightforward to place on a page, lists and forms require some more work. This section explains everything you need to know to work with forms in Choo.
Connecting to the network is essential for applications. This section is all about the browser's network APIs, and how to use them in Choo.
Choo is built up out of two parts: stores and views. In order to render a view,
it must be added to the application through app.route()
. This is the router.
Server rendering is an excellent way to speed up the load time of your pages. This section shows how to effectively render Choo apps in Node.
State machines are a great way to manage different states in your application. In this section we'll learn how to use and implement state machines.
Stores are Choo's data abstraction. They're meant to both hold application data, and listen for events to change it. In traditional systems this is sometimes also known as "models".
Views are Choo's rendering abstraction. It's the part that takes the internal state and renders elements to the DOM.