Mithril.js: A Tutorial Introduction (Part 1)

This is part 1 of a tutorial series. You can view part 2 here.

Mithril.js is a small (~8kb) and fast frontend JavaScript framework. It's a simpler, yet more capable version of React, replacing the need for libraries like jQuery. Mithril's small size and API makes it ideal for embedded JavaScript widgets, or user interfaces that have high performance requirements.

My personal favorite part about Mithril is that it's just JavaScript. If you know how JavaScript-the-language works, you can fully apply that knowledge when using Mithril, including any of JavaScript's functional programming features and techniques :)

Tutorial Overview

In this tutorial we are going to walk through building a real-world case study, slightly modified to focus on building the core pieces of a Mithril component. Specifically we will cover:

  • What a Mithril component is made of
  • How to do client-side routing
  • The concept of view-model and view state, and their differences
  • Two-way data binding
  • Showing a modified view based on a conditional
  • Creating new model data
  • Pointing out how everything is plain JavaScript!

Here's the full demo of what we'll be building.

This tutorial only covers the front-end; there will be no server-side code snippets.

Project User Stories

The sample app we will be building is intended to be used by an organizer who is manually signing up other volunteers for an event.

  • As a volunteer, I want to sign up for an event.
  • As a volunteer, I want to sign up multiple people at once.
  • As an organizer, I need at least one attendee email address to send event details to.
  • As an organizer, I want to charge $10 per attendee.
  • As an organizer, I want to provide coupons to offer a percent discount on total price.
  • As an organizer, I want to see all entered volunteer entries.

Getting Started

If you're coding along, use these commands to get started:

$ git clone https://github.com/gilbert/blog-post-examples
$ cd blog-post-examples
$ git checkout mithril-part-1

Then open index.html in your browser.

Keep in mind that the purpose of this project is to teach Mithril concepts, so it uses some non-best practices. In a real project you would minimize the use of DOM ids, concatenate and minify your JavaScript files, and so on.

Mithril Components

Conceptually, a component is a well-defined, self-contained part of a user interface. A Mithril component is a Plain Old JavaScript Object (POJO) with a view property.

For example, here is a component that returns an <h1> tag:

{ view: () => m('h1', 'Hello World') }

That's it. What you see is a full Mithril component. There's much more, but this is all you need to get started.

Mithril components make it very easy to write modular code. In this project we will be writing several:

  1. An EntryList component, which will display all current volunteer entries,
  2. An EntryForm component, which will host the form to enter a new volunteer entry, consisting of three child components:
  3. A Contacts component, which will manage the volunteer's contact information,
  4. A Total component, which will calculate the final price, and
  5. A Coupon component, which will handle checking the validity of an entered coupon via AJAX.

In this project, each component will live in a .js file under src/components/. If you cloned the GitHub repo, you will find a skeleton file for each one.

Strategy

In part 1 of this series we will build the EntryList component and part of the EntryForm component. To incrementally introduce Mithril concepts, we will take the following approach:

  1. Build the Model
  2. Build the full EntryList component
  3. Begin the EntryForm View State
  4. Begin the EntryForm View
  5. Adding EntryForm Actions
  6. Complete the EntryForm View
  7. Complete the EntryForm Component

1. The Entry Model

Let's begin with the model. You may have noticed that we haven't mentioned the model at all up to this point. That's because Mithril imposes nothing special on your model; you can use whatever design pattern you like.

For this project, the primary model we will be working with is Entry. Here is an example entry:

{
  "enteredAt": 1443018758199,
  "volunteers": [
    { name: "Alice", email: "alice@example.com" },
    { name: "Bob", email: "bob@example.com" }
  ]
}

To store this data, we use a simple array. You will find this code written in Entry.js:

// models/Entry.js
var store = []
var idCounter = 1

Entry.all = function () {
  return store
}

Entry.create = function (attrs) {
  attrs.id = (idCounter += 1)
  attrs.enteredAt = Date.now()
  store.push(attrs)
}

If you take a look at index.html, you will see the code that seeds the model with data for us to play with. In practice this data would come from the server.

Later on we will extend our Entry.js model file to add support for creating new entry view-models (more on that later).

2. The EntryList Component

Remember that a Mithril component is a POJO with a view function. Take a look at the EntryList component:

// src/components/EntryList.js
window.EntryList = {
  view() {
    return m('h2', "TODO: REPLACE ME!")
  }
}

Let's fill this in. We want to show a list of entries, where each one shows enteredAt date, as well as each volunteer belonging to that entry. For fun, we will do this with two helper functions:

// src/components/EntryList.js
window.EntryList = {
  view() {
    return m('.entry-list', [
      m('h1', "All Entries"),
      Entry.all().map( entryView )
    ])
  }
}

function entryView (entry) {
  // TODO
}
function volunteerView (volunteer) {
  // TODO
}

The m() helper creates virtual DOM elements for us (follow that link and skim over the "DOM Elements" section right now). Here are some important takeaways:

  • The first argument to m() is the tag of the element you are creating. For example, m('h1') creates an h1 element. If there is no tag, a div is created by default.
  • Within the first argument to m() you can use css-style syntax to add classes and HTML attributes. For example, m('a.btn[href=/]') creates an anchor tag with a class btn and an href attribute pointing to /.
  • Mithril supports arrays of children, so your Entry.all().map( entryView ) code works great.

In the previous code snippet, the first thing we do is create a wrapper div with a class entry-list (note: every component needs a single top-level element). Within that div, we add an h1 tag, and then wishfully map each entry from the Entry model to a sub-view. Using helper functions in this manner can be good style when using map.

I say wishfully here because entryView doesn't quite exist yet – that's coming up next.

Next let's fill in the entryView helper:

function entryView (entry) {
  var date = new Date(entry.enteredAt)

  return m('.entry', [
    m('label', "Entered at: ", date.toString()),
    m('ul', entry.volunteers.map( volunteerView ))
  ])
}

The nice thing about a helper function like this is you can focus on a single item – in this case, a single entry. Because we also need to display an entry's volunteers, we use map with another helper function (volunteerView), which will also help keep our level of nesting flat.

Finally, let's fill in the volunteerView helper:

function volunteerView (volunteer) {
  return m('li.volunteer', [
    m('label', volunteer.name),
    m('label', "(" + volunteer.email + ")")
  ])
}

This is the most basic so far. It displays a volunteer's name and email address within a single li element.

And that's it! If you refresh the page, you should see two entries, with two and three volunteers respectively.

Client-side Routing

The last thing we need to do to complete EntryList is to provide a link that sends the user to the entry form page. Before we do that, let's study the routes defined in index.html:

m.route(document.getElementById('app'), '/', {
  '/': EntryList,
  '/entries/new': EntryForm,
  '/inline-example': {
    view() {
      return m('a[href=/]', { oncreate: m.route.link }, 'Works the same!')
    }
  }
})

This is how you define routes in Mithril. Specifically:

  • The first argument to m.route is the DOM element you want to mount your app to,
  • the second argument is the default route, and
  • the third argument is a JS object, mapping URLs to components.

Remember that a Mithril component is a plain JS object with a view property. In the above routes, even though we pass in EntryList to our root route, we also have the option of inlining a component, like the /inline-example route. If you visit localhost:8000/#!/inline-example, you will see the page on that route. Clicking the link on that page will take you back to the root route.

Back to our task. We need to add a link to EntryList that takes the organizer to the entry form page, i.e. /entries/new. Modify EntryList.view to look like the following:
*

// src/components/EntryList.js
window.EntryList = {
  view() {
    return m('.entry-list', [
      m('h1', "All Entries"),

      // New!
      m('a[href=/entries/new]', { oncreate: m.route.link }, "Add New Entry"),

      Entry.all().map( entryView )
    ])
  }
}

Here we use an anchor tag with an href that points to /entries/new. We then hook it into Mithril's routing system with oncreate: m.route.link.

And that's all there is to routing! Try refreshing the page and clicking the link. After clicking, your URL bar should change to /#!/entries/new. You can also use your browser's back button to navigate to the previous page.

3. The EntryForm View State

So far we have created a stateless component. Now let's work on the EntryForm component, which is the component that will allow the organizer to enter new volunteer entries.

Unlike the EntryView component, our EntryForm component will need to manage view state. View state is "temporary" data related to the component's current visual state. In our case, the text that the user will be typing in – volunteer names and volunteer emails – is our view state.

In this project, our component manages its own view state using a view-model. A view-model is kind of like a "temporary model". We do not consider it "real" model data because it's data that has not been persisted. For example, the organizer might enter some volunteers, but later change their mind and reset the form. However, once the organizer hits "submit", the view-model then becomes "real" model data, and is stored in a database somewhere.

Let's start by building our view model. First let's extend the Entry model with a new view model initializer:

// src/models/Entry.js
Entry.vm = function () {
  return {
    enteredAt: null,
    volunteers: [ Entry.volunteerVM() ]
  }
}

Entry.volunteerVM = function () {
  return {
    name: '[Your name]',
    email: '[Your email]'
  }
}

Here we separate our view-model into two parts: Entry.vm and Entry.volunteerVM. This is because later we will be adding and removing volunteers within a single entry; it will prove useful to have Entry.volunteerVM as a separate function. We also initialize Entry.vm with a volunteer to have something to show on the page.

Next let's analyze the EntryForm component:

window.EntryForm = function () {
  // TODO: State goes here

  return {
    view() {
      return m('.entry-form', [
        m('h1', "New Entry"),
        // TODO
      ])
    }
  }
}

This style of Mithril component is called a closure component. It's a simple yet full-featured solution for writing a component with view state.

Instead of EntryForm being a POJO, EntryForm is now a function that returns a POJO. This allows us to initialize variables of any kind to keep track of our view state, and use them in our view.

Ok! Let's use our new view-model initializer in our closure component. Replace the first // TODO comment with the following:

var entry = Entry.vm()

It may seem strange, but we're actually done with our view state – for now, at least. We will come back to add more later.

4. The EntryForm View

Now that we have our state and view-model set up, we can update our view to render it. Here is everything at once (with the outer parts truncated):

// src/components/EntryForm.js
// ...
view() {
  return m('.entry-form', [
    m('h1', "New Entry"),
    m('h3', "Please enter each volunteer's contact information:"),

    entry.volunteers.map(function(volunteer, idx) {
      return m('fieldset', [
        m('legend', "Volunteer #" + (idx+1)),
        m('label', "Name:"),
        m('input[type=text]', { value: volunteer.name }),
        m('br'),
        m('label', "Email:"),
        m('input[type=text]', { value: volunteer.email }),
      ])
    })
  ])
}
// ...

Note how we're still using plain old JavaScript features. This is a big benefit of using Mithril — no preprocessors or build step needed. We can also refactor this code later (if necessary) using standard JavaScript and software design principles.

Here is the resulting HTML:

<div class="entry-form">
  <fieldset>
    <legend>Volunteer #1</legend>
    <label>Name:</label>
    <input type="text">
    <br>
    <label>Email:</label>
    <input type="text">
  </fieldset>
</div>

If you refresh your browser, you will see the inputs pre-filled with the values we initialized in Entry.vm. Easy! :)

5. Adding EntryForm Actions

Now it's time to add some user interactions. According to our user stories, the user needs to be able to add multiple attendees in the same form. Since this logic is decoupled from the view, it is quite straightforward:

window.EntryForm = function () {
  var entry = Entry.vm()

  function add () {
    entry.volunteers.push( Entry.volunteerVM() )
  }
  function remove (idx) {
    entry.volunteers.splice(idx, 1)
  }
  // return { ...[truncated]... }
}

The add and remove functions are called actions. They are simple functions that update component state. Next we will bind these actions to the view.

6. Completing the EntryForm View

We have two component actions to bind. Let's start with add, since it's easy — we just need a button that looks like this:

m('button', { onclick: add }, 'Add another volunteer')

The onclick: add is the part where we bind the add action to the button. Now, when the user clicks this button, the add function will run.

Insert this button code into the view like so:

// ...
view() {
  return m('.entry-form', [
    m('h1', "New Entry"),
    m('h3', "Please enter each volunteer's contact information:"),
    entry.volunteers.map(function (volunteer, idx) {
      // ...[truncated]...
    }),
    // New!
    m('button', { onclick: add }, 'Add another volunteer'),
  ]);
}

That's it! If you're coding along, refresh the page and click the new button. You should see a new volunteer fieldset appear.

Removing Attendees

The requirements for remove are trickier. We want users to be able to remove extraneous attendees, but not be able to remove all of them. In other words, the page needs to have at least one fieldset at all times.

To solve this, we will only show a remove button for an attendee if more than one exists. Fortunately, our views are Just JavaScript™, so this shouldn't be a problem.

To keep our view code clean, let's create a new variable within the view for our logic:

// ...
view() {
  var showRemove = (entry.volunteers.length >= 2)
  return m('.entry-form', /*...[truncated]...*/)
}

showRemove will be true if there are two or more attendees, and false if there is only one. This will help us prevent the user from removing the last volunteer.

Now we can elegantly add a remove button to our view:

// ...
view() {
  var showRemove = (entry.volunteers.length >= 2)
  return m('.entry-form', [
    m('h1', "Entry Form"),
    m('h3', "Please enter each volunteer's contact information:"),

    entry.volunteers.map(function(volunteer, idx) {
      return m('fieldset', [
        m('legend', "Volunteer #" + (idx+1)),
        m('label', "Name:"),
        m('input[type=text]', { value: volunteer.name }),
        m('br'),
        m('label', "Email:"),
        m('input[type=text]', { value: volunteer.email }),
        // === Here it is! === \\
        showRemove &&
        m('button', { onclick() { remove(idx) } }, 'remove')
      ])
    }),

    m('button', { onclick: add }, 'Add another volunteer')
  ])
}

Beautiful! When showRemove is true, the && will continue and render the button. Otherwise, the && will short-circuit to false and show nothing (Mithril does not render falsey values).

We're done!

Two-Way Data Binding

...well, almost done.

Try this: fill in the first fieldset, and then add another volunteer. Oh no, our data got lost! This happens because we only implemented one-way data binding — from component state to view. We still need to implement the other direction — from view to state.

To do so, we need to respond to the onchange event. As it turns out, it is not that difficult:

m('label', "Name:"),
m('input[type=text]', {
  value: volunteer.name,
  onchange(e) { // <-- new!
    volunteer.name = e.currentTarget.value
  }
}),

And that's all it takes. Notice how there is no magic going on here – we're only doing a simple assignment to a plain value. This is something I really appreciate about Mithril.

The reason the above code works is because Mithril will redraw after running a browser event callback. It all goes something like this:

  1. value: volunteer.name – Assign volunteer name to input box
  2. onchange() { ... } – Bind a callback to the onchange event
  3. Initially, Mithril renders the page; it takes our view and uses it to efficiently construct the DOM.
  4. Later, the user types something into the input box, triggering the onchange event
  5. The callback bound to onchange runs, setting volunteer.name = to what the user typed in.
  6. After this callback is done running, Mithril redraws – it repeats steps 1-3, updating all views automatically!

If you're coding along, take the pattern we just wrote for the name input, and update the email input to match.

Finishing Up

Lastly, let's complete the view by adding a submit button to the very end, right under the "Add another volunteer" button:

// Don't forget your commas!
m('button', { onclick: add }, 'Add another volunteer'),
m('br'), // <-- New!
m('button', { onclick: submit }, 'Submit') // <-- New!

Once again, we've done a bit of wishful programming, as submit does not yet exist; that is coming up next.

7. Completing the EntryForm Component

Now for the final step: submitting the form. As suggested by the previous step, we need to add a submit function to our component. Here's what it looks like:

function submit () {
  Entry.create(entry)
  m.route.set('/')
}

Wow, only two lines! Here we are taking all data the organizer has typed in (entry), and passing it to the model to turn it into "real" model data! Put another way, we're persisting our view-model data. Afterwards, we use m.route to redirect the organizer to the EntryList page.

If you're coding along, try it out now!

That's it folks, we're done. You can view the full gist here, or run git checkout mithril-part-1-solution in the git repository.

Conclusion

In this post we covered:

  • What a Mithril component is (plain JS object with a view property)
  • How to do client-side routing with m.route
  • The concept of view-model and view state
  • Two-way data binding
  • Showing a modified view based on a conditional
  • Committing new model data within a component action
  • How almost everything is plain JavaScript!

Because Mithril uses functional JavaScript constructs, we can refactor our view in ways that are framework-agnostic, allowing us to construct the architecture that's right for our application. Mithril is a wonderful, lightweight framework that embraces JavaScript-the-language instead of trying to abstract over it. For this and many other reasons, Mithril has persisted as my frontend framework of choice for the past five years.

In part 2, we will implement both the Total and Coupon components. Until next time!

Further Reading

Gilbert

Gilbert

https://github.com/mindeavor

Instructor, developer, writer.

View Comments
Navigation