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>
            &copy; 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 called view and takes a single parameter vnode, similar to class components in React (where this function is called render)
  • 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 use state, instead all access to state, props, child elements and so on is via the vnode 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.