Get started with Meteor
and
Build a chat application
The complete source code is available in steps on
Installing Meteor
Clone the sample application
If you have a Github Account
If you dont
Start the application
git clone git@github.com:RocketChat/workshop.git
cd workshop
meteor
#master
git clone https://github.com/RocketChat/workshop.git
curl https://install.meteor.com/ | sh
meteor update
git stash
CTRL + C
git add .
git commit -m "Description of my changes."
git checkout step-##
meteor add iron:router
#step-01
#step-02
/lib
/client #for the browser only
/lib
/stylesheets
/views
/home
/channel
/server #for the server only
/lib
/publications
/public #graphics, fonts, icons etc
rm workshop.js workshop.html workshop.css
Router.map(function (){
this.route('home', {
path: '/'
});
});
<template name="home">
<ul>
{{#each channels}}
<li>{{name}}</li>
{{/each}}
</ul>
</template>
lib/routes.js
client/views/home/home.html
#step-03
this.route('channel', {
path: '/channel/:_id'
});
<template name="channel">
<ul>
{{#each messages}}
<li>
{{message}}
</li>
{{/each}}
</ul>
</template>
lib/routes.js
client/views/channel/channel.html
#step-04
<template name="home">
<form>
<label for="name">Channel name:</label>
<input id="name" type="text">
<button type="submit">Add</button>
</form>
...
Template.home.events({
'submit form': function(event, instance) {
event.preventDefault();
var name = instance.find('input').value;
instance.find('input').value = '';
Channels.insert({name: name});
}
});
client/views/home/home.js
client/views/home/home.html
#step-05
Channels = new Mongo.Collection('channels');
lib/channels.js
...
{{#each channels}}
<li><a href="/channel/{{_id}}">{{name}}</a></li>
{{/each}}
...
#step-06
client/views/home/home.html
Template.home.helpers({
channels: function() {
return Channels.find();
}
});
client/views/home/home.js
#step-07
Template.channel.helpers({
channel: function() {
var _id = Router.current().params._id;
return Channels.findOne({_id: _id});
}
});
client/views/channel/channel.js
<template name="channel">
<h1>{{channel.name}}</h1>
...
...
{{> home}}
</template>
client/views/channel/channel.html
Create an event handler that insert a message
Messages = new Mongo.Collection('messages');
<form>
<textarea rows="1" cols="100"></textarea>
</form>
{{> home}}
client/views/channel/channel.html
lib/messages.js
#step-08
Template.channel.events({
'keyup textarea': function(event, instance) {
// Check if enter was pressed (but without shift).
if (event.keyCode == 13 && !event.shift) {
var _id = Router.current().params._id;
var value = instance.find('textarea').value;
instance.find('textarea').value = ''; // Clear
Messages.insert({_channel: _id, message: value});
}
}
});
client/views/channel/channel.js
Template.channel.helpers({
messages: function() {
var _id = Router.current().params._id;
return Messages.find({_channel: _id});
},
...
client/views/channel/channel.js
#step-09
<li>
{{#markdown}}{{message}}{{/markdown}}
</li>
client/views/channel/channel.html
meteor add markdown
// Markdown requires double spaces at the end of the line
value = value.replace("\n", " \n");
client/views/channel/channel.js
meteor add accounts-password
...
</ul>
{{> loginButtons}}
</template>
client/views/home/home.html
#step-10
meteor add accounts-ui
#step-11
instance.find('textarea').value = ''; // Clear
Messages.insert({
_channel: _id,
message: value,
_userId: Meteor.userId() // Add userId
});
client/views/channel/channel.js
user: function() {
return Meteor.users.findOne({_id: this._userId});
}
client/views/channel/channel.js
<div>{{user.emails.[0].address}}</div>
{{#markdown}}{{{message}}}{{/markdown}}
client/views/channel/channel.html
Messages.insert({
_channel: _id, // Channel reference.
message: value,
_userId: Meteor.userId(), // Add userId
timestamp: new Date() // Add a timestamp
});
client/views/channel/channel.js
#step-12
time: function() {
return moment(this.timestamp).format('h:mm a');
}
client/views/channel/channel.js
meteor add momentjs:moment
<li>
<div>{{user.emails.[0].address}} {{time}}</div>
{{#markdown}}{{{message}}}{{/markdown}}
</li>
client/views/channel/channel.html
date: function(messages) {
var dateNow = moment(this.timestamp).calendar();
var instance = Template.instance();
if (!instance.date || instance.date != dateNow) {
return instance.date = dateNow;
}
}
client/views/channel/channel.js
#step-13
moment.locale('en', {
calendar : {
lastDay : '[Yesterday]',
sameDay : '[Today]',
sameElse : 'MMMM Do, YYYY'
}
});
lib/moment.js
{{#each messages}}
<h2>{{date}}</h2>
client/views/channel/channel.html
meteor remove autopublish
#step-14
Meteor.publish('channels', function() {
return Channels.find();
});
server/publications/channels.js
#step-15
Meteor.publish('messages', function(channel) {
return Messages.find({_channel: channel});
});
server/publications/messages.js
#step-16
Meteor.publish("allUserNames", function () {
if (this.userId) { // We only send data to logged in users
return Meteor.users.find({},{
fields: {'profile.username': 1}
});
}
});
server/publications/accounts.js
Accounts.ui.config({
passwordSignupFields: 'USERNAME_AND_EMAIL'
});
client/lib/accounts.js
#step-17
Template.home.onCreated(function() {
this.subscribe('channels');
});
client/views/home/home.js
#step-18
Template.channel.onCreated(function() {
var instance = this;
// Listen for changes to reactive variables
// such as Router.current()
instance.autorun(function() {
var channel = Router.current().params._id;
instance.subscribe('messages', channel);
});
});
client/views/channel/channel.js
#step-19
Template.home.onCreated(function() {
this.subscribe('channels');
this.subscribe('allUserNames');
});
client/views/home/home.js
{{#each messages}}
<h2>{{date}}</h2>
<li>
<div>{{user.username}} {{time}}</div>
{{#markdown}}{{{message}}}{{/markdown}}
</li>
{{/each}}
client/views/channel/channel.html
#step-20
Channels.allow({
insert: function(userId, doc) {
if (userId) {
return true;
}
}
});
lib/channels.js
meteor remove insecure
Messages.allow({
insert: function(userId, doc) {
if (userId && doc._channel) {
return true;
}
}
});
lib/messages.js
#step-21
<template name="layout">
<header>
{{> yield "header"}}
</header>
<aside>
{{> yield "aside"}}
</aside>
<article>
{{> yield}}
</article>
<footer>
{{> yield "footer"}}
</footer>
</template>
client/layouts/layout.html
Router.configure({
layoutTemplate: 'layout'
});
lib/routes.js
#step-22
<template name="home">
{{#contentFor 'aside'}}
<form>
...
{{> loginButtons}}
{{/contentFor}}
</template>
../home/home.html
<template name="channel">
{{#contentFor 'header'}}
<h1>{{channel.name}}</h1>
{{/contentFor}}
...
{{#contentFor 'footer'}}
{{> channelForm}}
{{/contentFor}}
{{> home}}
</template>
../channel/channel.html
// We've moved the message form into a new template
// (channelForm), now we need to move the event map.
Template.channelForm.events({
'keyup textarea': function(event, instance) {
...
../channel/form.js
<template name="channelForm">
<form>
<textarea rows="1"
cols="100"></textarea>
</form>
</template>
../channel/form.html
#step-23
$aside: 220px;
$top: 53px;
header,
aside,
footer { position: fixed; }
header { top: 0; height: $top; margin-left: $aside; }
aside { top: $top; width: $aside; }
article { margin-left: $aside; }
footer { position: fixed; bottom: 0; margin-left: $aside; }
meteor add fourseven:scss
client/css/styles.scss
#step-24
$aside: 220px;
$top: 53px;
$primary: #453744;
$padding: 16px;
body { margin: 0; }
header, aside, footer { position: fixed; }
header {
top: 0; height: $top; margin-left: $aside; background: white; width: 100%;
h1 { line-height: 53px; margin: 0; padding: 0 $padding; }
}
aside { top: $top; width: $aside; background: $primary; height: calc(100% - 53px);
padding: $padding; box-sizing: border-box;
&, a { color: lighten($primary, 30%); }
}
article {padding: $padding; margin-top: 53px; height: calc(100% - 53px); margin-left: $aside;}
footer {position: fixed; bottom: 0; margin-left: $aside; padding: $padding; background: white;}
client/css/styles.scss
Add your own styling, and make it beautiful and fancy.
If you checkout step-25 we've made it somewhat fancy for you. ;)
#step-25