WORKSHOP
Get started with Meteor
and
Build a chat application
The complete source code is available in steps on
Agenda
- Introduction
- Setup + Install Meteor
- Frequent Issues
- Iron Router + Templates + Forms + Collections
- Accounts + Time (Moment.js)
- Publications
- Subscriptions
- Security
- Iron Layouts + SASS preprocessing + Styling
- Add your own style
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/RocketChat/workshop
-
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
Setup
-
Installing Meteor
-
Clone the sample application
If you have a Github Account
If you dont
-
Start the application
- Open in the different browsers simultaneously
http://localhost:3000
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
FREQUENT ISSUES
- How do I checkout the next step?
-
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."
git checkout step-##
Add Iron Router
- Add Iron-Router
- Check it out in the browser
http://localhost:3000
- WTF???
meteor add iron:router
#step-01
CREATE FOLDERS
-
Remove autogenerated files
- Create basic folders
#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
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-03
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-04
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>
...
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
CHANNEL SWITCHING
- Add a helper to show (find) all channels in home
- Create a link to channels from home
...
{{#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
SHOWING CHANNELS
-
Add a helper to get the current channels data
- Show the channel name in the channel
#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 a 'messages' Mongo collection
-
Add a form with textarea for new messages
-
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
Adding MESSAGES
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
Showing MESSAGES
<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
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 to show user's email (for now)
#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
- 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
timestamp: new Date() // Add a timestamp
});
client/views/channel/channel.js
#step-12
Adding TIME
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
- Create a helper to tell what date it is
only print 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>
client/views/channel/channel.html
Publish CHANNELS
- Remove autopublish
We don't want to publish everything always
- Create a publication for 'channels'
Lets publish all channels for now
meteor remove autopublish
#step-14
Meteor.publish('channels', function() {
return Channels.find();
});
server/publications/channels.js
Publish MESSAGES
- Create a publication for 'messages' for a channel.
#step-15
Meteor.publish('messages', function(channel) {
return Messages.find({_channel: channel});
});
server/publications/messages.js
Publish USERNAMES
- Add a username field to the sign-up form.
- Create a publication for 'allUserNames'
Only send the username to logged in users.
#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
SUBSCRIBE TO CHANNELS
- Subscribe to the 'channels' in the home template.
#step-17
Template.home.onCreated(function() {
this.subscribe('channels');
});
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);
});
});
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');
});
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 only logged in users
To insert channels and messages
#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
ADDING LAYOUTS
- Add a layout and set it as default in the router
with a yield for header, body, footer and aside
#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
CONTENT FOR
-
Move content into yields
Title should go in header, messageForm in 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}}
...
{{#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
SASS STYLes
- Add a SASS/SCSS preprocessor
- Create some styles
- aside be 220px fixed to the left
- header 53px fixed at the top
- footer fixed to the bottom.
#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
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 { 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
find the bug!
-
There's a bug now, we can't add channels. :-)
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
workshop
By Gabriel Engel
workshop
- 1,927