Layout
Before digging into how our main components are built, let's take a look at the Layout component we will use to wrap
the content of our view
functions with a consistent template structure (header, main content and footer).
This is one of the few points in the app where, for convenience, we won't fully separate server and client code (server
side code is contained in the app/server
folder and will not be included in the bundled SPA package). We will instead
use a single module for both worlds, using a conditional to detect the environment (server or client) and export the
right object.
Our app/components/Layout.js
will then look like this:
const m = require('mithril');
const t = require('../translate.js');
const setHead = (vnode) => {
const head = {};
head.title = (vnode.attrs.title || '[MISSING TITLE]') + ' · ';
if (vnode.attrs.slug === 'index') head.title = '';
head.title += t('header.title');
head.description = vnode.attrs.description;
return head;
};
const LayoutClient = {
// ...
};
const LayoutServer = {
// ...
};
module.exports = process.browser ? LayoutClient : LayoutServer;
Client side layout
On the client side, the exported object is quite concise. Since our app will be mounted on the document.body
of the
server-side rendered page, we already have the outer HTML markup, so the view
function only needs to return the
vnode.children
passed as argument:
const LayoutClient = {
oncreate: (vnode) => {
const head = setHead(vnode);
document.title = head.title;
$('meta[name=description]').replaceWith( '<meta name="description" content="' + head.description + '">' );
},
view: vnode => vnode.children
};
We use Mithril's oncreate
]lifecycle method to set the
page's title and metadata after the generated DOM is appended to the body.
Server side layout
On the server side, we need to build from scratch the full HTML markup before passing it to
mithril-node-render before, and as resulting string then to
Express res.send
function.
const LayoutServer = {
oninit: vnode => {
vnode.state.head = setHead(vnode);
},
view: vnode => [
m('!doctype[html]'),
m('html', {lang: vnode.attrs.globals.activeLanguage || 'en'}, [
m('head', [
m('meta', {charset: 'utf-8'}),
m('meta', {name: 'viewport', content: 'width=device-width, initial-scale=1, shrink-to-fit=no'}),
m('meta', {'http-equiv': 'x-ua-compatible', content: 'ie=edge'}),
m('title', vnode.state.head.title),
m('meta', {name: 'description', content: vnode.state.head.description}),
m('link', {rel: 'stylesheet', href: `/assets/css/styles.min.css`}),
m('link', {rel: 'shortcut icon', href: '/assets/img/favicon.ico'})
]),
m('body', [
vnode.children,
m('script', `window.__preloadedState = ${vnode.attrs.stateman._getString()}`),
m('script', {src: `/assets/js/app.min.js`})
])
])
]
};
You might have noticed this line:
m('script', `window.__preloadedState = ${vnode.attrs.stateman._getString()}`),
This is how we pass the app shared state to the client, as a stringified object (more details in the dedicated section).