Logo
ManuelSchoebel

Meteor.js Package Only App Structure With Mediator Pattern

There are some people out there saying that you should not use the client folder at all but only use packages for everything. I first read about this some time ago from matb33 in his post "Meteor project structure - the way forward". And not long ago there was a really nice presentation about larger meteor applications also pointing out, that this should be the way to go here and especially at this slides here.

So I've read about this twice and that's the point I had to try it, with some Glenfarclas in my glas. The thing with this packages is, that you can handle the dependencies inside the package itself and you have a better load order control. So let's look at this steps first and I will also show you the two example packages I use for this post. They are really simple and stupid but really are just to try this stuff and not to make that much sense.

We have one package called 'MyModule' and one package called 'Notify'. Let's look at the package.js file of 'MyModule'. This file describes the package and also sets the dependencies aaaand the load order, too.

    Package.describe({
      summary: "Some example package or module or something that has logic"
    });
 
    Package.on_use(function (api) {
      var both = ['client', 'server'];
 
      api.use('coffeescript', both);
 
      api.use('templating', 'client');
      api.use('handlebars', 'client');
      api.use('underscore', 'client');
      api.use('notify', 'client');
 
      api.add_files('my_module_server.coffee', 'server');
      api.add_files("my_module.html", "client");
      api.add_files("my_module.coffee", "client");
    });

As we can see there are some simple dependencies like this package needs handlebars on the client and underscore and the notify package and so on. Also it loads 'my_module.html' before 'my_module.coffee' simply because it is added to the api one line before. The package does only one thing. There is a button and if you click it, it calls a function from the 'Notify' package:

    Template.myModule.events({
      'click .show-err': function(evt, tpl) {
        Notify.setError('There is an error');
      }
    });

Okay, so this is basically how you can work in an app only with packages. But if you start working with so much packages, I think it would be even better if they are completely independent as well. Meaning: 'MyModule' should not have the dependency for 'Notify' but of course could somehow make 'Notify' set an error.

##Thanks Backbone Framework Chaplin Maybe you have heard of this really great architecture for backbone apps called Chaplin. I once used it and one thing I really liked was the implementaion of the 'mediator pattern'. This pattern simply says:

Define an object that encapsulates how a set of objects interact

For our two packages we want that they do not interact directly, but use an object that encapsulates their interaction. This object is a new package I called 'Mediator'. What you can do is simply to subscribe to an event and you can publish this event, adding data if you want to.

This is how our very simple Mediator object looks like (js2coffee.org if you like):

    var Mediator;
 
    Mediator = {
      channels: {},
      publish: function(name) {
        this.channels[name].args = _.toArray(arguments);
        this.channels[name].deps.changed();
      },
      subscribe: function(name) {
        if (!this.channels[name]) {
          this.channels[name] = {
            deps: new Deps.Dependency,
            args: null
          };
        }
        this.channels[name].deps.depend();
        this.channels[name].args;
      }
    };

The Mediator has two functions, one to subscribe to a channel and one to publish to a channel. We now want our 'Notify' package to subscribe to the channel called 'show_error' and if someone publishes an error to that channel, 'Notify' should show it. And this would look like this:

    var Notify;
 
    Meteor.startup(function() {
      return Deps.autorun(function() {
        var args;
        args = Mediator.subscribe('show_error');
        if (args) {
          Notify.setError(args[1]);
        }
      });
    });
 
    Notify = {
      setError: function(err) {
        console.log('Notify.setError', err);
      }
    };

On startup we can be sure, that the Mediator object exists and we do not have to worry about load orders here. We subscribe to 'show_error' in a reactive way because our Mediator is not much more than a kind of reactive dict, e.g. like the 'Session' object. I will show you later why not to use the 'Session' object.

Next we change how the click event of our 'MyModule' package works:

    Template.myModule.events({
      'click .show-err': function(evt, tpl) {
        Mediator.publish('show_error', 'There is an error');
      }
    });

Now we only publish the event and we do not use the 'Notify' package directly. Okay, you can argue that we only changed one dependency for another... BUT: What if more packages would listen to this event? You could have as many packages react to this event without them even knowing of each other. Also, what if you see a new shiny 'Notify' package that is far better than yours?! You can simply delete your old one and make the new one listen to the channel. You would not have to change a thing in your 'MyModule' package. Isn't this awesome?!

##Using the Mediator on the Server Now we will see why I did not use the Session object, that is because you cannot use it on the server. The next thing I want to do is the following:

I have three buttons all publishing the 'show_error' event. The difference is that one should only affect the client, one only the server and the third should affect both. Also the 'Notify' package should be able to handle client and server side subscriptions and even react differently. E.g. on the client it should show the error on the UI and on the server it should be logged in the database.

The events for the three buttons:

    var msg;
    msg = 'There is something wrong';
 
    Template.myModule.events({
      'click .show-err-both': function(evt, tpl) {
        Meteor.call('myModuleMethod', msg);
      },
      'click .show-err-client': function(evt, tpl) {
        Mediator.publish('show_error', msg);
      },
      'click .show-err-server': function(evt, tpl) {
        Meteor.call('myModuleMethodServer', msg);
      }
    });

Affecting server and client we call a Meteor.method that is available on the server and the client. This method publishes via the 'Mediator':

    Meteor.methods({
      myModuleMethod: function(msg) {
        Mediator.publish('show_error', msg);
      }
    });

Publishing directly on the client in the second event handler, only affects the client version of the 'Notify' Object. And the third event handler calls a Meteor.method that is only available on the server.

Now we edit the 'Notify' object. We create one setError function that is available on the client:

    Notify.setError = function(err) {
      console.log('CLIENT Notify.setError', err);
    };

One for the server:

    Notify.setError = function(err) {
      console.log('SERVER Notify.setError', err);
    };     

And the base 'Notify' object that is available on the client AND the server. Here we can also subscribe to the 'set_error' event. And we have to do this only once for the client and the server as well:

    Meteor.startup(function() {
      return Deps.autorun(function() {
        var args;
        args = Mediator.subscribe('show_error');
        if (args) {
          Notify.setError(args[1]);
        }
      });
    });
 
    this.Notify = {
      setError: function(err) {}
    };

This is all there is to do. We now have two completly decoupled packages, able to interact with each other and are easily to replace. And we can use this on the server and client as well and seperate the reaction to a mediator publication, too.

You can also clone a working project from github.

Sooo, what do you think? Is this only the Glenfarclas talking or does this make sense to you?

©️ 2024 Digitale Kumpel GmbH. All rights reserved.