Basic Components¶
Mithril, like React, uses a component-based structure. However, unlike React, Mithril comes with routing built in. To make this easier to manage, we will thus split the routing entry point from the main application component.
First create the src/index.html
template with the following code:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8"/>
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/2.8.94/css/materialdesignicons.min.css"/>
<% for (var css in htmlWebpackPlugin.files.css) { %>
<link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet"/>
<% } %>
<base href="/"/>
</head>
<body>
<div id="app-entry-point"></div>
<% for (var chunk in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[chunk] %>"></script>
<% } %>
</body>
</html>
As you can see, there is no difference between how the Mithril application and the React application is installed. However, because we set the install point to the src/js/index.js
, we next need to create that file and add the following content:
import m from "mithril";
import Athena from "./athena.jsx";
import "../styles/app.scss";
const root = document.getElementById("app-entry-point");
m.route(root, "/", {
"/": Athena
});
This is the core of Mithril’s built-in routing system. Note, that we utilized a variable to create a shorthand for Mithril. We use the m.route
function to define all available routes and attach the resulting application to the browser’s DOM. The function takes three parameters. The first is the attachmentpoint, specifying where in the DOM to place and render the application. The second is the default route. Whatever you specify here will be shown, if the URL path does not match any of the defined routes. It is thus important, that this route exists in the route definition (the third parameter). The third parameter is the route definition object, which maps URL paths to Mithril components. Here we map the root URL path '/'
to our Athena
component.
With the routing set up, the next step is to create our athena.jsx
file and add the following content:
import m from "mithril";
import AriaMenu from "./components/aria-menu.jsx";
export default function Athena() {
return {
view: (vnode) => {
return (
<div className="container-fluid gx-0">
<header className="row gx-0">
<nav className="navbar navbar-dark bg-dark" aria-label="Main">
<AriaMenu class="navbar-nav horizontal" label="Main">
<li className="navbar-brand">Athena - Study Portal</li>
<li className="nav-item">
<a className="nav-link active" role="menuitem" tabIndex="0">My Modules</a>
</li>
<li className="nav-item">
<a className="nav-link" role="menuitem" tabIndex="-1">My Exams</a>
</li>
</AriaMenu>
<AriaMenu class="navbar-nav horizontal" label="User">
<li className="nav-item">
<a className="nav-link" role="menuitem" tabIndex="0">Login</a>
</li>
<li className="nav-item">
<a className="nav-link" role="menuitem" tabIndex="-1">Logout</a>
</li>
</AriaMenu>
</nav>
</header>
<main>{vnode.children}</main>
<footer>
© 2018 - Mark Hall (
<a href="mailto:mark.hall@informatik.uni-halle.de" tabIndex="0">
mark.hall@informatik.uni-halle.de
</a>
)
</footer>
</div>
);
},
};
}
As you can see, it basically looks like a React component, with a few small changes:
- The React
return
statement is inside a function that is calledview
and takes a single parametervnode
, similar to class components in React (where this function is calledrender
) - Unlike React, Mithril uses normal HTML lower-case attributes and
class
. - In Mithril to access the component’s state in the
view
method, we do not usestate
, instead all access to state, props, child elements and so on is via thevnode
parameter. In this example here, we use{vnode.children}
to define where the child components are to be inserted (similar to how we used{{output}}
with Ember templates).
Our Athena
component uses the AriaMenu
component, thus create a new file src/js/components/aria-menu.jsx
and add the following code:
import m from "mithril";
export default function AriaMenu() {
function keyDown(ev) {
if (ev.keyCode === 39) {
let nextElement = ev.target.parentElement.nextElementSibling;
while (
nextElement &&
(nextElement.getAttribute("role") === "separator" ||
nextElement
.querySelector('*[role="menuitem"]')
.getAttribute("aria-disabled") === "true")
) {
nextElement = nextElement.nextElementSibling;
}
if (nextElement) {
nextElement.querySelector('*[role="menuitem"]').focus();
}
} else if (ev.keyCode === 37) {
let previousElement = ev.target.parentElement.previousElementSibling;
while (
previousElement &&
(previousElement.getAttribute("role") === "separator" ||
previousElement
.querySelector('*[role="menuitem"]')
.getAttribute("aria-disabled") === "true")
) {
previousElement = previousElement.previousElementSibling;
}
if (previousElement) {
previousElement.querySelector('*[role="menuitem"]').focus();
}
}
}
return {
view: (vnode) => {
return (
<ul
class={vnode.attrs.class}
role="menu"
onkeydown={keyDown}
aria-label={vnode.attrs.label}
>
{vnode.children}
</ul>
);
},
};
}
As stated above, we use the vnode
to access everything and here you can see, that all attribute values are available via vnode.attrs
. Apart from that, everything works the same as in the equivalent React component.
If you now run the dev server
$ webpack serve --config webpack.conf.js
you will see your Mithril application.