Of Sound Mind and Love

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 (7kb) and fast, classical MVC JavaScript framework. It encourages an architecture similar to Angular.js, and uses a virtual DOM like React.js, all while avoiding the need for libraries like jQuery. Mithril's small size and API makes it ideal for embedded JavaScript widgets and user interfaces that have high performance requirements.

My personal favorite part about Mithril is that it's just JavaScript. In other words, if you know how JavaScript-the-language works, you can fully apply that knowledge when using Mithril. Oh, and that also includes 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 comprised of
  • How to do client-side routing
  • The concepts of a view-model and view-state
  • Two-way data binding
  • Showing a modified view based on a conditional
  • Creating new model data
  • And how almost everything is plain JavaScript!

Here's the demo!

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/mindeavor/blog-post-examples
$ cd blog-post-examples
$ git checkout mithril-part-1

To make routing work properly, you need to view your index.html file through a server. The easiest way to do this is to cd into the project folder and run python -m SimpleHTTPServer. This will spin up a static asset server at localhost:8000.

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, wrap your code in anonymous function scopes, and so on.

Mithril Components

Conceptually, a component is a well-defined, self-contained part of a user interface. A Mithril component is implemented in two parts: a controller and a view.

We will see the details of each emerge as the series goes on. In the meantime, here's a brief overview of the two:

  • The controller holds up to two main responsibilies: providing controller actions for the view, and managing component view-state. In Mithril, the controller is a plain JS constructor function.
  • The view is what the user sees; it constructs itself based on view-state and model data, and it binds user events to controller actions. In Mithril, the view is a function that returns a plain JS object that represents a virtual DOM element.

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

  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, which will have three child components:
    1. A Contacts component, which will manage the volunteer's contact information,
    2. A Total component, which will calculate the final price, and
    3. 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. Inspect the Model
  2. Build the full EntryList component
  3. Begin the EntryForm Controller
  4. Begin the EntryForm View
  5. Update the EntryForm Controller
  6. Complete the EntryForm View
  7. Complete the EntryForm Controller

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 view-models when creating new entries (more on that later).

2. The EntryList Component

As mentioned, a Mithril component is composed of a controller and a view. Let's take a look at the EntryList component:

// src/components/EntryList.js
window.EntryList = {}

EntryList.view = function (ctrl) {}  

Wait a second, where's the controller? As it turns out, in Mithril the controller is optional. Some components – such as the one we are about to write – do not need to contain their own view state. Formally, these are known as stateless components.

Alright, let's write the view. We want to show a list of entries, where each list item shows enteredAt date, as well as all the volunteer contacts part of that entry. We will keep our code readable by writing two helper functions:

// src/components/EntryList.js
EntryList.view = function () {  
  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 "Usage" 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 infinitely nested arrays, so your Entry.all().map( entryView ) works perfectly fine.

In the previous code snippet, the first thing we do is create a wrapper div with class entry-list (every component needs a top-level element). For that div's children, we take all entries from the Entry model, and map each one to a view. Using helper functions in this manner is considered good style when using map.

We have done a little wishful programming here, since entryView doesn't quite exist yet – that's coming up next.

Next let's fill in entryView:

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 also helps keep our level of nesting flat.

Finally, let's fill in volunteerView:

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

This is the most basic view. It displays a volunteer's name and email within an 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,

  '/example': {
    controller: function () {
      var ctrl = this
      ctrl.data = [10,20,30]
    },
    view: function (ctrl) {
      return m('.example-page', [
        m('a[href=/]', { config: m.route }, "< Back"),
        m('select', ctrl.data.map(function(n) {
          return m('option', { value: n }, "Number: " + n)
        }))
      ])
    }
  }
})

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 two properties: controller and view. In the above routes, even though we pass in EntryList to our root route, we also have the option of "hardcoding" a component, like the /example route. If you visit localhost:8000/?/example, you will see the page on that route. Clicking the < Back 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
EntryList.view = function () {  
  return m('.entry-list', [
    m('h1', "All Entries"),

    // new!
    m('a[href=/entries/new]', { config: m.route }, "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 config: m.route.

And that's all there is to it! Try refreshing the page and clicking the link. After clicking, your URL bar should change to /?/entries/new.

3. The EntryForm Controller

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 view's current state. In our case, the data that the organizer will be typing in – volunteer names, volunteer emails, etc. – is our view state.

In this project, our controller will store and manage the aforementioned view state in a view-model. A view-model is kind of like a "temporary model". We do not consider it "real" model data, because it is data that may or may not be permanent. For example, the organizer might enter some volunteers, but later change their mind and reset the form. However, if the organizer hits "submit" instead, the view-model then becomes "real" model data, and is stored in a database somewhere.

Let's start building and using our view model. First let's extend the Entry model to house our view model makers to make them available to the controller:

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

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

And for context, here is how we will store it in our controller.

// src/components/EntryForm.js
EntryForm.controller = function () {  
  var ctrl = this
  ctrl.entry = Entry.vm()
}

Notice how we separated our view model into two parts: Entry.vm and Entry.volunteerVM. This is because 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 for the component to show on a new form.

It may seem strange, but we're actually done with our controller – at least, for now. We will come back to finish it up later.

4. The EntryForm View

Now that we have our controller and view-model set up, we can set up a view to present it. Here is everything at once:

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

    ctrl.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 }),
      ])
    })
  ])
}

Mithril automatically passes an instance of a component's controller into the view (more on that later), so we have access to that instance via the ctrl parameter.

Note how we're 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 techniques.

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 the view model. Simple! :)

5. Updating The EntryForm Controller

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

EntryForm.controller = function () {  
  var ctrl = this
  ctrl.entry = Entry.vm()

  ctrl.add = function () {
    ctrl.entry.volunteers.push( Entry.volunteerVM() )
  }
  ctrl.remove = function (idx) {
    ctrl.entry.volunteers.splice(idx, 1)
  }
}

The add and remove functions are called controller actions. We can now bind these actions to the view.

6. Completing the EntryForm View

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

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

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

Insert this into the view like so:

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

And that's it! If you're coding along, refresh the page and try clicking the new anchor in your browser.

Removing Attendees

The requirements for remove are trickier. We want users to be able to remove extraneous attendees, but not 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 link to remove an attendee if more than one exist. Fortunately our views are Just JavaScript™, so this shouldn't be a problem :)

To keep our view code clean, let's create a helper method. Add this code after your EntryForm.view function:

function removeAnchor (ctrl, idx) {  
  if (ctrl.entry.volunteers.length >= 2) {
    return m('button', { onclick: ctrl.remove.papp(idx) }, 'remove')
  }
}

This helper will only return a "remove" anchor link if there are two or more attendees.

Now we can [quite elegantly] add this new link to our view:

EntryForm.view = function (ctrl) {  
  return m('.entry-form', [
    m('h1', "Entry Form"),
    m('h3', "Please enter each volunteer's contact information:"),

    ctrl.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! === \\
        removeAnchor(ctrl, idx) // <--------
      ])
    }),

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

And 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 model to view. We still need to implement the other direction — from view to model.

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: function (e) { // <-- new!
    volunteer.name = e.currentTarget.value
  }
}),

That's all it takes! Notice how there is no magic going on here – something I really appreciate about Mithril.

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

  1. value: volunteer.name – Show volunteer name in input box
  2. onchange: function ... – Bind a callback to the onchange event
  3. Mithril draws the page. That is, 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 is run, consequently setting volunteer.name to what the user typed in.
  6. After the callback is done rendering, Mithril redraws – it repeats steps 1-3, updating all views automatically!

Finishing Up

If you're coding along, look at what we've written for the name input and update the email input to match. 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: ctrl.add }, 'Add another volunteer'),  
m('br'),  
m('button', { onclick: ctrl.submit }, 'Submit')  

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

7. Completing the EntryForm Controller

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

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

Wow, only two lines! Here we are taking what the organizer has typed in (ctrl.entry), and passing it to the model to turn it into "real" model data! Another way to put it is we convert our view-model to a persistent model. Afterwards, we use m.route to redirect the organizer to the EntryList page. Try it if you're coding along.

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

One more thing...

You also may have noticed the use of papp in the removeAnchor helper:

m('button', { onclick: ctrl.remove.papp(idx) }, 'remove')  

papp stands for partial application — a powerful functional programming technique. It allows us to mold generic functions to become more specific to our needs. In this case, papp will return a new copy of ctrl.remove with the first parameter bound to the value of idx. We then bind this new function as the button's onclick handler. As a result, when the user clicks the button, ctrl.remove will run with that specific idx as its first parameter.

You can find an implementation of papp and other useful function functions (yes, function functions) in this gist.

Conclusion

In this post we covered:

  • What a Mithril component is (plain JS object with controller and view properties)
  • How to do client-side routing with m.route
  • The concepts of a view-model and view-state
  • Two-way data binding
  • Showing a modified view based on a conditional
  • Creating new model data in a controller action
  • And how almost everything is plain JavaScript!

Because Mithril uses functional JavaScript constructs, we can refactor our views and controllers in ways that are framework-agnostic, allowing us to construct the right architecture for our application. Mithril is a wonderful, lightweight framework that doesn't try to change JavaScript-the-language. For this and many other reasons, it is now my frontend framework of choice.

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