WORKSHOP

Get started with Meteor

and

Build a chat application

 

 

The complete source code is available in steps on

github.com/RocketChat/workshop

slides.com/RocketChat/workshop

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

  1. Installing Meteor

     

  2. Clone the sample application
    If you have a Github Account

    If you dont

  3. Start the application

     

  4. 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

  1. How do I checkout the next step?

     
  2. I cannot checkout the next step!
    I don't want to save: Stash your changes.

    I want to save: Commit your changes.


     
  3. 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

  1. Add Iron-Router

     
  2. Check it out in the browser
    http://localhost:3000
     
  3. WTF???
meteor add iron:router

#step-01

CREATE FOLDERS

  1. Remove autogenerated files

     
  2. 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

  1. Create a home route in routes.js



     
  2. 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

  1. Create a channel route in routes.js



     
  2. 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

  1. Create a 'channels' Mongo collection
     
  2. Add a form to input new channels to home




     
  3. 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

  1. Add a helper to show (find) all channels in home



     
  2. 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

  1. Add a helper to get the current channels data



     
  2. 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

  1. Create a 'messages' Mongo collection
     
  2. Add a form with textarea for new messages


     
  3. 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

  1. Add helper to find messages for the channel



     
  2. Let's add markdown to enable rendering of newlines and paragraphs (more) in messages.
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

  1. Enable login with password to your application

     
  2. Now add login buttons




     
  3. 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

  1. Add a reference to the user on a message




  2. 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

  1. Add a timestamp to each message



     
  2. 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

  1. Create a helper to tell what date it is
    only print if it differs from the last printed date - use Template.instance()






     
  2. 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

  1. Remove autopublish
    We don't want to publish everything always

     
  2. 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

  1. 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

  1. Add a username field to the sign-up form.


     
  2. 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

  1. 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

  1. 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

  1. Create a subscription to the 'allUserNames' publication in the home template.



     
  2. 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

  1. Remove the insecure package
     
  2. 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

  1. 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

  1. 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

  1. Add a SASS/SCSS preprocessor
     
  2. 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

  1. 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.
  2. Fix the margin and padding on body, aside, footer and article.
  3. aside should fill the height of the page (100%-53px), tip use calc
  4. 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!

  1. There's a bug now, we can't add channels.  :-)
    ​Can you find the solution?

     
  2. 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