This is 2013. If you are going to build a webapp, you must add real-time capabilities to the app. It is the standard. Meteor does a pretty good job at helping you to quickly build and make apps real-time. But meteor is tightly coupled with MongoDB and it is the only way to add real-time capabilities. Sometimes, this is overkill.
MongoDB is a perfect match for Meteor. But we don't need to use MongoDB for all our real-time activities. For some problems, messaging based solutions work really well. It's the same problem that pubnub and real-time.co are also addressing.
It would be great if we could have a hybrid approach to real-time, in Meteor, combining the MongoDB Collection based approach and a Messaging based approach. Thus Meteor Streams was born to add this messaging based, real-time communication to Meteor.
Introducing Meteor Streams
A Stream is the basic building block of Meteor Streams. It is a real-time EventEmitter. With a Stream, you can pass messages back and forth between connected clients. It is highly manageable and has a very good security model.
Lets Give It a Try
Let's create a very simple, browser console based chat application with Meteor Streams. We'll first create a new Meteor application:
meteor create hello-stream
Next we install Meteor Streams from the atmosphere:
mrt add streams
Then we need to create a file named chat.js
and place in the following code:
chatStream = new Meteor.Stream('chat'); if(Meteor.isClient) { sendChat = function(message) { chatStream.emit('message', message); console.log('me: ' + message); }; chatStream.on('message', function(message) { console.log('user: ' + message); }); }
Start your app with:
meteor
Your app will now be running on - http://localhost:3000
.
Now you have a fully functioning chat app. To start chatting, open the browser console and use the sendChat
method as shown as below.
Let's Dive In Further
It's kind of hard to understand Meteor Streams with just a simple console based example, like the one we just built above. So, let's build a full featured chat application to become more familiar with Meteor Streams.
The App
The app we are creating is a web based chat application. Anyone can chat anonymously. Also, users can register and chat with their identity(username). It also has a filtering system, which filters out bad words (profanity).
At the end, it will look something like this. You can grab the source code from github to see the final result.
Let's Create the App
Let's create a standard Meteor app and install Meteor Streams from atmosphere. We'll also be adding support for bootstrap and Meteor Accounts.
meteor create awesome-chat-app cd awesome-chat-app meteor remove insecure autopublish meteor add bootstrap accounts-password accounts-ui mrt add streams rm awesome-chat-app.* //remove files added automatically
Let's Build the UI
The user interface for our app will be pretty simple. We have a div
showing the chat messages and an input
box to enter in new chat messages. See below for the complete HTML of our UI. Check out the inline comments if you need help understanding the code.
Add the following content into client/home.html
:
<head> <title>Awesome Chat App</title> <style type="text/css"> #chat-message { width: 500px; height: 50px; } #messages { width: 700px; height: 300px; border: 1px solid rgb(230, 230, 230); margin: 0px 0px 10px 0px; } </style> </head> <body> {{> mainBox}} </body> <!-- Main Chat Window --> <template name='mainBox'> <div class='container'> <h2>Awesome Chat App</h2> <!-- shows login buttons --> {{loginButtons}} {{> chatBox}} </div> </template> <!-- Chat Box with chat messages and the input box --> <template name='chatBox'> <div id='messages'> {{#each messages}} {{>chatMessage}} {{/each}} </div> <textarea id='chat-message'></textarea><br> <button class='btn btn-primary' id='send'>Send Chat</button> </template> <!-- Template for the individual chat message --> <template name='chatMessage'> <div> <b>{{user}}:</b> {{message}} </div> </template>
Wiring Up Our Chat
Meteor's reactivity is an awesome concept and very useful. Now, Meteor Streams is not a reactive data source. But it can work well with local only collections to provide reactivity.
As the name implies, local only collections do not sync its data with the server. Its data is only available inside the client(browser tab).
Add the following content into lib/namespace.js
to create our local only collection:
if(Meteor.isClient) { chatCollection = new Meteor.Collection(null); }
Now it's time to wire up our templates with the collection. Let's do following:
- Assign the collection to the
messages
helper in thechatBox
template. - Generate a value for the
user
helper in thechatMessage
template. - When the
Send Chat
button is clicked, add the typed chat message into the collection.
Add the following content to client/ui.js
:
// assign collection to the `messages` helper in `chatBox` template Template.chatBox.helpers({ "messages": function() { return chatCollection.find(); } }); // generate a value for the `user` helper in `chatMessage` template Template.chatMessage.helpers({ "user": function() { return this.userId; } }); // when `Send Chat` clicked, add the typed chat message into the collection Template.chatBox.events({ "click #send": function() { var message = $('#chat-message').val(); chatCollection.insert({ userId: 'me', message: message }); $('#chat-message').val(''); } });
With the above changes you'll be able to chat, but messages are only display on your client. So let's handover the rest of the job to Meteor Streams.
Let's Create the Stream
We'll be creating the stream on both the client and the server (with the same name) and adding the necessary permissions.
Append the following code into lib/namespace.js
to create the stream:
chatStream = new Meteor.Stream('chat-stream');
Just creating the stream alone is not enough; we need to give the necessary permissions, which allow clients to communicate through it. There are two types of permissions (read and write). We need to consider the event, userId, and the subscriptionId when we are creating the permission.
-
userId
is the userId of the client connected to the stream. -
subscriptionId
is the unique identifier created for each client connected to the stream.
For our chat app, we need to give anyone using the app full read and write access to the chat
event. This way, clients can use it for sending and receiving chat messages.
Add the following code to server/permissions.js
:
chatStream.permissions.read(function(eventName) { return eventName == 'chat'; }); chatStream.permissions.write(function(eventName) { return eventName == 'chat'; });
Connecting the Stream With the UI
Now that we have a fully functioning stream, let's connect it to the UI so others can see the messages that you are sending.
The first thing we need to do is add our chat messages to the stream, when we click on the Send Chat
button. For that, we need to modify the code related to the Send Chat button's click event(click #send), as follows (in client/ui.js
):
Template.chatBox.events({ "click #send": function() { var message = $('#chat-message').val(); chatCollection.insert({ userId: 'me', message: message }); $('#chat-message').val(''); // == HERE COMES THE CHANGE == //add the message to the stream chatStream.emit('chat', message); } });
Then we need to listen to the stream for the chat
event and add the message to the chatCollection
which is being rendered in the UI, reactively. Append the following code to the client/ui.js
file:
chatStream.on('chat', function(message) { chatCollection.insert({ userId: this.userId, //this is the userId of the sender subscriptionId: this.subscriptionId, //this is the subscriptionId of the sender message: message }); });
Now we need to modify the logic which generates the value for the user
helper in the chatMessage
template as follows:
- Logged in user -
user-<userId>
- Anonymous user -
anonymous-<subscriptionId>
Modify the code for the user
helper in the chatMessage
template to reflect the above changes (in client/ui.js
):
Template.chatMessage.helpers({ "user": function() { var nickname = (this.userId)? 'user-' + this.userId : 'anonymous-' + this.subscriptionId; return nickname; } });
Displaying the Username Instead of the userId
Showing just the userId
is not very useful. So let's change it to display the actual username. Here, we'll be using Meteor Pub/Sub to get the username for a given userId.
First of all, lets configure Meteor Accounts to accept the username when creating the user. Add the following code to client/users.js
:
Accounts.ui.config({ passwordSignupFields: "USERNAME_ONLY" });
Then let's create the publication for getting the user. Add the following code to server/users.js
. It simply returns the username for a given userId.
Meteor.publish("user-info", function(id) { return Meteor.users.find({_id: id}, {fields: {username: 1}}); });
Now we need to create a subscription on the client for each user we are interested in. We'll do this inside a method. Additionally, after we get the username, it needs to be assigned to a session variable. Then we can use the session variable inside the user
helper to get the username reactively.
Append the following code into client/users.js
:
getUsername = function(id) { Meteor.subscribe('user-info', id); Deps.autorun(function() { var user = Meteor.users.findOne(id); if(user) { Session.set('user-' + id, user.username); } }); }
Finally, let's modify the user
helper in the chatMessage
template to get the username from the session (in client/ui.js
):
Template.chatMessage.helpers({ "user": function() { if(this.userId == 'me') { return this.userId; } else if(this.userId) { getUsername(this.userId); return Session.get('user-' + this.userId); } else { return 'anonymous-' + this.subscriptionId; } } });
Filtering Out Bad Words
Our chat app will make sure to hide any profanity. If someone tries to send a message with some bad words, we need to filter those out. Meteor Stream has a feature called filters, which is designed for this. Let's see how we can filter out the word fool
from any chat message.
Add the following code into server/filters.js
:
chatStream.addFilter(function(eventName, args) { if(eventName == 'chat') { var message = args[0]; if(message) { message = message.replace(/fool/ig, '****'); } return [message]; } else { return args; } });
Feel free to add in your own filters.
Our chat app is now complete. You can see a live version of the app at http://streams-chat.meteor.com. Additionally, the Source code for the app is available on Github.
Conclusion
In this tutorial we built a chat application using local only collections
for adding in reactivity and used Meteor Pub/Sub
for getting the username of a user. Hopefully you can see how nicely Meteor Streams can work with existing Meteor functionality. Still, this is just an introduction to Meteor Streams, for additional resources, check out the following links:
- Introducing Meteor Streams - An article on MeteorHacks.
- Meteor Streams Documentation.
- Example Apps.
Comments