You are on page 1of 23

1

Table of Contents
1. Introduction.................................................................................................................... 3
What is Meteor Streams.......................................................................................................... 3
What's in this eBook................................................................................................................. 3
Source Code................................................................................................................................. 3

2. Creating the App............................................................................................................ 5


Adding Meteor Streams........................................................................................................... 5
With Atmosphere.................................................................................................................................... 5
Using Git..................................................................................................................................................... 5
Adding Meteor Router............................................................................................................. 6
Adding hammer.js..................................................................................................................... 6

3. Basic UI and Routes...................................................................................................... 7


Let's add the UI........................................................................................................................... 7
Adding Routes............................................................................................................................ 9

4. Let's build the Blackboard....................................................................................... 10


5. Designing the App for Meteor Streams................................................................ 12
Blackboard app and Stream events.................................................................................. 12
Permissions.............................................................................................................................. 13

6. Adding Meteor Streams Support........................................................................... 14


Integrating Blackboard (Pad)............................................................................................. 14

7. Adding Fine-Grade Permissions............................................................................ 17


Why we need permissions................................................................................................... 17
Let's do the modifications.................................................................................................... 17

Thank You.......................................................................................................................... 19

1. Introduction
I hope you have tried the Realtime Blackboard application that I've created. If not,
please watch this video and try it yourself. As the name implies, it is a realtime
blackboard where multiple users can draw and share on a common blackboard.

This project is based on Alan Shaws Meteor Blackboard. He used a Meteor


Collection to add realtime behavior to his project. That means that every pixel you
draw on the client needs to go through mongodb before it reaches other connected
clients. It works, but it is not the best approach for this kind of problem.
For my Realtime Blackboard app, I used a different approach to add the realtime
capabilities. Instead of going through mongodb, I choose Meteor Streams to add
realtime communication to the app.

What is Meteor Streams


Meteor Streams is a project created to add messaging-based communication to
Meteor. Once you've created a Stream, clients can pass messages back and forth.
Those messages are not routed through mongodb. Meteor Streams has a permission3

based security model, and through filters, you can control what clients are
communicating.
To follow this eBook, you don't need prior experience with Meteor Streams. But try
having a look at Meteor Stream documentation before you proceed.

What's in this eBook


With this eBook, I will guide you in building the Realtime Blackboard app using
Meteor Streams. I will assume you have some experience with Meteor already. If not,
read the Discover Meteor book.

Source Code
Source code for the application we are creating is available on the Github. Click
here to get it.
At the end of each chapter, you can view and download the source code with the
changes made in that chapter and previous chapters.
So why wait? Lets get started!

2. Creating the App


As usual, you need to create a meteor app and remove default packages and
unwanted files. You need to add the bootstrap package, too. Lets name our project
awesome-blackboard.

meteor create awesome-blackboard


cd awesome-blackboard
meteor remove insecure autopublish
meteor add bootstrap
rm awesome-blackboard.* #remove files added by default

Adding Meteor Streams


Now it's time to incorporate Meteor Streams into our project. Meteor Streams
distributes itself as a Smart Package, so you can very easily add it to the project.
There are two possible ways to add it:

With Atmosphere
Atmosphere is a community-managed Smart Package repository. You can install
atmosphere packages with meteorite. Once you've installed meteorite, installing
Meteor Streams is just a single shell command.

mrt add streams

Using Git
If you are not a fan of meteorite and atmosphere, you can add Meteor Streams to
your app as a git submodule.

mrt add streamsmkdir -p packages


#make sure you created the packages folder
git submodule add https://github.com/arunoda/meteor-streams.git
packages/streams

Now you've added Meteor Streams support for your app. Easy, wasnt it?

Adding Meteor Router


We will use the Router project to add basic routing capabilities to our app. To add it
to the project, I'll use meteorite to install it from atmosphere. You can also add it
using git as shown above.

mrt add router

Adding hammer.js
Our app needs to work smoothly on every possible browser, including mobile
browsers. We need some help from a third party-library. We'll use hammer.js to
support touch events. Let's add it to our project:

Download hammer.js jQuery version


Add downloaded file into client folder

You've now completed the first step of our journey!


View Source Code

Download Source Code

3. Basic UI and Routes


In our blackboard app, users can create as many blackboards as they want. (We use
the term pad internally to identify a blackboard.) Each blackboard has its own URL.
Also, if someone visits the root of the website (/), he will be redirected to a new
blackboard.
So, basically, we have 2 types of routes as shown below.
1. /
2. /:padId
padId can be any value, since we have no restrictions on it. But it normally contains a

random id generated with Random.id().

Let's add the UI


Since our focus here is on Streams, we don't need to discuss much about HTML and
CSS. It is minimal and straightforward. So let's add it to the app.

Add following html into a file named client/home.html


<head>
<title>Realtime Blackboard with Meteor Streams</title>
</head>
<body>
<!-- Where router render the content -->
{{renderPage}}
</body>

Now let's add following css into client/blackboard.css

body {
margin: 0;
overflow: hidden;
background-color: black;
}
#header {
position: fixed;
top: 0px;

left: 0px;
color: rgb(220, 220, 220);
border-bottom: 1px solid rgb(100, 100, 100);
padding: 15px 5px 5px 10px;
width: 100%;
}
#header #heading {
float: left;
width: 300px;
}
#header #controls {
float: right;
width: 600px;
text-align: right;
padding-right: 20px;
}
#header #heading h1 {
font-size: 28px;
line-height: 25px;
margin: 0px 0px 8px 0px;
float: left;
}
#header #heading h2 {
margin-bottom: 10px;
font-size: 16px;
line-height: 16px;
font-weight: normal;
}
#header #heading h2 a {
color: inherit;
text-decoration: none;
font-weight: bold;
border-bottom: 1px dashed white;
}
.nickname {
position: absolute;
font-family: 'Arial';
padding: 1px 4px 1px 4px;
font-size: 13px;
border-radius: 3px;
border: 2px solid rgb(100, 100, 100);
color: white;
background-color: black;
}

We have a single template named pad that the blackboard is rendered into. Add the
following content to client/view/pad.html

<template name='pad'>
<canvas></canvas>
<div id='header'>
<div id='heading'>
<h1>Realtime Blackboard</h1>
</div>
<div id='controls'>
<span id='show-nickname'>Hello, <b>user</b></span>
<input class='btn btn-info btn-small' type="button"
id='set-nickname' value='Change Nickname'/>
<input class='btn btn-info btn-small' type="button"
id='wipe' value='Clear Backboard'/>
<input class='btn btn-success btn-small' type="button"
id='create-new' value='Create New'/>
</div>
</div>
<script id='tmpl-nickname' type="text/html">
<span class='nickname'>
nickname
</span>
</script>
</template>

Now that we've added all the client side static files we need, its time to focus on the
routing.

Adding Routes
Add following content as client/routes.js

Meteor.Router.add({
'/': function() {
var newPadId = Random.id();
location.href = '/' + newPadId;
},
'/:padId': {
as: 'pad',
to: function(padId) {
Session.set('padId', padId);
return 'pad'
}
}
});

The first route is simple. It will create a new padId and redirect to the second route.
The second route is a named route, named pad. It will set the padId, given in the
URL, to the Session, where the rest of our app can read it reactively.

View Source Code

10

Download Source Code

4. Let's build the Blackboard


Now we have the UI placed into our app. Let's build the blackboard by integrating
both hammer.js and the JavaScript canvas API. Again, I'm not going to explain each
and every step I made. But the code is easy to understand.
I've created a small library, which allows drawing on a canvas by mouse dragging or
touching the screen. It is an extended version of the original Meteor Blackboard
project.
Add the following content into lib/pad.js

if(!Meteor.isClient) return;
this.Pad = function Pad(id) {
var canvas = $('canvas');
var ctx = canvas[0].getContext('2d');
var drawing = false;
var from;
var skipCount = 0;
var nickname;
var color;
setNickname(localStorage.getItem('nickname') || Random.id());
var pad = canvas.attr({
width: $(window).width(),
height: $(window).height()
}).hammer()
//hammer.js touch events
pad.on('dragstart', onDragStart);
pad.on('dragend', onDragEnd);
pad.on('drag', onDrag);
function onDrag(event) {
if(drawing) {
var to = getPosition(event);
drawLine(from, to, color);
from = to;
skipCount = 0;
}
}
function onDragStart(event) {
drawing = true;
from = getPosition(event);
}
function onDragEnd() {

11

drawing = false;
}
function getPosition(event) {
return {
x: parseInt(event.gesture.center.pageX),
y: parseInt(event.gesture.center.pageY)
};
}
function drawLine(from, to, color) {
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.closePath();
ctx.stroke();
}
function setNickname(name) {
nickname = name;
$('#show-nickname b').text(nickname);
localStorage.setItem('nickname', nickname);
color = localStorage.getItem('color-' + nickname);
if(!color) {
color = getRandomColor();
localStorage.setItem('color-' + nickname, color);
}
}
function wipe(emitAlso) {
ctx.fillRect(0, 0, canvas.width(), canvas.height());
}
ctx.strokeStyle = color;
ctx.fillStyle = '#000000';
ctx.lineCap = 'round';
ctx.lineWidth = 3;
ctx.fillRect(0, 0, canvas.width(), canvas.height());
// Stop iOS from doing the bounce thing with the screen
document.ontouchmove = function(event){
event.preventDefault();
}
//expose API
this.drawLine = drawLine;
this.wipe = wipe;
this.setNickname = setNickname;
this.close = function() {
pad.off('dragstart', onDragStart);
pad.off('dragend', onDragEnd);
pad.off('drag', onDrag);
};

12

function getRandomColor() {
var letters = '0123456789ABCDEF'.split('');
var color = '#';
for (var i = 0; i < 6; i++ ) {
color += letters[Math.round(Math.random() * 15)];
}
return color;
}

Now it is time to integrate the above Pad class with our routes. To do that, add
following content into client/views/pad.js

var pad;
Meteor.startup(function() {
Deps.autorun(function() {
if(pad) {
pad.close();
}
var padId = Session.get('padId');
pad = new Pad(padId);
});
});

Whenever the router sets a padId, the above code will take care of closing the existing
pad and create a new one.
Have you noticed the 3 buttons we have in the right top corner? Let's add their
functionality.
Append the following content to client/views/pad.js.

$(function() {
//Clear Blackboard
$('body').on('click', '#wipe', function() {
pad.wipe(true);
});
//Change Nickname
$('body').on('click', '#set-nickname', function() {
var name = prompt('Enter your nickname');
if(name && name.trim() != '') {
pad.setNickname(name);
}
});

13

//Create New
$('body').on('click', '#create-new', function() {
var newPadId = Random.id();
Meteor.Router.to('pad', newPadId);
});
});

Now we've a fully functioning blackboard. Give it a try.

In the next section you'll be starting to integrate Meteor Streams with the
Blackboard and make it realtime.
View Source Code

14

Download Source Code

5. Designing the App for Meteor


Streams
Now we have a fully functioning blackboard app. But it is not realtime and cannot be
shared with anyone. In this section I'll show you how to design our app for Meteor
Streams.

If you don't have a working blackboard app (maybe you've decided to directly jump
into this chapter), download it from here.

Meteor.Stream is the class exposed by Meteor Streams, which allows you to create a

Stream. It is a realtime EventEmitter and works across the Meteor. You can pass
messages between clients. You can even send and listen to the messages on the
Server.

Blackboard app and Stream events


In our blackboard app, when a user is drawing something on it, we need to send what
he is drawing to all other connected users. So we need to send following information:

Location where the mouse dragging (or touching on the screen) started
Locations the user is currently dragging
Location where the dragging has ended
Nickname and the color of the pencil

We'll use these three events to communication and listen to the above information.

dragstart (also sends the nickname and the color)


drag
dragend

But we have multiple pads in a single app, so we need to namespace the above events
with the padId as shown below. Let's assume our padId is L4nN7r8FpJ6xgpEiJ.

15

L4nN7r8FpJ6xgpEiJ:dragstart
L4nN7r8FpJ6xgpEiJ:drag
L4nN7r8FpJ6xgpEiJ:dragend

Permissions
By default, clients don't have read or write access to the Stream. We need to explicitly
enable permissions. We can use event name, userId and subscriptionId to make
the decision to allow or deny conditionally.

Read more on Meteor Streams security and permissions

For simplicity, we'll enable clients to communicate without any restrictions at first.
But in the later chapters, we'll discuss more on advanced permissions, which make
the app more efficient.
Let's start integrating Meteor Streams!

16

6. Adding Meteor Streams Support


We will be using a single Stream and it needs to be created on both client and server.
When creating the stream, we need to use the same name for it in both places. It is
not the variable name, but the name of the stream passed as the first parameter.
Let's add the following code in streams.js.

LineStream = new Meteor.Stream('lines');


if(Meteor.isServer) {
LineStream.permissions.read(function() {
return true;
});
LineStream.permissions.write(function() {
return true;
});
}

It will create a Stream in both client and server and add permissions. Here we simply
add no restrictions to the stream. Its not ideal, but we'll catch up with this later.

Integrating Blackboard (Pad)


Now we need to emit events when a user draws something so others can get those
events and make them visible on their screens. See following diff of the lib/pad.js
where you need to make changes.

if(drawing) {
var to = getPosition(event);
drawLine(from, to, color);
+
LineStream.emit(id + ':drag', nickname, to);
from = to;
skipCount = 0;
}
@@ -33,10 +33,12 @@ this.Pad = function Pad(id) {
function onDragStart(event) {
drawing = true;
from = getPosition(event);
+
LineStream.emit(id + ':dragstart', nickname, from, color);
}

17

function onDragEnd() {
drawing = false;
+
LineStream.emit(id + ':dragend', nickname);
}
function getPosition(event) {
@@ -66,6 +68,9 @@ this.Pad = function Pad(id) {

+
+
+

function wipe(emitAlso) {
ctx.fillRect(0, 0, canvas.width(), canvas.height());
if(emitAlso) {
LineStream.emit(id + ':wipe', nickname);
}

Click here to view the full file if you are having difficulty understanding the above
diff.

Now we are emitting events, so the next step is to listen to those events and render
them onto the screen. To do that, I've created a class called RemotePad. I've also
added inline comments for you to understand it quickly.
Add the following content to lib/remote_pad.js

if(!Meteor.isClient) return;
this.RemotePad = function RemotePad(padId, pad) {
var users = {};
//listening on the dragstart event for the given padId
LineStream.on(padId + ':dragstart', function(nickname, position,
color) {
//display the nickname pointer on the screen as remote user draws
on the pad
var pointer = $($('#tmpl-nickname').text());
pointer.text(nickname);
positionPointer(pointer, position);
$('body').append(pointer);
users[nickname] = {
color: color,
from: position,
pointer: pointer
};
});

18

//listening on the dragend event for the given padId


LineStream.on(padId + ':dragend', function(nickname) {
//cleaning at the dragend
var user = users[nickname];
if(user) {
user.pointer.remove();
users[nickname] = undefined;
}
});
//listening on the drag event for the given padId
LineStream.on(padId + ':drag', function(nickname, to) {
var user = users[nickname];
if(user) {
//when remote user is dragging, do the same here and re-position
the nickname pointer
pad.drawLine(user.from, to, user.color);
positionPointer(user.pointer, to);
user.from = to;
}
});
// listening on the wipe event and wipe the blackboard
LineStream.on(padId + ':wipe', function(nickname) {
pad.wipe();
});
function positionPointer(pointer, position) {
pointer.css({
top: position.y + 10,
left: position.x + 10
});
}
this.close = function() {
//remove all the listeners, when closing
LineStream.removeAllListeners(padId + ':dragstart');
LineStream.removeAllListeners(padId + ':dragend');
LineStream.removeAllListeners(padId + ':drag');
LineStream.removeAllListeners(padId + ':wipe');
};
}

19

As the last step, we need to integrate RemotePad when we are creating a route. See
following diff of the client/views/pad.js for how you can do it.

@@ -1,13 +1,16 @@
var pad;
+var remotePad;
Meteor.startup(function() {
Deps.autorun(function() {
if(pad) {
pad.close();
+
remotePad.close();
}
var padId = Session.get('padId');
pad = new Pad(padId);
+
remotePad = new RemotePad(padId, pad);
});
});

Click here to view the full file if you are having difficulty understanding the above
diff.

Okay, now our blackboard is realtime and multiple users can draw on it at the same
time. Nice! To see that for yourself, open the same blackboard URL in two different
browsers and see.
View Source Code

20

Download Source Code

7. Adding Fine-Grade Permissions


Our blackboard app is now working, but it is not efficient. Because we have failed to
add permissions, that makes sense.

Why we need permissions


Let's have a look at our app. It emits events and other listen to them. Have you
noticed that although we are only listening to the events for the current padId,
nothing prevents receiving events for other padIds as well.
This means that every event clients are emitting will be delivered to each and every
client, regardless of the padId. So we need to fix that.
Ideally, clients should receive events belongs to the padId, which they are currently
listening to. We can easily do it with permissions.
1. First, we need notify the server that client (client Y) is only interested in
receiving events for padId X.
2. In the server for client Y, we must only send events related to padId X

In the next section, you'll see that I've discussed subscription and
subscriptionId. A subscription is created when a client (browser tab) is connected
to a stream. Meteor Streams creates a unique id for each subscription and it is the
subscriptionId

Let's do the modifications


First, we need to notify the server about the padId we are interested in. We need to
modify lib/pad.js for that. The following diff explains how to do it.

@@ -10,6 +10,9 @@ this.Pad = function Pad(id) {


var color;
setNickname(localStorage.getItem('nickname') || Random.id());
+
+
+

21

//send padid to the sever


LineStream.emit('pad', id);

var pad = canvas.attr({


width: $(window).width(),

Then we need to listen to the pad event on the server and update our permissions. To
do that, you need to replace streams.js with the following content. I've added
inline comments to make it more understandable.

LineStream = new Meteor.Stream('lines');


if(Meteor.isServer) {
//map subscriptionId and the padId he is listening to
var subscriptionPadsMap = {};
LineStream.on('pad', function(padId) {
var subscriptionId = this.subscriptionId;
subscriptionPadsMap[subscriptionId] = padId;
this.onDisconnect = function() {
subscriptionPadsMap[subscriptionId] = undefined;
};
});
LineStream.permissions.read(function(event) {
//getting padId from the event
var matched = event.match(/(.*):/);
if(matched) {
var padId = matched[1];
//only allow events with padId where subscription is
interestedIn
return subscriptionPadsMap[this.subscriptionId] == padId;
} else {
//only allows events with padId to read from the stream
return false;
}
}, false); //end false make sure read permissons will not be cached.
See permission docs for more
LineStream.permissions.write(function(event) {
return true;
});
}

Hooray! Now our blackboard is fully functional and efficient. Deploy it and try with
your friends in realtime.
View Source Code

22

Download Source Code

Thank You
Thank you for taking the time to read this eBook. Ive tried to make it simple and
straight to the point. I hope you have enjoyed it.
Try to build something innovative using Meteor Streams. Try to use it with your next
project. Meteor Streams is a new project, but I will continue to develop it -- theres a
lot to do.
If you encounter something weird with Meteor Streams, create an issue for it. And I
would love to see your contributions. It could be a blog post, sample app, an issue or a
pull request.
Have a Good Day!
Arunoda Susiripala
@arunoda
http://meteorhacks.com

23

You might also like