What's the best way to attach behavior to a Meteor Collection?
Asked Answered
S

6

22

In Meteor, when you retrieve a record from a database, it's only a record. So if I have a collection called Dogs, a dog might have fur: 'brown' or breath: 'stinky', but it doesn't have a bark() method.

Obviously, I could create some functions that expect a dog as an argument and then perform operations on that dog. I could even encapsulate all these functions into a single constructor. I'm not crazy about this approach, but if someone's got a clean and sensible way to do it, I'm all ears.

Another thought I had was to wrap the dog in a Backbone.Model. This could be interesting, as fetch and save could be redefined to do find and insert or update, and you can define all your behavior there as well, but I've read that this type of thing is generally discouraged.

Is there a right way to do it? Is there a Meteor.Model officially in the works? How are others solving this problem?

Edit

For those coming to this question over a year after the accepted answer: At the time of this edit I am using Exygy's minimongoid mrt package, which has some improvements to haihappen's version that's mentioned in the blog post linked to by the accepted answer.

I'm currently contributing to the repository to make the result sets more relation-like. Hopefully others get use out of it and feel inclined to contribute helpful functionality.

Edit

Another answer suggested using the transform property when creating the collection. While I'm definitely preferring something that I don't really need to build out myself, this feature adds a lot of possibilities, and I would hope that any teams that are working on an ORM for Meteor would take advantage of this at the core.

Here's a blog post explaining how to use the transform property.

Also, minimongoid is now available as a Meteor package, and I am still using it. It has support for validation and for declaring relationships. I've added some functionality to this package as well, so if a board has many pieces, board.pieces().create(attributes) will persist a new piece record with the given attributes and will automatically associate with the board. Of the various solutions I've seen, this seems to be the most comprehensive.

Sher answered 21/2, 2013 at 16:22 Comment(2)
Worth mentioning is the collection-behaviors package.Fiedler
@DanDascalescu, thanks for the link! It doesn't quite fit what I'm looking for but would be very useful to me in other ways. Much appreciated!Sher
K
3

While there might be an official model system in the works there are some things you can do now:

There is one by Mario Uhler which is activerecord like and quite nice, in coffeescript: https://coderwall.com/p/_q9b1w

There is also a community package made by Tom Coleman thats very helpful with models: https://github.com/tmeasday/meteor-models, you might need meteorite to add it as a package.

And of course as you suggested Backbone. I personally use js prototypes but not everyone may be comfy with them, I just used them so its easy to transition when meteor's model system is out, its also easy to share between the client and server without too many packages to add.

Knighterrantry answered 21/2, 2013 at 16:52 Comment(5)
Thanks for your response. If it's not too much trouble, can you provide a simple example of how you leverage js prototypes to solve this problem?Sher
I will update this in a while, I just need to clean up the prototype a bitKnighterrantry
I decided to go with Minimongoid, it fits the bill quite well :-)Sher
I don't understand why a Collection's transform argument does not suffice.Grind
Thanks @the0ther and sorry I didn't see this sooner. I've updated the OP with some notes on transform, definitely worth the call out.Sher
B
28

You can use the transform parameter in the Collection to overload the object with custom functions

var Dogs = new Meteor.Collection("dogs", 
{
    transform:function(entry)
    {
        entry.bark = function(){ console.log(this.barkSound);};
        return entry;
    }
});

Then:

var aDogID = new Dogs.insert({barkSound: "ruff"})
Dogs.find(aDogID).bark(); // "ruff"

Bonus: If for any reason you would like to use a similar concept as proposed by Andrew Ferk, just use the _.defaults(object, *defaults) function.

var defaults = {
             barkSound: "ruff",
             bark: function() {
                        console.log(this.barkSound);
                    }
            }

Dogs = new Meteor.Collection("dogs",
        {
            transform: function(object) {
                return _.defaults(object, defaults);
            }
        });
Blackman answered 4/2, 2014 at 7:39 Comment(3)
This looks like the right, official way to do it. I'm gonna go with this.Grind
This is an awesome solution, thanks!!! Completely changes my meteor code now. How did I miss this in the docs???Hachman
Post on the subject with more examples: okgrow.com/posts/2014/05/19/meteor-transformDauphin
R
4

This is a start at overriding Meteor.Collection to support methods on objects.

Meteor.Kollection = Meteor.Collection;
Meteor.Kollection.extend = function(constructor) {
  var parent = this;
  var child = function() {
    Meteor.Kollection.apply(this, arguments);
    constructor.apply(this, arguments);
  };

  _.extend(child, parent);

  function __proto__() { this.constructor = child; };
  __proto__.prototype = parent.prototype;
  child.prototype = new __proto__;

  return child;
};

Meteor.Collection = Meteor.Kollection.extend(function(name, options) {
  if (options && options.defaults) {
    this.defaults = options.defaults;
  }
});

Meteor.Collection.prototype.applyDefaults = function(attrs) {
  return _.defaults(attrs, this.defaults);
};

Meteor.Collection.prototype.create = function(attrs) {
  if (typeof attrs !== "object") attrs = {};
  return this.applyDefaults(attrs);
};

Meteor.Collection.prototype.findOne = function(selector, options) {
  var object = Meteor.Kollection.prototype.findOne.apply(this, arguments);
  return this.applyDefaults(object);
};

You may notice the new collection.create method, and that collection.findOne has been overridden. I imagine all collection.* methods will need to be overridden, but this is a start.

So what can you do with this?

var Dogs = new Meteor.Collection("dogs", { defaults: {
  barkSound: "ruff",
  bark: function() {
    console.log(this.barkSound);
  }
}});

if (Meteor.isClient) {
  var regularDog = Dogs.create();
  regularDog.bark(); // ruff

  var smallDog = Dogs.create({
    barkSound: "yip"
  });
  smallDog.bark(); // yip

  Dogs.insert(smallDog, function(error, id) {
    Dogs.findOne(id).bark(); // yip
  });
});

I'm not exactly sure how this happens, but any functions in an object are removed when they are inserted. Therefore, we can directly apply the methods to the object. First, you create your collection by passing in an object with the defaults property. This property can include properties or methods. To create a new object for a given collection, use collection.create(attrs), where attrs is an option argument that includes additional or overridden properties and methods.

Rosa answered 21/2, 2013 at 21:56 Comment(0)
D
4

Astronomy is a new answer to this old question of Meteor models. The author has a lot more features planned that are in the works and the support of other prominent Meteor package authors. The only downside is perhaps it's a bit bleeding edge.

Astronomy is also highly modularized, a prime example for building in a meteor packaged/modularized style.

Dauphin answered 20/8, 2015 at 1:53 Comment(0)
K
3

While there might be an official model system in the works there are some things you can do now:

There is one by Mario Uhler which is activerecord like and quite nice, in coffeescript: https://coderwall.com/p/_q9b1w

There is also a community package made by Tom Coleman thats very helpful with models: https://github.com/tmeasday/meteor-models, you might need meteorite to add it as a package.

And of course as you suggested Backbone. I personally use js prototypes but not everyone may be comfy with them, I just used them so its easy to transition when meteor's model system is out, its also easy to share between the client and server without too many packages to add.

Knighterrantry answered 21/2, 2013 at 16:52 Comment(5)
Thanks for your response. If it's not too much trouble, can you provide a simple example of how you leverage js prototypes to solve this problem?Sher
I will update this in a while, I just need to clean up the prototype a bitKnighterrantry
I decided to go with Minimongoid, it fits the bill quite well :-)Sher
I don't understand why a Collection's transform argument does not suffice.Grind
Thanks @the0ther and sorry I didn't see this sooner. I've updated the OP with some notes on transform, definitely worth the call out.Sher
T
2

This is a relatively old question in terms of Meteor, but I think dburles:collection-helpers fits the bill of what you wanted to achieve, by implementing what Flavien Volken suggested. Perhaps it's useful for anyone wandering in here recently.

Trophozoite answered 2/4, 2015 at 17:4 Comment(1)
Thanks, I appreciate being updated with new techniques. I'm still using minimongoid (atmospherejs.com/kaptron/minimongoid) which supports declarations like has_one and belongs_to.Sher
M
0

Additions to @Flavien Volken's answer you can pass arguments to the method you've added to the collection.

Car = new Mongo.Collection('car', {
    transform: function(entry) {
        entry.is_watched = function(userId) {
            var is_watched = false;
            if (entry.watchList) {
                for (var i in entry.watchList) {
                    if (entry.watchList[i].userId == userId) {
                        is_watched = true;
                        break;
                    }
                }
            }
            return is_watched;
        };
        return entry;
    }
});

In template (passing id of the logged in user):

{{# if is_watched currentUser._id }}
    Watched
{{ else }}
    Not watched
{{/ if }}
Matheson answered 21/9, 2015 at 5:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.