Get started with Meteor
and
Build a chat application (Slack style)
The complete source code is available in steps on
https://slides.com/timbrandin/meteor-slack
curl https://install.meteor.com/ | sh
meteor update
git stash
CTRL + C
git add .
git commit -m "Description of my changes."
https://slides.com/timbrandin/meteor-slack
// If you have a Github Account:
git clone git@github.com:studiointeract/meteor-slack.git
// If not:
git clone https://github.com/studiointeract/meteor-slack.git
cd meteor-slack
meteor
#master
https://slides.com/timbrandin/meteor-slack
CTRL + C
meteor add iron:router
meteor
#step-1
#step-2
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-3
...
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-4
<template name="home">
<form>
<label for="name">Channel name:</label>
<input id="name" type="text">
<button type="submit">Add</button>
</form>
<ul>
...
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-5
Channels = new Mongo.Collection('channels');
lib/channels.js
...
{{#each channels}}
<li><a href="/channel/{{_id}}">{{name}}</a></li>
{{/each}}
...
..
{{> home}}
</template>
client/views/channel/channel.html
#step-6
client/views/home/home.html
Template.home.helpers({
channels: function() {
return Channels.find();
}
});
Template.home.events({
...
client/views/home/home.js
#step-7
Template.channel.helpers({
channel: function() {
var _id = Router.current().params._id;
return Channels.findOne({_id: _id});
}
});
../channel/channel.js
<template name="channel">
<h1>{{channel.name}}</h1>
<ul>
...
../channel/channel.html
Messages = new Mongo.Collection('messages');
...
</ul>
<form>
<textarea rows="1" cols="100"></textarea>
</form>
{{> home}}
...
client/views/channel/channel.html
lib/messages.js
#step-8
...
});
Template.channel.events({
'keyup textarea': function(event, instance) {
if (event.keyCode == 13 && !event.shift) { // Check if enter was pressed (but without shift).
var _id = Router.current().params._id;
var value = instance.find('textarea').value;
instance.find('textarea').value = ''; // Clear the textarea.
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});
},
channel: function() {
...
client/views/channel/channel.js
#step-9
...
{{#each messages}}
<li>
{{#markdown}}{{message}}{{/markdown}}
</li>
...
../channel/channel.html
meteor add markdown
...
var value = instance.find('textarea').value;
// Markdown requires double spaces at the end of the line to force line-breaks.
value = value.replace("\n", " \n");
instance.find('textarea').value = ''; // Clear the textarea.
...
../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 the textarea.
Messages.insert({
_channel: _id,
message: value,
_userId: Meteor.userId() // Add userId to each message.
});
}
}
});
client/views/channel/channel.js
...
},
user: function() {
return Meteor.users.findOne({_id: this._userId});
}
});
Template.channel.events({
...
../channel/channel.js
...
{{#each messages}}
<li>
<div>{{user.emails.[0].address}}</div>
{{#markdown}}{{{message}}}{{/markdown}}
...
../channel/channel.html
...
Messages.insert({
_channel: _id, // Channel reference.
message: value,
_userId: Meteor.userId(), // Add userId to each message.
timestamp: new Date() // Add a timestamp to each message.
});
...
client/views/channel/channel.js
#step-12
...
},
time: function() {
return moment(this.timestamp).format('h:mm a');
}
});
Template.channel.events({
...
client/views/channel/channel.js
meteor add momentjs:moment
...
{{#each messages}}
<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>
<li>
...
../channel/channel.html
meteor remove autopublish
#step-14
Channels = new Mongo.Collection('channels');
if (Meteor.isServer) {
Meteor.publish('channels', function() {
return Channels.find();
});
}
lib/channels.js
#step-15
Messages = new Mongo.Collection('messages');
if (Meteor.isServer) {
Meteor.publish('messages', function(channel) {
return Messages.find({_channel: channel});
});
}
lib/messages.js
#step-16
...
}
else {
Meteor.publish("allUserNames", function () {
if (this.userId) { // We should only send data to logged in users.
return Meteor.users.find({}, {fields: {'profile.username': 1}});
}
});
}
lib/accounts.js
if (Meteor.isClient) {
Accounts.ui.config({
passwordSignupFields: 'USERNAME_AND_EMAIL'
});
}
lib/accounts.js
#step-17
Template.home.onCreated(function() {
this.subscribe('channels');
});
Template.home.helpers({
...
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);
});
});
Template.channel.helpers({
...
client/views/channel/channel.js
#step-19
Template.home.onCreated(function() {
this.subscribe('channels');
this.subscribe('allUserNames');
});
Template.home.helpers({
...
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
...
if (Meteor.isServer) {
Channels.allow({
insert: function(userId, doc) {
if (userId) {
return true;
}
}
});
Meteor.publish('channels', function() {
...
lib/channels.js
meteor remove insecure
...
if (Meteor.isServer) {
Messages.allow({
insert: function(userId, doc) {
if (userId && doc._channel) {
return true;
}
}
});
Meteor.publish('messages', function(channel) {
...
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'
});
Router.map(function (){
...
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}}
<ul>
...
</ul>
{{#contentFor 'footer'}}
{{> messageForm}}
{{/contentFor}}
{{> home}}
</template>
<template name="messageForm">
<form>
<textarea rows="1" cols="100"></textarea>
</form>
</template>
../channel/channel.html
...
// We've moved the message form into a new template
// (messageForm), now we need to move the event map.
Template.messageForm.events({
'keyup textarea': function(event, instance) {
...
../channel/channel.js
#step-23
$aside: 220px;
$top: 53px;
header,
aside,
footer {
position: fixed;
}
header {
top: 0;
height: $top;
margin-left: $aside;
}
...
client/css/styles.scss
meteor add fourseven:scss
...
aside {
top: $top;
width: $aside;
}
article {
margin-left: $aside;
}
footer {
position: fixed;
bottom: 0;
margin-left: $aside;
}
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 {
padding: 0 $padding; // padding only on left and right side.
margin: 0;
line-height: 53px;
}
}
...
client/css/styles.scss
...
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
#step-25