How to Build Your First Meteor App and Discover Your Inner Artist


I recently gave a demo to the Boston Meteor Meetup group of something amusing I made with Meteor (the framework we use for the web app version of Skimbox). The title of the meetup was "Build Your First Meteor App," and Meteor Multidraw, which took about  two and a half hours start to finish, fits that beginner-friendly bill.

Effectively, Meteor Multidraw is a simple real time collaborative drawing application. Everyone shares the same drawing state, and any new drawing will automatically propagate to all other users currently viewing the page. It's just Meteor and d3 -- the latter actually handles drawing points. The source is up on github

The application itself is pretty simple, but it shows a bunch of Meteor's features all at once. Since it's really meant to be more instructive than an example of best practices, some of the things done are more for educational effect than for any sort of practical reason. Here's how it works, starting with the d3 piece:

 

Part 1: Create Your Canvas

// A simple object to handle creating an SVG canvas,
// clearing the screen, and actually drawing our points.
function Canvas() {
var self = this;
var svg;

// Creates the SVG canvas.
var createSvg = function() {
svg = d3.select('#canvas').append('svg')
.attr('width', '100%')
.attr('height', '100%');
};
createSvg();

// Clears the SVG canvas.
self.clear = function() {
d3.select('svg').remove();
createSvg();
};

// Naively draws an array of simple point objects.
self.draw = function(data) {
if (data.length < 1) {
self.clear();
return;
}
if (svg) {
// This is what actually does the drawing. We're not
// going to cover d3 in any great detail here.
svg.selectAll('circle').data(data, function(d) { return d._id; })
.enter().append('circle')
.attr('r', 10)
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; });
}
};
}

All that we're doing here is creating a Canvas object which can render an SVG canvas and clear itself. Additionally it naively handles drawing points as small circles, given an array of them. Pretty simple. What we're going to do then with Meteor is propagate around these arrays of points so that they can be drawn for everyone. There are two major components to any Meteor app - a server, and a client. The server is generally responsible for maintaining the absolute state of the data, in our case, a collection of points. Let's take a look at it in:

 

Part 2: Serve It Up

// This is our actual points collection. It's stored in an 
//internal mongo database.
points = new Meteor.Collection('pointsCollection');

// This function will publish a named subscription of points
// that the client can then subscribe to. It returns all points
// currently in the points collection.
Meteor.publish('pointsSubscription', function () {
return points.find();
});

// This declares a server side method that the client can
// remotely call, which simply removes all of the points
// from the database. This is necessary because only the server
// may remove multiple documents at once.
Meteor.methods({
'clear': function () {
points.remove({});
}
});

Not super complicated. It declares a points collection, blindly publishes a cursor of all the points, and provides a method for removing all of them. Let's look at the client:

 

Part 3: The Client

// Declaring our points collection on the client side.
points = new Meteor.Collection('pointsCollection');

// Just a reference for our canvas.
var canvas;

// Creates a reactive context around us subscribing to our
// points collection. See the discussion about reactivity
// below.
Deps.autorun( function () {
Meteor.subscribe('pointsSubscription');
});

// Runs when Meteor is all set to start. It creates our
// canvas out of the Canvas object we declared above and..
Meteor.startup( function() {
canvas = new Canvas();

// Creates a reactive context around us getting all points
// out of our points collection. Fetch will turn the cursor
// into an array. We then pass off this array to the canvas'
// draw method to actually draw all the points.
// (Not performant!)
Deps.autorun( function() {
var data = points.find({}).fetch();
$('h2').hide();
if (canvas) {
canvas.draw(data);
}
});
});

// Totally unnecessary, but used to illustrate how
// template helpers work. By calling in {{title}}
// in our template (see below), we write out
// the title of our app.
Template.drawingSurface.title = function () {
return 'Draw with Me!';
}

// Declares an events hash of all events scoped to
// a particular template. Here we're handling the click
// event on the clear button.
Template.drawingSurface.events({
'click input': function (event) {
Meteor.call('clear', function() {
canvas.clear();
});
}
})

// Just some DRY for inserting a point into the points
// collection based on the cursor position.
var markPoint = function() {
var offset = $('#canvas').offset();
points.insert({
x: (event.pageX - offset.left),
y: (event.pageY - offset.top)});
}

// Another events hash. This one handles capturing the
// drawing-related events. Just for reference, Session can
// also establish a reactive context, but we're not using that
// here.
Template.canvas.events({
'click': function (event) {
markPoint();
},
'mousedown': function (event) {
Session.set('draw', true);
},
'mouseup': function (event) {
Session.set('draw', false);
},
'mousemove': function (event) {
if (Session.get('draw')) {
markPoint();
}
}
});

This is probably the most complicated piece. It handles all of the client side interactions. It's also where we're taking the most advantage of reactivity, the heart of Meteor. By utilizing reactivity, changes to our underlying data will rerun functions that depend on our data. Above, the points.find({}).fetch(is some of our underlying data. Our subscription declares that we're subscribing to data from the server, in this case, all of our points. Whenever the server's data changes, it passes those changes onto the client. When the client's data changes, our collection is automatically updated. The call to points.find({}).fetch(is in a reactive context, and when the client side collection changes the function is rerun, which naively causes all of the points to be drawn onto the canvas, including the new ones, thus updating the overall drawing. When the user themselves draws new points, these are inserted into the collection and then synced back to the server, which adds them to the server side collection, thus triggering all of the above for all other clients. This is what allows us to maintain our drawing state across everyone! Now, last piece, swear, the template:

Part 4: The Template

<head>
<title>meteor-multidraw-demo</title>
</head>

<body>
{{> drawingSurface}}
</body>

<template name="drawingSurface">
<h1>{{title}}</h1>
<input class='clearButton' type='button' value='Clear'/>
<h2>Loading...</h2>
{{> canvas}}
</template>

<template name="canvas">
<div id='canvas'></div>
</template>

Not much going on. The {{> drawingSurface }} just says render the drawingSurface template here. You can see where we stick in the title as mentioned above. Lastly, we can nest templates too, as seen with the canvas template. Note that the template names here exactly match those used in the client side code above.

And that's pretty much it. You can fork the repository and mess with the code yourself from https://github.com/nwmartin/meteor-multidraw-demo, which also includes a set of directions for running it locally. If you want to mess with it, try making it so folks can draw with different colors or select a different "brush" radius. Most importantly, have fun with it. If you have any questions you can shoot me an email at nickm@ripariandata.com or just ask in the comments. Good luck!

Related posts

 

 

 

1 Comment