Why you should use MithrilJS

Jason Mavandi

mvndaai.com

What is Mithril?

My, and soon your, favorite JavaScript Framework.

Quick Framework Comparion

What  the comparison shows

  • Loading the framework from a CDN
  • A GET request
  • Repeating over the response
  • No compiling
  • Inline html

AngularJS 

Version 1.6.5

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.5/angular.min.js"></script>

<div ng-app='ang' ng-controller="angController">
    <div>Angular</div>
    <div>{{name}}</div>
    <div ng-repeat="e in r.data">
        {{e.year.split('-')[0]}} : {{e.number_of_homeless}}
    </div>
</div>

<script>
    var app = angular.module('ang', []);
    app.controller('angController', function($scope, $http) {
        $http.get(utahHomeless).then((r) => $scope.r = r);
    });
</script>

Issues with Angular

  • Two places to look at: HTML and JavaScript
  • Angular specific component: 'ng-repeat'
  • Magic strings
  • Templated HTML appears of there are errors
    • '{{name}}'
  • Errors are hard to find

ReactJS

Version 15.6.1

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react-dom.min.js"></script>

<div id="react"></div>

<script>
    fetch(utahHomeless).then((r) => r.json()).then((r) =>
        ReactDOM.render(Component(r), document.getElementById("react"))
    );

    var Component = (r) => React.createElement('div', null,
        React.createElement('h1', null, 'React'),
        r.map((e) =>
            React.createElement('div', null,
                `${e.year.split('-')[0]} : ${e.number_of_homeless}`
            )
        )
    );
</script>

React looks a little better

HONEST NOTE: I haven't used React much, that was probably a horrible example

MithrilJS

Version 1.1.3

<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/1.1.3/mithril.min.js"></script>

<div id="mithril"></div>

<script>
    var Component = {
        r : [],
        oninit: (vnode) => {
            m.request(utahHomeless).then((r) => vnode.state.r = r);
        },
        view: (vnode) => [
            m('h1', 'Mithril'),
            vnode.state.r.map(
                (e) => m('', `${e.year.split('-')[0]} : ${e.number_of_homeless}`)
            )
        ]
    };
    m.mount(document.getElementById('mithril'), Component);
</script>

Improvements with Mirthril

  • Errors are shown directly in the browser console
  • Lifecycle method 'oninit' tells when the request should be made
  • The component is reusable
  • That JavaScript it how is written by developers

What Mirthril Says

It is small and performant

Demo

Setup

Follow along here or clone my  repo to see everything

https://github.com/mvndaai/presentation-mithril

Web Server

Download Web Server for Chrome

 

This is just a simple web server. Tell it a folder on your computer, it does the rest.

index.html

<!DOCTYPE html>
<html>
    <head>
        <title>MithrilJS Demo</title>
        <script src="//unpkg.com/mithril/mithril.js"></script>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous"/>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.4.3/css/bulma.min.css" integrity="sha256-VC9bpAao257nf22leBRLU7AqKGwS9/Ylz8hfpHmOob4=" crossorigin="anonymous" />
        <script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
        <script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script>
        <script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
    </head>
    <body></body>
    <script type="module"> import './app.js';</script>
</html>

What are all those imports?

  • Bulma
    • A small CSS Framework built with flex boxes (like Bootstrap, but mobile first)
  • Font Awesome
    • An icon system to give the page some flare
  • Traceur
    • ES6 compiler (like Babel), mostly for 'import'/'export' but also makes ES6 work on IE...

app.js

m.render(document.body, 'Hello World');

Check your page and you should have a "Hello World"

First Component

A reusable piece

HTML in Mithril

  • Mithril uses hyperscript syntax
    • vnode = m(selector, attributes, children)
    • attributes and children are optional
    • a selector of an empty string creates a div
  • Mithril can you JSX, but you must compile it with Babel
  • If you need some help converting your HTML try this

A Simple Button

var button = m('button', 'I am a button!');

m.render(document.body, button);

You should now have a button, but what is the point if it doesn't do anything?

Counter Component

// app.js
var Counter = {
    view: vnode => m('button', 'Count')
};

m.mount(document.body, Counter);
  • A component is just an object with an attribute 'view' that contains a vnode function
  • Make sure to change 'render' to 'mount' since this component will update the page

Finish Counter Component

// app.js
var Counter = {
    count: 0,
    view: vnode => [
        m('button', {
            onclick: _ => vnode.state.count++
        }, 'Count'),
        vnode.state.count
    ]
};

m.mount(document.body, Counter);
  • The state of a component is contained in 'vnode.state'
  • We have an array to hold our two components one is just the count value
  • An 'onclick' attribute makes this work

Move component into file

// components/counter.js
export default {
    count: 0,
    view: vnode => [
        m('button', {
            onclick: _ => vnode.state.count++
        }, 'Count'),
        vnode.state.count
    ]
};
// app.js
import Counter from './components/counter.js';
m.mount(document.body, Counter);

Note: This is why we added Traceur

Let's reuse it

// app.js
import Counter from './components/counter.js';
m.mount(document.body, {
    view: _ => [
        m(Counter),
        m(Counter)
    ]
});
  • Mount takes a component so surround your array with an object with view
  • Views only hold vnodes so wrap your 'counter' in an 'm()'
  • Each counter will count on its own

Surround component in div

// component/counter.js
export default {
    count: 0,
    view: vnode => m('', [
        m('button', {
            onclick: _ => vnode.state.count++
        }, 'Count'),
        vnode.state.count
    ])
};

I don't like our components lined up, so lets wrap each in a div.

Take note of the m('div', [...]) 

Giving each a name

// component/counter.js
export default {
    count: 0,
    view: vnode => m('', [
        m('button', {
            onclick: _ => vnode.state.count++
        }, vnode.attrs.name || 'Count'),
        vnode.state.count
    ])
};

You can access attributes through 'vnode.attrs'

// app.js
import Counter from './components/counter.js';
m.mount(document.body, {
    view: _ => [
        m(Counter, {name:'Counter 1'}),
        m(Counter, {name:'Counter 2'})
    ]
});

Routing

Single Page Application

Just call m.route

// app.js
import Counter from './components/counter.js';

m.route(document.body, "/home", {
    "/home": {view: _ => "Home" },
    "/counter": Counter
});

We now have two routes

  • /home - Just has a string of 'Home'
  • /counter - contains our counter

Requests

// components/loader.js
export default {
    view: _ => m('.has-text-centered', [
        m('i.fa.fa-spinner.fa-pulse.fa-5x[aria-hidden="true"]', {
            style: {fontSize: '5rem'},
        }),
        m('span.sr-only', 'Loading...')
    ])
};

Loading Animation

We do not want a blank screen while waiting for a request to come back

Mithril lets you add classes or IDs by using CSS selectors. The '.' means class, and a '#' would be an ID.  Other attributes can be added between '[' and ']'. Anything 'fa' is font awesome and the other classes are are from bulma.

// components/request.js
import Loader from './loader.js';
export default {
    oninit: vnode => m.request('https://odn.data.socrata.com/resource/j8a6-qa8k.json')
        .then(r => vnode.state.r = r),
    view: vnode => m('.container', vnode.state.r ?
        vnode.state.r.map(e => m('', JSON.stringify(e)) ) : m(Loader)
    )
};

Request Component

  • There is a ternary operator to choose the response or the loader component
  • Using 'map' to create a 'div' for each response line
  • The lifecycle method 'oninit' makes the request when the component is created before it added to the DOM. If you need the DOM use 'oncreate'

Add A Request Route

// app.js
import Counter from './components/counter.js';
import Request from './components/request.js';

m.route(document.body, "/home", {
    "/home": {view: _ => "Home" },
    "/counter": Counter,
    '/request': Request,
});

Make It Presentable

Tips Though Example 

// frame.js
var Header = {
    view: _ => m('', 'This will be a header')
};

export default {
    view: vnode => m('section.hero.is-fullheight.is-light', [
        m('.hero-head.is-info', m(Header)),
        m('.hero-body', vnode.children),
        m('.hero-foot.has-text-centered',
            m('a[href="http://mvndaai.com"][target="_blank"]', 'mvndaai.com')
        )
    ])
};

Header and Footer

  • Bulma hero gives us an easy header and footer.
  • Other components come through 'vnode.children'
// app.js
import Frame from './components/frame.js';
import Counter from './components/counter.js';
import Request from './components/request.js';

m.route(document.body, "/home", {
    "/home": {view: _ => m(Frame, "Home")},
    "/counter": {view: _ => m(Frame, m(Counter))},
    "/request": {view: _ => m(Frame, m(Request))},
});

Wrapping Routes

Wrap our routes in our new header/footer frame

// frame.js
var Header = {
    view: _ => m('nav.navbar', [
        m('.navbar-brand', [
            m('a.navbar-item[href=/home]', {oncreate: m.route.link}, 'MithrilJS Demo'),
            m('a.navbar-item[href=/counter]', {oncreate: m.route.link}, 'Counter'),
            m('a.navbar-item[href=/request]', {oncreate: m.route.link}, 'Request'),
        ])
    ])
};

...

Navigation Bar

Update our Header component to have naviation

The  'oncreate: m.route.link' lets the href point to a route rather than a URL.

// frame.js
var Header = {
    oninit: vnode => vnode.state.route = m.route.get(),
    view: vnode => m('nav.navbar', [
        m('.navbar-brand', [
            m('a.navbar-item[href=/home]', { oncreate: m.route.link,
                class: vnode.state.route === '/home' ? 'is-active' : '',
            }, 'MithrilJS Demo'),
            m('a.navbar-item[href=/counter]', { oncreate: m.route.link,
                class: vnode.state.route === '/counter' ? 'is-active' : '',
            }, 'Counter'),
            m('a.navbar-item[href=/request]', { oncreate: m.route.link,
                class: vnode.state.route === '/request' ? 'is-active' : '',
            }, 'Request'),
        ])
    ])
};
...

Adding Classes By Route

We want the class 'is-active' only when it is the current route

In a Bulma navbar-item, 'is-active' only highlights on smaller screens

Takeaways

Small

That is the entirety of the API!

 

You can read and understand the entire thing in a day

 

8kb

Completely JavaScript

Errors in the browser console!

 

You can use a linter!

 

No having to go back and forth with an HTML page!

Everything needed built in

Routing for single page applications

 

Requests with promises that auto redraw when completed

MIT License

No worries about being sued

Questions?

Comments?

Were you converted?

Made with Slides.com