Views are Choo's rendering abstraction. It's the part that takes the internal state and renders elements to the DOM.
Choo is entirely event-driven. It follows the paradigm of "data down, events up". This means that in views, only data is ever passed down — and to trigger something in a parent node, an event is emitted.
In Choo we wanted to make rendering declarative. This means that you declare what the DOM should look like, and Choo takes care of making it happen. This is similar in spirit to virtual DOM libraries, but instead of using a "virtual" DOM, we use actual DOM nodes.
Declarative rendering is great, because it generally leads to more maintainable, performant and reliable code.
To render something in Choo, there's a few steps we need to follow. First we create an application instance, then we declare a route, and finally we mount the application on the DOM.
Each route takes a route name, and a callback as arguments. The route maps
directly to the browser's window.location
.
var html = require('choo/html') // 1.
var choo = require('choo')
var app = choo() // 2.
app.route('/', (state, emit) => { // 3.
return html`
<body> <!-- 4. -->
Hello World
</body>
`
})
app.mount('body') // 5.
choo/html
module. It's
common to name the import html
which is picked up for syntax highlighting
by GitHub and many editors.app.route()
. The first argument is the route name,
the second argument is the view that's called. It's passed two arguments:
state
and emit
. state
is a shared object that's shared throughout the
application. emit
allows emitting events, which in turn can be picked up
by stores."Hello World"
.app.mount()
we can tell the router to start rendering on the
DOM's document
body.app.mount()
is the primary call to start rendering elements on the DOM. It
takes a CSS
selector
or DOM Node as the first argument, and treats it as the root node to diff it
against. This is ideal in combination with Server Rendering, because the
application in the client can pick up right where the server left off.
app.start()
is similar to app.mount()
, but instead of starting to apply
patches on nodes straight away, it returns a node that can be added manually
onto the DOM.
var choo = require('choo')
var app = choo()
app.mount('body') // 1.
var target = document.querySelector('body') // 2.
app.mount(target)
var element = app.start() // 3.
document.body.appendChild(element)
document.body
. Internally we wait
for the DOM to finish loading before performing a document.querySelector()
call, and starting diffing the DOM.app.mount()
directly. Be careful here, because if the DOM hasn't finished loading yet,
it may not be able to find the node requested by the query selector.app.mount()
, the DOM node that's being selected as the root node
must be the same type as the DOM most outer DOM node returned by a view. For
technical reasons the outer node must stay the same type for DOM diffing to
work.To handle user input, views can attach event listeners onto the DOM. These
listeners can in turn emit events on the event bus. To make sure that the
application's flow is easy to reason about, views cannot attach listeners on
the event bus themselves. This is where "data down, events up" becomes
visible in the code: the view only has access to emit()
, while anything that is
declared through app.use()
, such as a store, has access to the whole event bus
through emitter
. This can both send events with emitter.emit()
, as well as
receive them with emitter.on()
.
There are many events available on DOM elements, and most are available as
attributes on DOM elements. For example form submissions can be detected by
adding an onsubmit
attribute to the form.
var html = require('choo/html')
var choo = require('choo')
var app = choo()
app.use((state, emitter) => { // 1.
emitter.on('click', () => { // 2.
console.log('clicked')
})
})
app.route('/', (state, emit) => {
return html`
<body>
<button onclick=${() => emit('click')}> <!-- 3. -->
Click Me
</button>
</body>
`
})
app.mount('body')
'click'
event is emitted, we'll console.log()
some text to the
console.'click'
event. This will be picked up by the store, and in
turn logs a value to the console.An application often has multiple views. Defining them all in a single file can
quickly turn a neat application into something unmaintainable. Instead a common
pattern is to define views in a views/
directory.
Each view then imports all the code it needs, and exports the function to create the view.
var html = require('choo/html')
module.exports = function (state, emit) {
return html`
<body>
What's up with choo?
</body>
`
}
Every good story needs a good title, and web pages are no different. Choo has
built-in support to edit the page's title through the 'DOMTitleChange
event.
module.exports = function (state, emit) {
emit('DOMTitleChange', 'Cool main page')
return html`
<body>
I choo choose you
</body>
`
}
The page title will now update whenever the page loads. Different pages can set
different titles, so it will always be up to date. Even better: when combining
it with server rendering, the title can be picked up from state.title
, and
used to set the title correctly in the header for initial render.
There are a few downsides though: if you're debugging, this might fire a lot of title changed events even if nothing's changed. And given that our title is static, we could be caching it. Let's optimize it a little.
var title = 'Cool main page' // 1.
module.exports = function (state, emit) {
if (state.title !== title) emit('DOMTitleChange', title) // 2.
return html`
<body>
I choo choose you
</body>
`
}
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.