Creating a Single Page App in Meteor

Meteor is a name I’ve seen around for a while, often surrounded by talk of how different and innovative it is. I’ve been meaning to have a proper play with it since I first heard about it in a write up of the Throne of JS conference from 2012. With version 1.0 being released in October last year, the first version with an official Windows installer (1.1) following in March, and the announcement this month that version 1.2 will (almost) fully support ECMAScript 6 out of the box, I decided that I was out of excuses and needed to install it and have a look. Whether or not it lives up to some of the hype floating around the web is probably too subjective a question to answer, but it was certainly a really interesting and different experience to write an app with Meteor.

I created a simple single page stock manager app because that’s the sort of rock ‘n’ roll lifestyle I lead. You can see all the code at GitHub.

Meteor Stocks full screen

So What Is It?

Meteor is an open source reactive JavaScript framework for building web apps. But the word ‘framework’ seems like underselling; it’s certainly a different beast than front end frameworks like Angular and Knockout. It’s really an ecosystem (or atmosphere) of related technologies that allow you to work seamlessly between the client and server to create fast, modern apps. Meteor is built on Node to create a complete end-to-end web framework that lets you run the same code on client and server and tightly incorporates MongoDB to give database access out of the box. The core packages that come with every new project set-up contain a lot of powerful tools for getting up and running, on both client and server, with minimal boiler plate.

If you have the time, I recommend working through the getting started tutorial to get a decent idea of what Meteor is about.

Starting Up

To create Meteor Stocks, I’ll use the create command:

meteor create meteor-stocks

This gives us a basic project template as well as a load of standard packages and all the important gubbins to be able to run the app at http://localhost:3000:

cd meteor-stocks
meteor

One thing lacking from the main tutorial is any discussion of how to organise your code to keep it neat and maintainable. However, there is a good section in the docs, which is worth a read, and an even better section on the Unofficial Meteor FAQ. One important take-away is that anything you put in a folder called ‘server’ will only go to the server, anything in a folder called ‘client’ will only go to the client, and everything else will go to both.

As an aside, the fact that most code gets given to both client and server by default is really the crux of what makes Meteor so interesting. If you write a web app using a traditional stack, such as .Net or Java and Spring at the back end and JavaScript (perhaps with Knockout or Angular) at the front end, it is almost impossible to avoid some duplication of model objects between your client and server code. You may have, say, a person object in your data layer that you convert into a person viewmodel object to send to the client via JSON that you then convert into a nearly identical JavaScript person object. Meteor avoids all of this by creating a truly unified client/server environment where a person can just be represented by one single object throughout.

Anyway, based on the recommended layouts, I’ll create the following structure:

/.meteor/
/client/
- meteor-stocks.html
- meteor-stocks.css
- meteor-stocks.js
/lib/
/server/

where lib is for any code common to client and server. I won’t actually use meteor-stocks.js so I’ll delete that and create more modular and specialised JS files as I go along.

Out of the box, Meteor contains some functionality to manipulate and query your database directly from the client. This is great for quick prototyping, and for showing off at nerdy parties, but as the tutorial points out, it would be a dangerous thing to keep in production, so I’ll remove these packages early to avoid temptation:

meteor remove insecure
meteor remove autopublish

User Accounts

The idea of this app is to let users build up and save their own list of stocks to keep an eye on, and for that to work users will need to be able to set up accounts. Luckily, this is blindingly easy using the method described in the getting started tutorial.

Adding Stocks

Now that a user can sign-up and log-in, they need to be able to add stocks. So first we need to create a collection. I’ll create lib/collections.js and add:

Stocks = new Mongo.Collection("stocks");

This creates a new collection on the server backed by an integrated MongoDB database. It tells the client to create a cached version of the server collection so that it can do clever things like predict the outcomes of server calls without having to wait for a response, meaning the UI can respond to input extremely quickly. This collection is also reactive, so changes to the collection will automatically be reflected in templates that use it.

Because I removed the autopublish package, I have to manually set up publishing and subscribing for this collection, so I’ll create server/publish.js:

Meteor.publish("stocks", function () {
  return Stocks.find();
});

and client/subscribe.js:

Meteor.subscribe("stocks");

Then I can create a client function to query the collection via that subscription and make it available in the <body> section of my view. I’ll create client/body.js and add:

Template.body.helpers({
  stocks: function () {
    return Stocks.find({owner: Meteor.userId()}, {sort: ["symbol", "asc"]});
  }
});

and then in meteor-stocks.html within my <body> tags:

<ul>
  {{#each stocks}}
    {{> stock}}
  {{/each}}
</ul>

This uses the Spacebars templating engine and means that each stock returned by the stocks function will have its contents output to a template called ‘stock’. So I’ll add that template by creating client/stock.html:

<template name="stock">
  <li class="stockListing”>
    <span class="listSymbol">{{symbol}}</span>
    <span class="numSelected">({{count}})</span>
    <div class="options">
      <span class="toggle-favourite"></span>
      <span class="delete" title="Delete">x</span>
    </div>
  </li>
</template>

That’s great but we don’t have any entries in the database yet, so there’s not much to show. Let’s create a way to add stocks in the UI. In meteor-stocks.html:

<form class="add-stock">
  <input type="text" name="symbol" placeholder="Search for a symbol..." />
</form>

and in body.js:

Template.body.events({
  "submit .add-stock": function(event) {
    event.preventDefault();

    var input = event.target.symbol;
    Meteor.call("addStock", input.value);
    input.value = "";
  }
});

If I hadn’t removed the insecure package earlier I could have inserted a record straight into the database from this event listener, but because I’m trying to be responsible, there’s one more step.

Notice the line Meteor.call("addStock", input.value) - that’s calling a Meteor method, addStock(). Both client and server can respond to meteor methods: if there’s one common method, it will be run on the server while the client will attempt to predict the outcome so that any changes can be reflected on the UI immediately. Any discrepancies get sorted out once the server responds. Alternatively, you can specify what you want each to do by writing separate methods with the same name in client and server-specific files.

I’ll start off doing it the first way. So I’ll create lib/methods.js and add:

Meteor.methods({
  addStock: function(symbol){
    if(!Meteor.userId()){
      throw new Meteor.Error("not-authorized");
    }
    symbol = symbol.toUpperCase();

    // Don't allow duplicates
    if(Stocks.findOne({symbol: symbol, owner: Meteor.userId()})){
      return;
    }

    Stocks.insert({
        symbol: symbol,
        owner: Meteor.userId()
    });
  }
});

The call to Stocks.insert() will be run on both the actual stocks collection in the database and the client’s cached version so adding a new entry should be reflected instantly on the UI. If you open up two browsers side by side and add a stock, the new entry should appear on both screens quickly, but slightly more quickly on the screen where you actually added it.

Add new stock

This is all very nice, but we’re not really adding stocks, we’re just adding whatever someone wants to type into the input. I’ll use Yahoo Finance to provide stock information because it’s free and has a simple API. I could write some code to handle calls to the API but a quick Google shows it already exists. Meteor and Atmosphere are fairly mature, despite still being on quite low version numbers, and there seems to be a good vibrant community and a lot of existing packages. That’s good because I’m allergic to doing any more work than I have to. So I’ll add the Yahoo Finance package:

meteor add ajbarry:yahoo-finance

The description shows that this is a server-only interface, that means it can only be called from server-based code. First I’ll create server/methods.js and add:

Meteor.methods({
  getName: function(symbol){
    var data = YahooFinance.snapshot({
      symbols: [symbol],
      fields: ['n']
    });
    return data[symbol].name;
  }
});

This method will take a financial symbol and return its name if it exists or the string “N/A” if it doesn’t. So I’ll update my addStock method to only add stocks that are actually stocks:

if(Meteor.isServer){
  Meteor.call("getName", symbol, function(error, result){
    if(result !== "N/A"){
      Stocks.insert({
        symbol: symbol,
        name: result,
        owner: Meteor.userId()
      });
    }
  });
}

This means we’ve lost the benefit of the client-side collection stubbing I described earlier because now we’re only doing the insert on the server. To get it back, we could add something like:

if(Meteor.isClient){
  Stocks.insert({
    symbol: symbol,
    owner: Meteor.userId()
  });
}

Now the stock would be added to the cached collection, and therefore the list in the UI, before the response from the server telling us whether it exists. In some cases that would be a good route to take, but here it probably isn’t. There are a lot more strings that aren’t financial symbols than ones that are, so it wouldn’t be a good idea to predict that the insert will succeed. If we entered a random string, it would flash up in the list and then disappear again when the server comes back to tell us it doesn’t exist.

To add a bit more functionality to the stock list, I’ll let the user delete stocks and make favourites. I already added the necessary html in the stock template, so I’ll add client/stock.js:

Template.stock.events({
  "click .toggle-favourite": function() {
    Meteor.call("setFavourite", this._id, !this.favourite);
  },

  "click .delete": function() {
    Meteor.call("removeStock", this._id);
  }
});

and to the Meteor.methods object in lib/methods.js I’ll add:

removeStock: function(stockId){
  Stocks.remove(stockId);
},
setFavourite: function(stockId, favourite){
  Stocks.update(stockId, {$set: {favourite: favourite}});
}

I’ll also add a count of how many users are watching each stock so you can see if you’re watching popular stocks or if you’re out there on the wild west of the financial universe. In stock.js:

Template.stock.helpers({
  count: function() {
    return Stocks.find({symbol: this.symbol}).count();
  }
});

Now if you get all your friends and family to sign up and add stocks too (or create some new users in another browser window; whichever takes less awkward conversation) you should see the count numbers go up and down to reflect what other users are doing.

Selecting Stocks

Now that we’ve got a list that we can add to, we want to be able to select from that list and view some actual data. To do this I’ll set up a reactive variable that I can update with the details of the selected stock so that anything that depends on it will automatically update whenever those details change. For this I’ll add the reactive-vars package:

meteor add reactive-vars

and then add one in my collections.js file:

SelectedStock = new ReactiveVar(null);

Now I’ll add a method to server/methods.js to get all the data for a given stock for the last year:

getData: function(symbol){
  var end = new Date();
  var start = new Date(end);
  start.setDate(start.getDate() - 365);

  return YahooFinance.historical({
    symbol: symbol,
    from: start,
    to: end
  });
}

and then add another listener to the events object in stock.js:

"click .stockListing": function() {
  var name = this.name;
  var symbol = this.symbol;
  Meteor.call("getData", this.symbol, function(error, result){
    if(result){
      var currentPrice = result[result.length-1].close.toFixed(2);
      SelectedStock.set({
        name: name,
        symbol: symbol,
        data: result,
        price: currentPrice
      });
    }
  });
}

This listens for clicks on any of the stock listing <li> elements and calls my server-side getData() method then updates the SelectedStock reactive var with an object literal containing the results. All that’s left to do is create a template to show the contents of SelectedStock. So in meteor-stocks.html I’ll add:

{{#with selectedStock}}
  {{> stockDetail}}
{{/with}}

and then I’ll create client/stockDetail.html:

<template name="stockDetail">
  <h2>{{name}} <span class="detail-symbol">({{symbol}})</span></h2>
  <div class="current-price">{{price}}</div>
</template>

Now clicking a stock in the list will give us more information: the stock’s name and its most recent close price.

It would be nice to show a bit more than that, so I’ll use Highstock to output the data in a chart. Another quick Google shows there’s a handy Meteor wrapper around Highstock already so I’ll add that:

meteor add jhuenges:highstock

I’ll stick a div for my chart in stockDetail.html:

<div id="container-area"></div>

and then create client/stockDetail.js to deal with the chart set-up:

function createChart() {
  var stock = SelectedStock.get();

  $('#container-area').highcharts('StockChart', {
    series: [{
      type: 'line',
      name: 'Close',
      data: stock.data.map(function(item){
        return {
          x: item.date,
          y: item.close
        };
      }),
      tooltip: {
        valueDecimals: 2
      }
    }]
  });
};

Template.stockDetail.rendered = function() {
  Tracker.autorun(function () {
    createChart();
  });
};

The Tracker.autorun() method automatically tracks any reactive data sources that it detects as dependencies. That means that because createChart() calls SelectedStock.get(), it will be run any time SelectedStock changes, and the chart will be recreated with new data.

Session Variables

Meteor also gives easy access to session variables, so I’ll try that out by giving the user an option to customise what they see. I added the ability to set stocks as favourites earlier, so now I’ll let users filter their stock list to show only their favourites.

First I’ll add something to click, so in meteor-stocks.html:

<div class="fav-filter">
  Filter favourites
</div>

and a corresponding event listener in the events object in body.js:

"click .fav-filter": function() {
  var oldValue = Session.get("filterFavourites") || false;
  Session.set("filterFavourites", !oldValue);
}

Session.set() takes a string as a key and then whatever arbitrary value you want to set it to; in this case a boolean. Like the collections and the reactive var, Session is reactive, so templates and other objects can respond to changes automatically. Now I’ll just update the stocks function in my body helpers to reflect this filter state:

stocks: function () {
  var filter = Session.get("filterFavourites") || false;

  var find = filter ? {owner: Meteor.userId(), favourite: filter} :
    {owner: Meteor.userId()};

  return Stocks.find(find, {sort: ["symbol", "asc"]});
},

Now we can update the filter session variable by clicking in the UI and immediately see that change reflected in the stock list.

In the finished app, I used the same method to add a button to toggle the chart between a line series and an OHLC chart.

Final Thoughts

Meteor seems like a fresh way to work. It makes a lot of things, like CRUD operations and keeping your UI in line with data changes, extremely easy and it adds a lot of nice, clever tricks, like the client-side stubbing of server-side methods.

There are a few things that struck me as awkward, like the reliance on passing strings to call Meteor Methods, which makes me worry that I’m always only one ham-fisted typo away from a nasty and confusing bug that an IDE won’t help me with. I’m also a little uneasy about how global everything feels: there’s no need to import specific modules, as you would with Require, everything’s just sort of there. That makes development very quick and easy, but I wonder what it would mean for maintainability on a larger project.

Still it’s very impressive how much you can achieve with relatively few lines of code and Meteor gluing everything together behind the scenes, so I think this is definitely a framework to try.

blog comments powered by Disqus