Mithril and Meteor

Some people have asked me how one could go about integrating Mithril with Meteor.

For those who don't know, Meteor is a NodeJS framework that allows developers to write code that can be reused in the server and in the client. Some of its other selling points include a live reload feature, and a unified API for dealing with data persistence.

In this article, we'll look at replacing Meteor's built-in templating engine, Blaze w/ Mithril's, and in the process, we'll hopefully demistify some of the magic in Meteor.

To install Meteor, follow these instructions. If you're on Windows, there's an unofficial installer here. Once you've installed it, let's start a new project. Open up your terminal and type:

meteor create leaderboard
cd leaderboard
meteor run

The first line creates a blank application in a folder called leaderboard. The last line starts the Meteor's built-in server at the URL http://localhost:3000

In this article, we'll build a simple version of the Leaderboard app. Leaderboard is the "hello world" of Meteor. It shows a list of users and their respective scores, and it allows us to select a user by clicking on their name, and then give them points by hitting a button at the bottom. An example implementation can be found here.

An important thing to notice that is not obvious at first is that this app updates in real time. To see what I mean, open the app in two different windows side-by-side, and give some points to one of the players. You'll see the score for that player update on both pages, without needing to refresh either page. This gives us an important clue about how Meteor works, and we'll explore that in a bit.

But first things first. Let's create a collection to store data. Meteor uses MongoDB as its database system and in Meteor/MongoDB lingo, a collection is roughly equivalent to a table from SQL databases. Some major differences between MongoDB and SQL databases, however, are that MongoDB collections don't necessarily need to have a schema and that items in the collection can have deeply nested fields (think a JSON document).

Anyways. Fire up your browser and go to http://localhost:3000, and type these commands in your developer tools console.

var PlayerList = new Meteor.Collection("players")
PlayerList.insert({name: "John", score: 0})
PlayerList.insert({name: "Bob", score: 0})
PlayerList.insert({name: "Mary", score: 0})

Hopefully, those commands should be self-explanatory: we created a collection called players and populated it with 3 entries. What might not be so obvious, though, is that these entries have been saved to the database on our server. Obviously, in a real application, being able to arbitrarily write to the database from the browser command-line is a big no-no, and Meteor does let us lock down this ability.

We can also query the collection to ensure our data is really there. Type this in the browser's console:

PlayerList.find().fetch()

You should see our three players, and each should have an _id field generated by the database, in addition to the name and score fields that we defined.


Now that we have some test data to work with, let's start building our application. First delete the leaderboard.html, leaderboard.css and leaderboard.js files. These initial files just contain a simple hello world app, which is not relevant to this article.

Put a copy of the mithril.js file in this folder, and create a new blank file called leaderboard.js.

The first thing we'll do is create a helper function to integrate Mithril to Meteor. As you saw in the working example application, Meteor updates data on screen in real time. This is possible thanks to web sockets, a technology that allows us to push data from the server to the client without clients initiating AJAX requests. Clients can then listen to reactive data sources, which are at their core, event handlers that fire whenever new data is available from a web socket data push.

The simplest way to integrate to Meteor's reactive data sources is to use its Deps micro-library. Specifically, Deps.autorun is exactly what we need: it runs an arbitrary function any time there's new data, and returns a handle to an object that allows us to stop calling this arbitrary function (for example, if a route changes, we don't want to keep firing things related to the old page).

Let's see what this looks like in code. First we'll implement a controller transformer. Much like template transformers, a controller transformer simply takes a controller function as an argument and returns another controller function. We'll be using this technique as a trick to emulate class inheritance for dynamically-scoped controllers (recall that Javascript does not natively support class-based OOP).

var reactive = function(controller) {
    return function() {
        var instance = {}

        controller.call(instance)

        return instance
    }
}

The code above calls one controller's constructor function with a blank object as it's target, which makes the this keyword inside controller point to the instance object. This effectively copies any members of controller onto the instance object, so in the end, this instance has the same public members as if we called new controller. We can now freely augment the instance object, without necessarily knowing anything about the code inside the controller function.

So let's go ahead and hook up Deps.autorun:

var reactive = function(controller) {
    return function() {
        var instance = {}

        var computation = Deps.autorun(function() {
            m.startComputation()
            controller.call(instance)
            m.endComputation()
        })

        instance.onunload = function() {
            computation.stop()
        }

        return instance
    }
}

Notice that we wrapped controller.call(instance) in a m.startComputation / m.endComputation context. This is important. What's happening here is that calling Deps.autorun registers a new callback which will run whenever new data is available. Since this callback runs asynchronously, then any time it fires, we need to tell Mithril that things are ready to be redrawn.

As we saw earlier, calling controller.call(instance) copies data over to our instance. So, calling it repeatedly every time new data is available essentially makes instance update all its public data, and this updated data is then consumed by the view when Mithril's redrawing system kicks in.

The other thing to notice is that we defined an onunload handler. This is a Mithril handler that fires for modules when a route changes. While this handler will not be used for this article's example, it's important to be aware that subscriptions to reactive data sources have a lifecycle that needs to be managed. As I mentioned earlier, Deps.autorun returns a handle that can be used to stop firing the callback. So if a route changes in a Mithril app, onunload fires, which in turn, calls computation.stop(). Once that is called, new data from the server will no longer trigger the callback that contains our controller.call(instance) call. If we didn't define this onunload handler, our callback would keep firing forever even after the controller is no longer being used by the application due to a route change.

Note that I'm forcefully assigning a callback to onunload here for the sake of keeping the code easy to understand, but in real life, this code would need to ensure that we don't clobber previously defined onunload handlers.


Ok, now that we have a bit of appreciation for what the magic in Meteor is all about, we can finally start building the application.

First, let's make the players collection available. Put this in the leaderboard.js file:

//model

//Meteor collection of players
var PlayerList = new Meteor.Collection("players")

Next, let's create a View Model entity which will hold functions to express the things that we want to be able to do with our players data (i.e. get a list of players, select a player, ask whether a given player is selected, and give points to a selected player).

//Leaderboard view model
var Leaderboard = {}

//selected player
Leaderboard.selectedPlayerID = null

//application actions
Leaderboard.players = function() {
    return PlayerList.find().fetch()
}
Leaderboard.select = function(player) {
    Leaderboard.selectedPlayerID = player._id
}
Leaderboard.selected = function(player) {
    return Leaderboard.selectedPlayerID == player._id
}
Leaderboard.givePoints = function() {
    if (Leaderboard.selectedPlayerID) {
        PlayerList.update({_id: Leaderboard.selectedPlayerID}, {$inc: {score: 5}})
    }
}

Notice that Leaderboard.players uses the same code for retrieving a list of players, as we saw earlier.

The Leaderboard.givePoints function calls PlayerList.update. The first argument is a "where" clause, and the second argument is the action to perform. So this code updates a player whose _id matches the selected player's _id, and increments their score by 5. $inc is one of the many modifiers that can be used to define actions to be performed on Meteor collections. A complete list of modifiers can be found on the MongoDB docs.


Next, let's create our controller:

//controller
Leaderboard.controller = reactive(function() {
    this.players = Leaderboard.players()
})

All we're doing here is specifying the scope of the data which we're going to be work with. In our case, we're just grabbing the whole list of players.

Notice that all the state management code is in the model layer, and that our controller does almost nothing. This is a good example of the lean controller philosophy that I talked about in another article.

The key thing to notice in the snippet above is that we finally used the reactive helper function we created earlier. If you recall, reactive makes a controller constructor re-run every time new data is available from the server. So in our case, we call Leaderboard.players() every time new data arrives. It then calls PlayerList.find().fetch(). And here is where we can see the beauty of Meteor in action: PlayerList.find() does not actually make another round-trip request to the server at this point. Remember that this entire function is only running because Meteor signaled via Deps.autorun that new data is available. What it also did under the hood is sync said data to its internal client-side cache, so calling PlayerList.find within a Deps.autorun context is as cheap as looking for the data in a Javascript object. And because the data is already there, .fetch() can return the data synchronously.

If you understand the benefits of Mithril templates being declarative re-runnable entities, you should see how the Meteor data flow system fits like a glove: our module can now always run from top to bottom, and the state of the application is completely decoupled and always synced everywhere. This makes it extremely easy to reason about application state.


Now that our reactive controller is setup, let's create a simple template to actually see things on screen:

//view
Leaderboard.view = function(ctrl) {
    return [
        m("ul", [
            ctrl.players.map(function(player) {
                return m("li", {
                    style: {background: Leaderboard.selected(player) ? "yellow" : ""},
                    onclick: Leaderboard.select.bind(this, player)
                }, player.name + ": " + player.score)
            })
        ]),
        m("button", {onclick: Leaderboard.givePoints}, "Give 5 points")
    ]
}

Nothing fancy there, just a list of players and their scores. Clicking on a player selects and highlights them, and clicking on the button on the bottom gives points to the selected player.

We just need a last snippet to initialize our app as a Mithril module:

//render the app
if (Meteor.isClient) {
    Meteor.startup(function() {
        m.module(document, Leaderboard)
    })
}

And we're done!


Here's the full code:

//meteor helper
var reactive = function(controller) {
    return function() {
        var instance = {}

        var computation = Deps.autorun(function() {
            m.startComputation()
            controller.call(instance)
            m.endComputation()
        })

        instance.onunload = function() {
            computation.stop()
        }

        return instance
    }
}

//model

//Meteor collection of players
var PlayerList = new Meteor.Collection("players")

//Leaderboard view model
//Leaderboard view model
var Leaderboard = {}

//selected player
Leaderboard.selectedPlayerID = null

//application actions
Leaderboard.players = function() {
    return PlayerList.find().fetch()
}
Leaderboard.select = function(player) {
    Leaderboard.selectedPlayerID = player._id
}
Leaderboard.selected = function(player) {
    return Leaderboard.selectedPlayerID == player._id
}
Leaderboard.givePoints = function() {
    if (Leaderboard.selectedPlayerID) {
        PlayerList.update({_id: Leaderboard.selectedPlayerID}, {$inc: {score: 5}})
    }
}

//controller
Leaderboard.controller = reactive(function() {
    this.players = Leaderboard.players()
})

//view
Leaderboard.view = function(ctrl) {
    return [
        m("ul", [
            ctrl.players.map(function(player) {
                return m("li", {
                    style: {background: Leaderboard.selected(player) ? "yellow" : ""},
                    onclick: Leaderboard.select.bind(this, player)
                }, player.name + ": " + player.score)
            })
        ]),
        m("button", {onclick: Leaderboard.givePoints}, "Give 5 points")
    ]
}

//render the app
if (Meteor.isClient) {
    Meteor.startup(function() {
        m.module(document.body, Leaderboard)
    })
}

Conclusion

The reactive helper changes the semantics of controllers in such a way that forces state to be in the model layer, and allows us to think of controllers as declarative entities, much like we already do with Mithril templates. In addition, the automatic data syncing provided by Meteor greatly simplifies the complexity of application state itself: if you are using a Meteor collection, then you can be sure the data on the database is what you see on screen (and vice versa).

Conversely, Mithril brings the power and the performance of its templating engine to the world of Meteor.

There's obviously a lot more to Meteor than what I covered here, and I encourage you to try implementing one of the other example apps if Meteor looks interesting to you.

Also, if anyone is using Mithril and Meteor together, please post below. I'd love to hear about it!


comments powered by Disqus

Latest Articles







Flattr