Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

Jason McIver


A Javascript developer excited about Meteor, Mongo, React and Node.

Publish your own Meteor package

Intro

We will walk through creating a simple Meteor package that manages an Icecream truck. Mostly how to create, structure and build a reusable Meteor package. Touching on OOP.


Setup

I generally keep my package development and tests in a seperate folder than my other Meteor projects.

|meteorpackages/
               |icecream/
|meteortestapps/
               |testicecream/

mkdir meteortestapps  
mkdir meteorpackages  
cd meteorpackages  
meteor create --package <your meteor username>:icecream  

Create testapp

Lets create a new Meteor app just for testing our new package:

cd ../meteortestapps  
meteor create testicecream; cd testicecream  
mkdir -p packages  
mrt link-package <location of your new package>  

If you need to install mrt

sudo npm install -g meteorite  

Add your package locally

meteor add <your meteor username>:icecream  

Run test app

meteor  

Writing your package

A Meteor package can be written like a Meteor app with Blaze, Template events and helpers like you do normally.
That is one approach but to make your package modular and re-usable you should be putting your business logic in a seperate object.
This makes it easy to mock (or switch out) components for testing, provided access to you package logic between Templates, to the outer Meteor app and other 3rd party Meteor packages.
Setup your package like so:

|meteorpackages/
               |icecream/
                    |- icecream.js
                    |- icecream.html
                    |lib/truck.js

I choose the name 'truck' simply for the fact that it will contain business logic for an ice cream truck, you could extend this with multiple instances of truck for many ice cream trucks. create yours with a single javascript object like so:

truck = {  
    options: {
        colour: ''
    },
    onSoldOut: function(icecream){

    },
    sellIcecream: function(icecream_id){

    },
    getIcecreams: function(){

    }
}

We don't use var in front of truck as it needs to be accessible globally.

This gives us a blueprint of our icecream truck. We can set all of the parameters of the truck here and make it acessible to ourselves and other developers simply like so truck.onSoldOut() or truck.getIcecreams().

Open up icecream.js and add the following code:

Icecreams = new Meteor.Collection('icecreams');  
Meteor.methods({  
    sellIcecream: function(icecream_id){
        return Icecreams.update({'_id': icecream_id}, {$inc:{'quantity':-1}});
    },
    addIcecream: function(name, quantity){
        var newIcecream = {
            name: name,
            quantity: quantity
        };
        return Icecreams.insert(newIcecream)
    }
})

if(Meteor.isClient){  
    Template.icecreams.helpers({
        icecreams: function(){
            return truck.getIcecreams();
        }
    })
}

Now we have database manipulation on the server side. Any attempts from the user to spoof the database from his browser will only effect his local copy, then quickly reverted as it fails on the server.

The client can get a list of ice creams from the truck by calling truck.getIcecreams(). Keep in mind we are not using Pub/Sub in this example but you would in in the real world.

To list our ice creams create icecream.html and add the following template:

<template name="icecreams">  
    <b>Sold out: {{notify.name}}</b>

    {{#each icecreams}}
        <li>{{name}} {{quantity}}</li>
    {{else}}
        <p>No Ice creams</p>
    {{/each}}
</template>  

Update your package.js file to include templating and your files:

api.use([  
    'templating']
);
  api.addFiles([
    'icecream.html',
    'icecream.js',
    'lib/truck.js'
    ],
     ['client', 'server']);

  api.export('truck', 'client');
  });

I'll explain what these do further down.

And lastly call your template from your testicecream App
testicecream.html

{{> icecreams}}

Point your browser to http://localhost:3000 and you'll notice there are not icecreams, so lets add some.
Open the javascript console in your browser and enter the following

Meteor.call('addIcecream', 'Bubblegum', 3)  

Add a few more with different names and quantities. Now we have Icecreams, yum! But we can't eat them so lets sell them with a button.

Open up icecream.html and change the template like so:

<template name="icecreams">  
    <b>Sold out: {{notify.name}}</b>

    {{#each icecreams}}
        <li>{{name}} {{quantity}} <button class="sellIcecream" id="{{_id}}">Sell</button></li>
    {{else}}
        <p>No Icecreams</p>
    {{/each}}
</template>  

To capture the sell button event create an icecream events function in icecream.js

    Template.icecreams.events({
        "click .sellIcecream": function(event){
            truck.sellIcecream(event.target.id)
        }
    })

Put the above code after the icecream helper. This will carry the value in the button id field into the truck objects sellIcecream method.

Lets add the handling code inside the truck object.

Meteor.call('sellIcecream', icecream_id)  

Now when you press sell we should see the quantity reduce each time.

But wait there is a problem. It keeps reducing down past 0 and we get a negative quantity.
Fix this by checking what the quantity is before we reduce the amount. Replace the content of our Meteor method with this code.

        var icecream = Icecreams.findOne({'_id': icecream_id});
        if(icecream.quantity > 0){
            return Icecreams.update({'_id': icecream_id}, {$inc:{'quantity':-1}});
        }

findOne doesn't provide a cursor like find(). we can access the data right away.
If all went well we can not longer sell more than we have.

We want to be notified when we sell out of icecreams so lets create an event like system. First we need some feedback after we sell an icecream to know how many are left. Change the Meteor method like so:

    sellIcecream: function(icecream_id){
        var icecream = Icecreams.findOne({'_id': icecream_id});
        if(icecream.quantity > 0){
            return Icecreams.update({'_id': icecream_id}, {$inc:{'quantity':-1}});
        }
        return Icecreams.findOne({'_id': icecream_id});

    },

    },

We can add a callback function as a parameter into the sellIcecream method that will contain some business logic. If you return a value from a Meteor.methods you can expect the result in your callback handler as the second param like so:

    sellIcecream: function(icecream_id){
        var self = this;
        Meteor.call('sellIcecream', icecream_id, function(err, icecream){
            if(icecream.quantity <= 0){
                self._onSoldOut.set(icecream)
            }
        })
    },

What is happening here is the function (callback) passed into the method call will be executed once it has completed. Inside the callback we have access to err (error) and res (result). res is what we returned from the find query inside the method.

If you want to return an err from your Method use: throw new Meteor.Error('invalid-icecream', 'Please select an Icecream.');

Finally the event. ReactiveVar is a Meteor package which provides reactiveness to variables we assign. Anything associated with the variable will be called and executed. It applies two handy methods .set() and .get().

To handle our event we need two variables. _onSoldOut acts like a private var that we assign as a ReactiveVar, onSoldOut is a public association for external use. It is a function that queries the private ReactiveVar with get().

Your onSoldOut var should look like this:

    _onSoldOut: new ReactiveVar({}),
    onSoldOut: function(){
        return this._onSoldOut.get()
    },

Note we have added: _onSoldOut: new ReactiveVar({}),

Now anywhere inside our package logic (truck.js) we want to update the event or be updated when a change occurres we just need call: this._onSoldOut.set(obj) or get() and outside our package use truck.onSoldOut.set() or get().


Create a Git Repo

Goto Github and create your repo

Then with the following commit and push your code into your new repo.

git init  
git add -A  
git commit -m 'first commit'  
git remote add origin <your github repo address here>  
git pull origin master  
git push origin master  

If vim appears you can easily quit with esc then ':' then 'q!' then enter.


package.js

Fill in the fields, Git must be filled in with your repo path as it will be used on Atmospherejs.com and Meteor CLI to download your package.

api.use() is for adding your dependencies of other packages.

  api.use([
    'templating',
    'reactive-var',
    'session'
  ], 'client');

Notice these API methods take a second argument as either a string or array to make available to client and/or server.

Any files in your package need to be included with either or both client or server

api.addFile(['templates.html'],['client']);  

'client' means that your template will be availble to the client. Anything you want available to the server you can use 'sever' or both ['client', 'server'] for things like Meteor.methods();

To make the truck object accessible outside of your package you need to export it like so:

api.export('truck',['client','server']);  

Now we have access to truck and it's methods, properties and events anywhere in your Meteor app.

truck.getIcecreamTrucks('Melbourne');  

Fill in your README

A good package always comes with handy readme. Include things like how to install and use your package. Descriptions of API call are enough but examples will make it very easy for people to use.

Don't forget links and documentation to any 3rd party libs used.

Publish to Atmosphere

meteor publish --create  

Future updates

Because we are good developers and fix bugs in our code when we can. To update your package, first increment your version in package.js as you see fit. Commit and Push your changes to the repo and then update atmosphere again with:

meteor publish  

FAQ

Q

While publishing the package:  
error: Longform package description is too long. Meteor uses the section of the Markdown documentation file  
between the first and second headings. That section must be less than 1500 characters long.  

A

In your README file try leaving a space in the headings between the hashes: # Heading. not #Heading.