Rapid prototyping of Single Page Applications

Using Mithril and ParcelJS

The goal of this article is to show my current way of fast building a prototype of web frontends, that I usually use to try out things or to create tiny games. There is no repository attached. This is a tutorial. Maybe you want to follow the steps.

Parcel

Parcel – Blazing fast, zero configuration web application bundler

https://parceljs.org/

Mithril

Mithril is a modern client-side JavaScript framework for building Single Page Applications. It’s small (< 10kb gzip), fast and provides routing and XHR utilities out of the box.

https://mithril.js.org

Mithril will serve as our abstraction of the DOM. It can also be used to communicate with a backend, but this tutorial is frontend only. We use its hyperscript to create templates and wire them together. Anything that has a ‚view‘ function is a component. The ‚view‘ function returns a hyperscript object with can look e.g. like

m('div.wide-container#main-component', {onclick: e => startSomething()},'Hello world')

which is equivalent to

<div class="wide-container" id="main-component" click="startSomething()>Hello world</div>

in HTML. We will in addition use tagl which lets us write the above hyperscript

div.wideContainer$mainComponent({onclick: e => startSomething()},'Hello world')

what can be regarded as hyper hyper. One great benefit of Mithril is that it only contains about 10-20 functions and can be learned really quick.

Setting up the environment

Create a folder with a catchy name and enter it. I will use parcel-mithril-showcase

mkdir parcel-mithril-showcase
cd parcel-mithril-showcase
npm init -y

Let’s setup our environment. ParcelJS bundler is used to create this app. We can either install it globally, in case we want to always be able to create such prototypes fast

npm install -g parcel-bundler

or locally to the project (this can also be done at a later stage), e.g. when we work with others on this prototype and we want to spare them the setup.

npm install --save-dev parcel-bundler

Since the latter does not install the executable to the path we can add it to our package.json file like this.

"scripts": {
    "start": "parcel src/index.html",
    ...
}

This instructs parcel that the index.html is the main entry point of the application. Next we need to create this file and some more empty files to be able to start coding.

.
├── package.json
└── src
    ├── index.html
    ├── main.css
    └── main.js

The content of index.html is

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Parcel Mithril Showcase</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
    <script src="main.js"></script>
</body>
</html>

Now the development environment is started by

parcel src/index.html

or, depending on your above decision

npm start

Coding

Open the file main.js and import mithril

import m from 'mithril';

When you save and watch your shell, parcel will in the background immediately start to import mithril and automagically add it to package.json as a dependency. We do the same for tagl-mithril.

import tagl from 'tagl-mithril';

Since we use tagl now, we can define the tags used in our page as abbreviations as follows, let’s start with a fat heading <h1>

const {h1, div, p} = tagl(m);

Tagl will use Mithril’s hyperscript function m to provide Mithril components that can be used directly in the code. This is all that is needed to render the first content to the page with a component, that will replace the html body.

m.mount(document.body, {
    view: vnode => div.container(
        h1('Hello Mithril'),
        p('With the help of tagl'),
    )
});

Open a browser and navigate to http://localhost:1234 which is the default of parcel for the preview. You will see the „Hello world“, rendered by Mithril.

Basically this is it. You might say, well I’ve seen hello world in a gazillion programming languages before. Correct, but this is not where the fun stops.

Usually I like it not to write too much css. When I don’t want to write anything and just use bare tags, I just include mini.css for the sake of my eyes. In main.css add the line

@import 'mini.css';

…, save and parcel does the magic. The browser will update it’s contents and the page looks a bit nicer.

Creating a more fancy component

Since the creation of components is something you do very often, I created a snippet for this matter:

import m from 'mithril';
import tagl from 'tagl-mithril';

// prettier-ignore
const { address, aside, footer, header, h1, h2, h3, h4, h5, h6, hgroup, main, nav, section, article, blockquote, dd, dir, div, dl, dt, figcaption, figure, hr, li, ol, p, pre, ul, a, abbr, b, bdi, bdo, br, cite, code, data, dfn, em, i, kdm, mark, q, rb, rp, rt, rtc, ruby, s, samp, small, span, strong, sub, sup, time, tt, u, wbr, area, audio, img, map, track, video, embed, iframe, noembed, object, param, picture, source, canvas, noscript, script, del, ins, caption, col, colgroup, table, tbody, td, tfoot, th, thead, tr, button, datalist, fieldset, form, formfield, input, label, legend, meter, optgroup, option, output, progress, select, textarea, details, dialog, menu, menuitem, summary, content, element, slot, template } = tagl(m);

export default vnode => {
    return {
        view(vnode) {
            return [
                'Write your hyperscript here'
            ];
        }
    };
};

This is our starting point. The component everyone needs for his webpage is an analogue clock, because they look nice and something moves. Since Mithril can render any tag, let us try to code it in svg, which is a tag based graphics format and therefore can be directly written using the toolkit.

Stealing the design

Let us visit wikipedia and look for svg-codes for analogue clocks… Ah, here we are (Read about the license on the linked page).

<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     viewBox="-1024 -1024 2048 2048" width="600" height="600">
  <title>Swiss Railway Clock</title>
  <style type="text/css">
    .bg {stroke: none; fill: white;}
    .fc {stroke: none; fill: black;}
    .h1 {stroke: none; fill: black;}
    .h2 {stroke: none; fill: #aa0000;}
  </style>
  <defs>
    <path id="mark1" d="M -20,-1000 l 40,0 0,100 -40,0 z" />
    <path id="mark2" d="M -40,-1000 l 80,0 0,240 -80,0 z" />
    <path id="mark3" d="M -40,-1000 l 80,0 0,300 -80,0 z" />
    <path id="handh" d="M -50,-600  l 50,-50 50,50 0,800  -100,0 z" />
    <path id="handm" d="M -40,-900  l 40,-40 40,40 0,1180 -80,0  z" />
    <g    id="hands">
      <path d="M -10,-910 l  10,-10 10,10 2,300 -24,0 z
               M -13,-390 l  26,0         7,690 -40,0 z" />
      <path d="M   0,-620 a 120,120 0 0 1 0,240
                          a 120,120 0 0 1 0,-240 z
               M   0,-560 a  60,60  0 0 0 0,120
                          a  60,60  0 0 0 0,-120 z" />
    </g>
    <g id="face1">
      <use xlink:href="#mark1" transform="rotate(06)" />
      <use xlink:href="#mark1" transform="rotate(12)" />
      <use xlink:href="#mark1" transform="rotate(18)" />
      <use xlink:href="#mark1" transform="rotate(24)" />
    </g>
    <g id="face2">
      <use xlink:href="#face1" />
      <use xlink:href="#face1" transform="rotate(30)" />
      <use xlink:href="#face1" transform="rotate(60)" />
      <use xlink:href="#mark3" />
      <use xlink:href="#mark2" transform="rotate(30)" />
      <use xlink:href="#mark2" transform="rotate(60)" />
    </g>
    <g id="face">
      <use xlink:href="#face2" />
      <use xlink:href="#face2" transform="rotate(90)"  />
      <use xlink:href="#face2" transform="rotate(180)" />
      <use xlink:href="#face2" transform="rotate(270)" />
    </g>
  </defs>
  <circle class="bg" r="1024" />
  <use xlink:href="#face"  class="fc" />
  <use xlink:href="#handh" class="h1" transform="rotate(304.5)" />
  <use xlink:href="#handm" class="h1" transform="rotate(54)" />
  <use xlink:href="#hands" class="h2" transform="rotate(12)" />
  <!-- hands at 10:09:02 -->
</svg>

Well either you are really good with regular expressions or you also might use the nice Mithril from HTML generator by Arthur Clemens, which directly outputs this:

m("svg", {"xmlns":"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink","viewBox":"-1024 -1024 2048 2048","width":"600","height":"600"},
  [
    m("title", 
      "Swiss Railway Clock"
    ),
    m("style", {"type":"text/css"}, 
      " .bg {stroke: none; fill: white;} .fc {stroke: none; fill: black;} .h1 {stroke: none; fill: black;} .h2 {stroke: none; fill: #aa0000;} "
    ),
    m("defs",
      [
        m("path", {"id":"mark1","d":"M -20,-1000 l 40,0 0,100 -40,0 z"}),
        m("path", {"id":"mark2","d":"M -40,-1000 l 80,0 0,240 -80,0 z"}),
        m("path", {"id":"mark3","d":"M -40,-1000 l 80,0 0,300 -80,0 z"}),
        m("path", {"id":"handh","d":"M -50,-600  l 50,-50 50,50 0,800  -100,0 z"}),
        m("path", {"id":"handm","d":"M -40,-900  l 40,-40 40,40 0,1180 -80,0  z"}),
        m("g", {"id":"hands"},
          [
            m("path", {"d":"M -10,-910 l  10,-10 10,10 2,300 -24,0 z\n               M -13,-390 l  26,0         7,690 -40,0 z"}),
            m("path", {"d":"M   0,-620 a 120,120 0 0 1 0,240\n                          a 120,120 0 0 1 0,-240 z\n               M   0,-560 a  60,60  0 0 0 0,120\n                          a  60,60  0 0 0 0,-120 z"})
          ]
        ),
        m("g", {"id":"face1"},
          [
            m("use", {"xlink:href":"#mark1","transform":"rotate(06)"}),
            m("use", {"xlink:href":"#mark1","transform":"rotate(12)"}),
            m("use", {"xlink:href":"#mark1","transform":"rotate(18)"}),
            m("use", {"xlink:href":"#mark1","transform":"rotate(24)"})
          ]
        ),
        m("g", {"id":"face2"},
          [
            m("use", {"xlink:href":"#face1"}),
            m("use", {"xlink:href":"#face1","transform":"rotate(30)"}),
            m("use", {"xlink:href":"#face1","transform":"rotate(60)"}),
            m("use", {"xlink:href":"#mark3"}),
            m("use", {"xlink:href":"#mark2","transform":"rotate(30)"}),
            m("use", {"xlink:href":"#mark2","transform":"rotate(60)"})
          ]
        ),
        m("g", {"id":"face"},
          [
            m("use", {"xlink:href":"#face2"}),
            m("use", {"xlink:href":"#face2","transform":"rotate(90)"}),
            m("use", {"xlink:href":"#face2","transform":"rotate(180)"}),
            m("use", {"xlink:href":"#face2","transform":"rotate(270)"})
          ]
        )
      ]
    ),
    m("circle", {"class":"bg","r":"1024"}),
    m("use", {"class":"fc","xlink:href":"#face"}),
    m("use", {"class":"h1","xlink:href":"#handh","transform":"rotate(304.5)"}),
    m("use", {"class":"h1","xlink:href":"#handm","transform":"rotate(54)"}),
    m("use", {"class":"h2","xlink:href":"#hands","transform":"rotate(12)"})
  ]
)

Now we can use a less fancy regex to convert to tagl format (which is not necessary, but I prefer it in terms of readability).

/m\("([^"]*)",/$1(

which gives us

svg( {"xmlns":"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink","viewBox":"-1024 -1024 2048 2048","width":"600","height":"600"},
  [
    title( 
      "Swiss Railway Clock"
    ),
    style( {"type":"text/css"}, 
      " .bg {stroke: none; fill: white;} .fc {stroke: none; fill: black;} .h1 {stroke: none; fill: black;} .h2 {stroke: none; fill: #aa0000;} "
    ),
    defs(
      [
        path( {"id":"mark1","d":"M -20,-1000 l 40,0 0,100 -40,0 z"}),
        path( {"id":"mark2","d":"M -40,-1000 l 80,0 0,240 -80,0 z"}),
        path( {"id":"mark3","d":"M -40,-1000 l 80,0 0,300 -80,0 z"}),
        path( {"id":"handh","d":"M -50,-600  l 50,-50 50,50 0,800  -100,0 z"}),
        path( {"id":"handm","d":"M -40,-900  l 40,-40 40,40 0,1180 -80,0  z"}),
        g( {"id":"hands"},
          [
            path( {"d":"M -10,-910 l  10,-10 10,10 2,300 -24,0 z\n               M -13,-390 l  26,0         7,690 -40,0 z"}),
            path( {"d":"M   0,-620 a 120,120 0 0 1 0,240\n                          a 120,120 0 0 1 0,-240 z\n               M   0,-560 a  60,60  0 0 0 0,120\n                          a  60,60  0 0 0 0,-120 z"})
          ]
        ),
        g( {"id":"face1"},
          [
            use( {"xlink:href":"#mark1","transform":"rotate(06)"}),
            use( {"xlink:href":"#mark1","transform":"rotate(12)"}),
            use( {"xlink:href":"#mark1","transform":"rotate(18)"}),
            use( {"xlink:href":"#mark1","transform":"rotate(24)"})
          ]
        ),
        g( {"id":"face2"},
          [
            use( {"xlink:href":"#face1"}),
            use( {"xlink:href":"#face1","transform":"rotate(30)"}),
            use( {"xlink:href":"#face1","transform":"rotate(60)"}),
            use( {"xlink:href":"#mark3"}),
            use( {"xlink:href":"#mark2","transform":"rotate(30)"}),
            use( {"xlink:href":"#mark2","transform":"rotate(60)"})
          ]
        ),
        g( {"id":"face"},
          [
            use( {"xlink:href":"#face2"}),
            use( {"xlink:href":"#face2","transform":"rotate(90)"}),
            use( {"xlink:href":"#face2","transform":"rotate(180)"}),
            use( {"xlink:href":"#face2","transform":"rotate(270)"})
          ]
        )
      ]
    ),
    circle( {"class":"bg","r":"1024"}),
    use( {"class":"fc","xlink:href":"#face"}),
    use( {"class":"h1","xlink:href":"#handh","transform":"rotate(304.5)"}),
    use( {"class":"h1","xlink:href":"#handm","transform":"rotate(54)"}),
    use( {"class":"h2","xlink:href":"#hands","transform":"rotate(12)"})
  ]
)

This can be copied into the snippet, where it says: ‚Write your hyperscript here‘. Save it in a file called clock.js. This code now uses some tags that are not in the default snippet, that only contains all HTML5 tags as tagls. So we need to add at the top of the file:

const { svg, circle, title, g, path, defs, use, style } = tagl(m);

Then we need to import it to our application main.js.

import clock from './clock';

And we can now use it as an own ‚tag‘ with the usual hyperscript syntax

m(clock)

after the div(), call in main.js. Next thing needed is to come up with a useful interface to the clock. Either we make it totally self-contained and it knows about the time to display on its own, or we pass in the angles of the hands. Or we let the user supply the hour from 0 to 24 and the minute from 0 to 60 and the seconds analogously. This sounds reasonable, but we could add both interfaces later on. All property key value pairs that are supplied on the JS-object that is passed as first parameter to a tagl can be used inside a component by querying its vnode.attrs. So we read in clock.js all attribute values like this

export default vnode => {
    return {
        view(vnode) {
            const {hour, minute, second} = vnode.attrs;
            return [
...

and use them to replace the predefined wikipedia angles of hour=304.5 minute=54 and second=12 by replacing the lines with:

use( {"class":"h1","xlink:href":"#handh","transform":"rotate(${hour*360/12})`)"}),
use( {"class":"h1","xlink:href":"#handm","transform":"rotate(${minute*360/60})`)"}),
use( {"class":"h2","xlink:href":"#hands","transform":"rotate(${second*360/60})`)"})

Now back in main.js we can supply the clock with the values of the current time

view: vnode => {
    const time = new Date()
    const hour = time.getHours();
    const minute = time.getMinutes();
    const second = time.getSeconds();
...
    m(clock, {hour, minute, second})

Now it looks as if the story ends here, but when you look in the browser, you might notice that the clock doesn’t move. Yes and that’s part of the supreme performance of Mithril. It knows exactly when to update the view and when not. Usually nothing will update automatically except when user events such as clicks or other subscribed events are triggered. Some other types of events that are not temporarily directly coupled to the users doings need a manual trigger, such as finished HTTP requests or a clock that wants to be updated. We can place the following line anywhere in main.js:

setInterval(m.redraw,1000)

Now the story could be over, but of course the clock doesn’t behave very natural, because the hour hand will remain on exactly the 11th hour mark from 23:00-24:00 and then move from 330° to 0° in one instant. Using this last code in the main.js file, we are now set with the clock.

import m from 'mithril';
import tagl from 'tagl-mithril';
import clock from './clock';

const { h1, div, p } = tagl(m);

setInterval(m.redraw, 100)

m.mount(document.body, {
    view: vnode => {
        const time = new Date();
        const hour = time.getHours() + time.getMinutes() / 60;
        const minute = time.getMinutes() + time.getSeconds() / 60;
        const second = time.getSeconds() + time.getMilliseconds() / 1000;
        return div.container(
            h1('Hello Mithril'),
            p('With the help of tagl'),
            m(clock, { hour, minute, second, size: 120})
        );
    }
});

This last modification concludes the tutorial. More might follow, but why? This is it, except for:

Deployment

Now there is the second time parcel messes up with the tutorial.

parcel build src/index.html

You can also bundle it all together in one html file, as I did here (Yes, it IS the 404 page that I mean! :). Behold the final result:

Homework 1

You might have noticed the size parameter in

        m(clock, { hour, minute, second, size: 120})

Make the clock’s size configurable.

Homework 2

When a Swiss colleague of mine saw the result and I told him that his was supposed to be a Swiss station clock he directly remarked that the second hand stops at the top and waits until the minute is switched and then goes of again. It turns out that the second hand is driven by an electrical motor. So another function is needed to transform the time coordinates into displayed angles. The result is displayed here. Note the pause at the top. Actually my colleague pointed out even more „design flaws“ such as the pointy end of the second hand which more looks like the Hilfiker imitation used on German stations.

Disclaimer

Not all of the above are best Mithril practices. E.g. the calculation of the clock angles in the view function is not regarded optimal.

2 Kommentare

Schreibe einen Kommentar zu iceman Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert