WORKSHOP

Get started with Meteor

and

Build a chat application (Slack style)

 

 

The complete source code is available in steps on

github.com/studiointeract/meteor-slack

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

  1. Clone the sample application


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

  1. Stop the application

     
  2. Add iron:router

     
  3. Start the application again

     
  4. Check it out in the browser

http://localhost:3000

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

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

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

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

  1. Create a link to a channel from home



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

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



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

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

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

  1. Add a helper that finds messages for the current 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});
  },

  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

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

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

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



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

../channel/channel.html

Publish CHANNELS

  1. Remove autopublish (we don't want to publish everything always).

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

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

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


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

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

  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);
  });
});

Template.channel.helpers({
...

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');
});

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

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

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

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

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

  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 {
    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!

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

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