Skip to content

Instantly share code, notes, and snippets.

@andersr
Last active October 6, 2015 03:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save andersr/169036aaaab32e479870 to your computer and use it in GitHub Desktop.
Save andersr/169036aaaab32e479870 to your computer and use it in GitHub Desktop.

Instructions for building this Meteor CRUD app, a basic app for teaching Meteor basics.

Step 0: Create app/basic dev setup

  • Run git clone git@github.com:andersr/meteor_crud from your terminal, followed by cd meteor_crud
  • Open the current directory in a text editor (eg using edit .)
  • (If you were creating this app from scratch, you would simply have typed meteor create app in this directory.)
  • Cd into the "app" directory and run meteor from the command line.
  • Open a browser window and enter the URL: http://localhost:3000
  • Leave this window open while building the app. Meteor will auto-refresh as you make changes.
  • Why have your app in an "app" sub-directory? Making your app a sub-directory of your working directory allows for making external files, such as config files, build files, etc, be part of your repo without being mixed in with actual app files.

Step 1: App File Structure

  1. Delete the default files: app.html, app.js, app.css (eg using rm app.*)
  2. Add client, lib, and server directories: mkdir client lib server (Files in 'client' will only run on the client, files in 'server' will only run on the server, while files in 'lib' will be loaded first and run on both the server and client.)
  3. Learn more about recommended Meteor file structure: http://meteortips.com/first-meteor-tutorial/structure/
  4. Run git checkout step1-file-structure for the finalized version of this step.

Step 2: Basic Setup

  • Add a meteor bootstrap package: meteor add twbs:bootstrap (Optional: add a Sass package, which we'll use later: meteor add fourseven:scss)
  • In client/templates/, add an index page with <head> and <body> blocks only, containing some basic markup for layout. This will be the core app page.
<head>
  <title>Meteor CRUD</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
  <nav class="navbar">
    <div class="container">
      <div class="navbar-header">
        <h2 class="navbar-brand">Meteor CRUD</h2>
      </div>
    </div>
  </nav>
  <div class="container">
    (Main page content goes here)
  </div>
</body>
  • (Optional) Add a scss.json file to the root directory with: { "enableAutoprefixer": true}
  • Run ```git checkout step2-basic-setup`` for the finalized version of this step.

Step 3: Create an Items List

  • In /lib/collections, add an items.js file with a 'items' Mongo collection:
Items = new Mongo.Collection('items');
  • Open your browser console (eg using Option + Cmd + J) and insert some items into your new collection: Items.insert({title:"My first item", createdAt: Date()}); (If all goes well, Mongo should return the id of the newly created item.
  • In /client/templates/itemsList, add a itemList.js file containing a itemsList helper, which will reactively find all items in the 'items' collection:
Template.itemsList.helpers({
  allItems: function () {
    return Items.find();
  }
});
  • Add the following view template files (we need to break out each individual item instance into a "singleItem" template to support editing of an item in a later step):

/client/templates/itemsList/itemsList.html:

<template name="itemsList">
  <ul class="list-group">
    {{#each allItems}}
       {{> singleItem}}
    {{/each}}
  </ul>
</template>

/client/templates/singleItem/singleItem.html:

<template name="singleItem">
<li class="list-group-item">
  {{title}}
</li>
</template>
  • Invoke your "itemsList" template in the main index file to display its contents in your app:

/client/templates/index.html

...
  <div class="container">
    {{> itemsList}}
  </div>
...
  • Run git checkout step3-items-list for the finalized version of this step.

Step 4: Add a form for adding items

  • Add the following form:

/client/templates/addItem/addItem.html

<template name="addItem">
  <li class="list-group-item">
     <form class="add-item-form">
       <input type="text" class="item-title form-control" placeholder="Add item...">
    </form>
  </li>
</template>
  • Insert the form into the itemsList template:
...
  <ul class="list-group">
    {{> addItem}}
    {{#each allItems}}
...
  • Add an event handler for the addItems form:
Template.addItem.events({
  "submit .add-item-form": function(event, template){
    event.preventDefault();

    var itemTitle = template.find('.add-item-form .item-title').value;

    Items.insert({
      title: itemTitle,
      createdAt: Date()
    });

    $('.add-item-form')[0].reset();
  }
});
  • You should now be able to add items using the form. The itemsList will update reactively as new items are added.
  • Update the sorting of the items list to be reverse chronological, so that newest items appear on top:

/client/template/itemsList/itemsList.js

...
    return Items.find({}, {sort: {createdAt: -1}});
...
  • Run git checkout step4-add-items for the finalized version of this step.

Step 5: Allow for deleting items (components)

  • We'll implement this feature as a component so that it can be re-used in multiple contexts.
  • We'll need to be able to reference collections dynamically to do this. Install this package to support that: meteor add dburles:mongo-collection-instances
  • Create a deleteBtn component:

/client/templates/components/deleteBtn/deleteBtn.html

<template name="deleteBtn">
  <button type="button" class="btn btn-default btn-xs delete"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></button>
</template>
  • Add an event handler for deleteBtn:

/client/templates/components/deleteBtn/deleteBtn.js

Template.deleteBtn.events({
  'click .delete': function () {

    var collection = this.collection;
    var id = this.currentItem._id;
    var confirmDelete = confirm("Really delete this?");

    if (confirmDelete) {
      Mongo.Collection.get(collection).remove({ _id: id });
    };
  }
});
  • Invoke the deleteBtn component in the singleItem template and pass in the necessary instance arguments:

/client/templates/singleItem/singleItem.html

...
  {{title}} <span class="pull-right">{{> deleteBtn currentItem=this collection="items"}}</span>
...
  • Run git checkout step5-delete-items for the finalized version of this step.

Step 6: Allow for editing items (reactive variables)

  • This feature will be implemented by reactively toggling each item between an edit and view state.
  • We will need Reactive Variables to achieve this: meteor add reactive-var
  • Update the singleItem template to include a form that displays conditionally:
<template name="singleItem">
{{#if editing}}
<li class="list-group-item">
  <form class="update-item two-col-row">
    <input type="text" class="form-control item-title main-content" placeholder="Update..." value="{{title}}">
     <span class="secondary-content"><button type="button" class="btn btn-default btn-xs cancel-edit">Cancel</button></span>
  </form>
  </li>
  {{else}}
  <li class="list-group-item edit-item clickable">
    {{title}} <span class="pull-right">{{> deleteBtn currentItem=this collection="items"}}</span>
  </li>
  {{/if}}
</template>
  • Set up reactively tracking individual items and their state:

/client/templates/itemsList/itemsList.js

Template.singleItem.onCreated(function(){
  var
  templateInstance               = this;
  templateInstance.currentItem   = new ReactiveVar(templateInstance.data._id),
  templateInstance.editableItem  = new ReactiveVar(""),
  templateInstance.editing       = new ReactiveVar(false)
  ;

  templateInstance.autorun(function(){
    if (templateInstance) {
      templateInstance.editing.set(
        templateInstance.currentItem.get() ===
        templateInstance.editableItem.get()
      );
    };
  });
});
  • In the same file, add a helper for reactively getting edit state and event handlers for editing:
Template.singleItem.helpers({
  editing: function () {
    return Template.instance().editing.get();
  }
});

Template.singleItem.events({

  'click .edit-item': function () {
    Template.instance().editableItem.set(this._id);
  },

  'click .cancel-edit': function () {
    Template.instance().editableItem.set("");
  },

  "submit .update-item": function(event, template){
    event.preventDefault();

    Items.update({ _id:this._id }, {
      $set : {
        title: template.find('.update-item .item-title').value
      }
    });
    $('.update-item')[0].reset();
    Template.instance().editableItem.set("");
  }

});
  • Add some css to provide ui feedback that line items are editable: /client/stylesheets/styles.scss
.two-col-row {
  display: flex;
  flex-flow: row nowrap;
  align-items:center;

  .main-content {
    flex: 1;
  }

  .secondary-content {
    text-align: right;
    width: 5em;
  }

}

.clickable {
  cursor: pointer;

  &:hover {
    background-color: #F2F2F2;
  }
}
  • Run git checkout step6-edit-items for the finalized version of this step.

Step 7: Add Publish and Subcribe

  • The current version of the app is not secure (eg it is possible to insert basically anything directly from the client.)
  • Remove the packages autopublish and insecure: meteor remove autopublish insecure
  • You'll notice that items are no longer displaying. This is because you need to explicitly publish content from the server and subscribe to it from the client.
  • Publish items from the server: /server/publications.js
Meteor.publish('items', function() {
  return Items.find(); 
});
  • Subscribe from the client: /client/lib/subscriptions.js
Meteor.subscribe('items');
  • Add db operations using Meteor.methods with some basic pattern validation:

/lib/collections/items.js

Items = new Mongo.Collection('items');

Meteor.methods({

  addItem:function(itemTitle){
    check(itemTitle, String);
    Items.insert({
      title: itemTitle,
      createdAt: Date()
    });
  },

  updateItem: function(itemAttributes){
    check(itemAttributes, {
      id:    String,
      title: String
    });

    Items.update({ _id:itemAttributes.id },
    {
      $set: {
        title: itemAttributes.title
      }
    });
  },

  removeFromCollection: function(collectionAttributes){
    check(collectionAttributes, {
      collection: String,
      id:         String
    });
    Mongo.Collection.get(collectionAttributes.collection).remove({ _id: collectionAttributes.id });
  }
});
  • In the templates, replace direct db calls with calls to the Meteor methods:

eg in /client/templates/addItems/addItems.js

Items.insert({
title: itemTitle,
createdAt: Date()
});

    Meteor.call('addItem', itemTitle, function(error, result){
      if (error){
        console.log(error.reason);
      } else {
       $('.add-item-form')[0].reset();
      }
    });
  • Run git checkout step7-pub-sub for the finalized version of this step.
@gzpgg3x
Copy link

gzpgg3x commented Oct 6, 2015

This is such a great tutorial!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment