WORKSHOP
Get started with Meteor
and
Build a chat application (Slack style)
The complete source code is available in steps on
Agenda
- Install Meteor (1.1.0.2)
- Introduction + Frequent Issues
- Setup
- Iron Router + Templates + Forms + Collections
- Accounts + Time (Moment.js)
- Publications
- Subscriptions
- Security
- Iron Layouts + SASS preprocessing + Styling
- Add your own style
https://slides.com/timbrandin/meteor-slack
Install Meteor
curl https://install.meteor.com/ | sh
Update Meteor 1.1.0.2
meteor update
Introduction
- We will build a chat application with channels for multiple users and infinite scroll and a list of channels in a sidebar.
- Start with the boilerplate we've created for you on: github.com/studiointeract/meteor-slack
- Try to solve each problem before you checkout the solution, each step is contained within the repository.
- Learn about Templates (Blaze), Iron Router and Publications and Subscriptions
FREQUENT ISSUES
- I cannot checkout the next step!
I don't want to save: Stash your changes.
I want to save: Commit your changes.
- How do I stop the application?
git stash
CTRL + C
git add .
git commit -m "Description of my changes."
https://slides.com/timbrandin/meteor-slack
Setup
- Clone the sample application
- Start the application
// 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
Add Iron Router
- Stop the application
- Add iron:router
- Start the application again
- Check it out in the browser
CTRL + C
meteor add iron:router
meteor
#step-1
CREATE FOLDERS
- First remove meteor-slack.js, meteor-slack.html and meteor-slack.css
- lib/
- client/ (for the browser only)
- client/views/
- client/views/home/
- client/views/channel/
- server/ (for the server only)
- public/ (graphics, fonts, icons etc.)
#step-2
HOME route
- Create a home route in routes.js
- Create a template for home listing channels
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
CHANNEL route
- Create a channel route in routes.js
- Create a template for a channel listing messages
...
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
Adding CHANNELS
- Create a 'channels' Mongo collection
- Add a form to input new channels to home
- Create an event handler that insert a channel
<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
CHANNEL SWITCHING
- Create a link to a channel from home
- Add the links to the channels in a channel
...
{{#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
SHOWiNG CHANNELS
- Add a helper to show (find) all channels in home
- Show the channel name in the channel
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
- Create a 'messages' Mongo collection
-
Add a form with a single line textarea for new messages
- Create an event handler that insert a message on enter (but not when shift is pressed).
And make sure the message has a
reference to the current channel too.
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
Adding MESSAGES
...
});
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
Showing MESSAGES
...
{{#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
Adding USERS
- Enable login with password to your application
- Now add login buttons
- Now create an account for yourself in your app: localhost:3000/
meteor add accounts-password
...
</ul>
{{> loginButtons}}
</template>
client/views/home/home.html
#step-10
meteor add accounts-ui
WHO TYPED WHAT
- Add a reference to the user on a message
- Create a helper and show the username (just show the email for now).
#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
- Add a timestamp to each message
- Show the time in nice formatting, i.e. "16:19 PM"
...
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
Adding TIME
...
},
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
- Create a helper to tell what date it is, but only print the date if it differs from the last printed date (use Template.instance()).
- And localize it to show 'today' or 'yesterday'
...
},
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
Split BY DAY
moment.locale('en', {
calendar : {
lastDay : '[Yesterday]',
sameDay : '[Today]',
sameElse : 'MMMM Do, YYYY'
}
});
lib/moment.js
...
{{#each messages}}
<h2>{{date}}</h2>
<li>
...
../channel/channel.html
Publish CHANNELS
- Remove autopublish (we don't want to publish everything always).
- Create a publication for 'channels' and publish all channels.
meteor remove autopublish
#step-14
Channels = new Mongo.Collection('channels');
if (Meteor.isServer) {
Meteor.publish('channels', function() {
return Channels.find();
});
}
lib/channels.js
Publish MESSAGES
- Create a publication for 'messages' for a channel.
#step-15
Messages = new Mongo.Collection('messages');
if (Meteor.isServer) {
Meteor.publish('messages', function(channel) {
return Messages.find({_channel: channel});
});
}
lib/messages.js
Publish USERNAMES
- Add a username field to the sign-up form.
- Create a publication for 'allUserNames' but only send the username to logged in users.
#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
SUBSCRIBE TO CHANNELS
- Create a subscription to the 'channels' publication in the home template.
#step-17
Template.home.onCreated(function() {
this.subscribe('channels');
});
Template.home.helpers({
...
client/views/home/home.js
SUBSCRIBE TO MESSAGES
- Subscribe to messages for a channel in the channel template.
#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
SHOW USERNAMES
- Create a subscription to the 'allUserNames' publication in the home template.
- Replace the printed email with the username for messages in the channel template
#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
input security
- Remove the insecure package
- Allow logged in users to only insert channels and messages.
#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
ADDING LAYOUTS
- Add a layout with a yield for header, body, footer and aside and set it as default in the router.
#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
CONTENT FOR
- Move content into yields, i.e. the title should go in the header, messageForm in the footer and channels in aside.
#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
SASS STYLes
- Add a SASS/SCSS preprocessor
- Create some styles, i.e. let aside be 220px fixed to the left and header 53px fixed at the top, and footer fixed to the bottom.
#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
COLORS + PADDING
- The primary color of Slack is #453744 and aside is in this color, and the text and links in aside is somewhat lighter than this.
- Fix the margin and padding on body, aside, footer and article.
- aside should fill the height of the page (100%-53px), tip use calc
- The h1 in the header should be vertically centered, tip: set line-height to 53px.
#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
find the bug!
- There's a bug now, we can't add channels. XD Can you find the solution? A hint, compare home and channel, js and html files. ;)
Add your STYLE
- 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
Meteor Workshop (Building Slack)
By Tim Brandin
Meteor Workshop (Building Slack)
- 18,380