Components

Our main components (or first level components, located in app/components/pages) are the ones mapped with the routes. They are plain Mithril components, Javascript objects that contain a view method (the function that renders a virtual DOM vnode into a real DOM element) and other optional lifecycle methods, among which the most important is the oninit, which can be considered as the controller of our component (it is run only once and before the view is rendered).

Our typical main component will look like this:

const m = require('mithril');
const Footer = require('../components/Footer.js');
const Header = require('../components/Header.js');
const LoadingDots = require('../components/LoadingDots.js');
const Layout = require('../components/Layout.js');
const NotFound = require('./NotFound.js');
const resources = require('../resources.js');

module.exports = {
    oninit: vnode => new Promise((resolve) => {
        vnode.state.loading = true;
        resources.getSomething() // any async data fetching
            .then((content) => {
                vnode.state.content = content;
                vnode.state.loading = false;
                m.redraw();
                resolve();
            })
            .catch((err) => {
                vnode.state.error = err;
                m.redraw();
                resolve();
            });
    }),

    view: vnode => vnode.state.error ? m(NotFound) : m(Layout, [
        m(Header),
        m('main', [
            vnode.state.loading ? m(LoadingDots) : vnode.state.content
        ]),
        m(Footer)
    ])
};

In the oninit function we will typically need to async fetch remote content data: in this simplified example we are passing down the fetched data to the view via vnode.state, component's vnode internal state. In the view we pass it down to the Layout component as argument, inside the entire vnode tree of the page (together with the Header and the Footer); otherwise, in case of errors, we are going to render a different component (Error - more on this later).

Blocking vs non-blocking data flow

Since the rendering sequences for server-side web and client-side web are different (the first is blocking, the second is async), we need to set a slightly different behaviour in the corresponding code.

On the server side, Express needs to know when all our async operations in the oninit are done before proceeding with the rendering of the resulting view. We build then our main components as async components: the oninit function is returning a Promise, and we just have to resolve it by calling resolve() when our async calls are complete (successfully or not).

On the client side, the view function runs for the first time before the async fetch is completed, showing a loading animation as default (the vnode.state.loading loading flag is set to true). Same as before, when the async calls are completed, we set the loading flag as false and we finally call Mithril's m.redraw() to manually trigger a DOM redraw.