Instructions for building this Meteor CRUD app, a basic app for teaching Meteor basics.
- Run
git clone [email protected]:andersr/meteor_crud
from your terminal, followed bycd 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.
- Delete the default files: app.html, app.js, app.css (
eg using rm app.*
) - 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.) - Learn more about recommended Meteor file structure: http://meteortips.com/first-meteor-tutorial/structure/
- Run
git checkout step1-file-structure
for the finalized version of this step.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
This is such a great tutorial!