An Introduction to Meteor.js

Kevin Harvey | @kevinharvey

Agenda

  1. Introductions
  2. Conceptual Overview
  3. Demo

Who is this guy?

  • Senior Systems Engineer at SmileCareClub
  • Building the web since 2004

Who are you guys?

What is Meteor?

Full-stack web framework

  • Pure-Javascript
  • Same API on the front and back ends

What is it like?

How is it different?

Requests vs. Subscription

Single-Page Apps

... unless you install a router package

Database Everywhere

  • MongoDB on the server
  • MiniMongo in the browser
  • Meteor keeps it all in sync

Reactivity

This is some serious hocus pocus

  1. Client UI and database are updated first
  2. Changes sent to server
  3. Issues are resolved after the fact

Latency Compensation

Continuing the wizadry theme

  1. Changes from the server are updated automatically
  2. Models are simulated locally

Framework + Package Manager


$ meteor create myapp
$ meteor add accounts-ui accounts-password
$ meteor
					

Development Happens Fast

  1. Hardly any boilerplate
  2. Tons of stuff for free
  3. Context switching reduced

Demo

questionator.meteor.com

Installing Meteor


$ curl https://install.meteor.com | /bin/sh
					

Starting a Project


$ meteor create the-questionator
$ cd the-questionator
$ meteor
[[[[[ ~/dev/the-questionator ]]]]]            

=> Started proxy.                             
=> Started MongoDB.                           
=> Started your app.                          

=> App running at: http://localhost:3000/
					

Initial Project Structure


$ ls
.meteor
the-questionator.css
the-questionator.html
the-questionator.js
					

the-questionator.html


<head>
  <title>the-questionator</title>
</head>

<body>
  <h1>Welcome to Meteor!</h1>

  {{> hello}}
</body>

<template name="hello">
  <button>Click Me</button>
  <p>You've pressed the button {{counter}} times.</p>
</template>
					

the-questionator.js


if (Meteor.isClient) {
  // counter starts at 0
  Session.setDefault("counter", 0);

  Template.hello.helpers({
    counter: function () {
      return Session.get("counter");
    }
  });

  Template.hello.events({
    'click button': function () {
      // increment the counter when button is clicked
      Session.set("counter", Session.get("counter") + 1);
    }
  });
}

if (Meteor.isServer) {
  Meteor.startup(function () {
    // code to run on server at startup
  });
}
					

Installing Bootstrap


$ meteor add twbs:bootstrap
						

Make Moar Prettier


$ the-questionator.html
...
<body>
<div class="container-fluid" 
style="padding-top:60px;">
  
  <nav role="navigation"
    class="navbar navbar-default navbar-fixed-top">
    <div class="container-fluid">
      <div class="navbar-header">
        <a class="navbar-brand" href="#">
          The Questionator
        </a>
      </div>
    </div>
  </nav>

  {{> hello}}
</div>
</body>
...
					

Even Moar Prettier!


$ the-questionator.html
...
  <form role="form">
    <div class="form-group">
      <textarea id="question"
        class="form-control" rows="2">
      </textarea>
    </div>
    <div class="form-group">
      <input class="btn btn-default"
        type="submit" value="Ask away" />
    </div>
  </form>

  {{> hello}}
</div>
</body>
...
					

Template Inclusion


$ the-questionator.html
...
    {{> hello}}
...
<template name="hello">
  <button>Click Me</button>
  <p>You've pressed the button {{counter}} times.</p>
</template>
					

Extending the Template


$ the-questionator.html
...
  {{> questionsList}}
</div>
</body>

<template name="questionsList">
<div id="questions">
  {{#each questions}}
    <hr />
    <h6>{{text}} <small>Votes: {{votes}}</small></h6>
    <a class="btn btn-default btn-xs vote-up">up</a>
    <a class="btn btn-default btn-xs vote-down">down</a>
  {{/each}}    
</div>
<hr />
</template>
					

Hardcoded Data


$ the-questionator.js
if (Meteor.isClient){
var questionsData = [
  {
    text: 'Why does the sun shine?',
    votes: 0
  },
  {
    text: 'If you were a hot dog, and you'
   + 'were starving to death, would you eat yourself?',
    votes: 0
  },
  {
    text: 'What is the airspeed velocity'
  	+ ' of an unladen swallow?',
    votes: 0
  }
];

Template.questionsList.helpers({
  questions: questionsData
});
}
					

Creating a Collection


$ the-questionator.js
Questions = new Mongo.Collection('questions');
...
          

Available in Client and Server

Terminal


$ meteor mongo
MongoDB shell version: 2.4.9
connecting to: 127.0.0.1:3001/meteor
Welcome to the MongoDB shell.
meteor:PRIMARY> db.questions.find()
          

Browser Console


> Questions.find()
> LocalCollection.Cursor {collection: LocalCollection, sorter: null, _selectorId: undefined...}
          

Update the Helper


$ the-questionator.js
Questions = new Mongo.Collection('questions');

if (Meteor.isClient){
Template.questionsList.helpers({
  questions: Questions.find(),
});
}
					

Preload Data


$ the-questionator.js

if (Meteor.isServer) {
if (Questions.find().count() === 0) {
  Questions.insert({
    text: 'Why does the sun shine?',
    votes: 0
  });

  Questions.insert({
    text: 'If you were a hot dog, and you were...',
    votes: 0 
  });

  Questions.insert({
    text: 'What is the airspeed velocity of...',
    votes: 0 
  });
}
}
					

Remove Autopublish


$ meteor remove autopublish
          

Publishing Data


if (Meteor.isClient) {
Meteor.subscribe('questions');
...
}

if (Meteor.isServer) {
...
Meteor.publish('questions', function() {
  return Questions.find();
});
}
          

Sorting Results


Template.questionsList.helpers({
questions: Questions.find({}, {sort: {votes: -1}}), 
});
          

Event Handling


if (Meteor.isClient) {
...  
Template.questionsList.events({
  'click .vote-up': function(e) {
    e.preventDefault();
    Meteor.call('upvote', this._id);
  },
  
  'click .vote-down': function(e) {
    e.preventDefault();
    Meteor.call('downvote', this._id);
  }
});
}
          

Methods


Meteor.methods({
upvote: function(questionId) {
  var question = Questions.findOne(questionId);
  Questions.update(
    questionId,
    { $set: { votes: question.votes + 1 }}
  );
},

downvote: function(questionId) {
  var question = Questions.findOne(questionId);
  Questions.update(
    questionId,
    { $set: { votes: question.votes - 1 }}
  );
}
});
          

Template Refactor


  {{> questionForm}}
...
<template name="questionForm">
<form role="form">
  <div class="form-group">
    <textarea id="question"
      class="form-control" rows="2"></textarea>
  </div>
  <div class="form-group">
    <input class="btn btn-primary"
      type="submit" value="Ask away" />
  </div>
</form>
</template>
          

Rinse and Repeat


if (Meteor.isClient) {
...
Template.questionForm.events({
  'submit form': function(e) {
    Questions.insert({
      'text': $(e.target).find('#question').val(),
      'votes': 0
    });
  }
});
}
          

Accounts


$ meteor add accounts-twitter
$ meteor add ian:accounts-ui-bootstrap-3
          

{{> loginButtons}}
          

Adding {{> loginButtons}}


<div class="navbar-header">
  <a class="navbar-brand" href="#">The Questionator</a>
  
  <button type="button" class="navbar-toggle collapsed" 
    data-toggle="collapse" data-target="#navigation">
    <span class="sr-only">Toggle navigation</span>
    <span class="icon-bar"></span>
    <span class="icon-bar"></span>
    <span class="icon-bar"></span>
  </button>
</div>

<div class="collapse navbar-collapse" id="navigation">
  <ul class="nav navbar-nav navbar-right">
    {{> loginButtons}}
  </ul> 
</div>
          

Configure Twitter Login

Instructions included in the app

Restricting Access


Questions.allow({
insert: function(userId, doc) {
  return !! userId;
}
});

// .. and the methods we added earlier
          

Removing "Insecure"


$ meteor remove insecure
          

End of Demo

Deployment

Deploy to meteor.com


$ meteor deploy questionator.meteor.com
Deploying to http://questionator.meteor.com
          

Modulus


$ npm install -g modulus
$ modulus login
$ modulus project create
          

Meteor Up


$ npm install -g mup
$ mkdir ~/deploy-the-questionator
$ cd ~/deploy-the-questionator
$ mup init
          

Behind the curtain...

Web Sockets

A departure from the request/response loop

https://developer.mozilla.org/en-US/docs/WebSockets

Distributed Data Protocol (DDP)

Publication & Subscription

http://2012.realtimeconf.com/video/matt-debergalis

Oplog Tailing

Getting realtime updates from MongoDB

https://github.com/meteor/meteor/wiki/Oplog-Observe-Driver

Tracker

Hot code pushes, reactions to database changes, etc.

https://github.com/meteor/meteor/tree/devel/packages/tracker

Resources

Fin

Thanks

Kevin Harvey | @kevinharvey