Joost Döbken
Joost Döbken

Reputation: 4007

Meteor Reactivity Using a Collection and SubCollection (e.g. Reactive Joins)

Is it possible to implement reactivity by a sub-class with a transformed collection?

This is the example code of jamgold on the Meteor forum; the sub-class subcollection is joined to the main-class collection_name. If something changes on the collection_name collection, Meteor is in fact reactive. However, when something changes on the sub-collection subcollection, this is not reactively pushed to this publish/subscription.

Collection = new Meteor.Collection('collection_name');
if(Meteor.isServer)
{
    Meteor.publish('collection', function(query,options) {
        var self = this;
        var handler = null;
        query = query == undefined ? {} : query;
        options = options == undefined ? {} : options;
        //
        handler = Collection.find(query,options).observeChanges({
            added: function (id, doc) {
                var object = null;
                doc.object = Meteor.subcollection.findOne({_id: doc.objectId},);
                self.added('collection_name', id, doc);
            },
            changed: function (id, fields) {
                self.changed('collection_name', id, fields);
            },
            removed: function (id) {
                self.removed('collection_name', id);
            }
        });
        self.ready();
        self.onStop(function () {
            if(handler) handler.stop();
        });
    });
}

if(Meteor.isClient)
{
    Meteor.subscribe('collection');
}

Upvotes: 0

Views: 150

Answers (1)

jordanwillis
jordanwillis

Reputation: 10705

To make it reactive for the SubCollection, you would need to also observe it's changes as well. Keep in mind that this becomes very complex fast and my example only works if there is a 1 to 1 relationship between your Collection and SubCollection. You could implement something that works for a 1 to many relationship, but you will have some logic issues to address (e.g. when a doc in SubCollection changes...does that invalidate all associated Collection docs that were already published with that SubCollection doc. If so then do you emit a removed then an added to re-send them with their updated SubCollection doc, etc.).

Here is the full example.

const Collection = new Meteor.Collection('collection_name');
const SubCollection = new Meteor.Collection('sub_collection_name');

if (Meteor.isServer) {
  Meteor.publish('collection', function(query,options) {
    var self = this;
    var handler = null;
    query = query == undefined ? {} : query;
    options = options == undefined ? {} : options;

        // enable reactivity for Collection
    handler = Collection.find(query, options).observeChanges({
        added: function (id, doc) {
          // find the associated object (using it's id) and add it to the doc
          doc.object = SubCollection.findOne({_id: doc.objectId});

                    // now pass the original doc + the associated object down to client
          self.added('collection_name', id, doc);
        },
        changed: function (id, fields) {
                    // doc.object is assumed to already exist on the doc...so just notify the subscriber
                    // of the changes in Collection
          self.changed('collection_name', id, fields);
        },
        removed: function (id) {
                    // doc.object is assumed to already exist on the doc...so just notify the subscriber
                    // of the changes in Collection
          self.removed('collection_name', id);
        }
    });

        var handleSubCollectionDocChange = function(callbackThis, id) {
            // find the doc from Collection that has a reference to the new SubCollection doc
            var parentCollectionDoc = Collection.findOne({objectId: id});

            // only do something if one exists
            if (parentCollectionDoc) {
                // remove the previously published doc since the SubCollection doc changed (if it was previously published)
                self.removed('collection_name', parentCollectionDoc._id);

                // store the new SubCollection doc in Collection.object
                parentCollectionDoc.object = doc;

                // send down the Collection doc (with new SubCollection doc attached)
                self.added('collection_name', parentCollectionDoc._id, parentCollectionDoc);
            }
        };

        // enable reactivity for SubCollection
        subhandler = SubCollection.find().observeChanges({
          added: function (id, doc) {
                // find the doc from Collection that has a reference to the new SubCollection doc
                var parentCollectionDoc = Collection.findOne({objectId: id});

                // only do something if one exists
                if (parentCollectionDoc) {
                    // remove the previously published doc since the SubCollection doc changed (if it was previously published)
                    self.removed('collection_name', parentCollectionDoc._id);

                    // store the new SubCollection doc in Collection.object
                    parentCollectionDoc.object = doc;

                    // send down the Collection doc (with new SubCollection doc attached)
                self.added('collection_name', parentCollectionDoc._id, parentCollectionDoc);
                }
          },
          changed: function (id, fields) {
                // get the full SubCollection doc (since we only get the fields that actually changed)
                var doc = SubCollection.findOne({_id: id});

                // find the doc from Collection that has a reference to the new SubCollection doc
                var parentCollectionDoc = Collection.findOne({objectId: id});

                // only do something if one exists
                if (parentCollectionDoc) {
                    // remove the previously published doc since the SubCollection doc changed (if it was previously published)
                    self.removed('collection_name', parentCollectionDoc._id);

                    // store the new SubCollection doc in Collection.object
                    parentCollectionDoc.object = doc;

                    // send down the Collection doc (with new SubCollection doc attached)
                self.added('collection_name', parentCollectionDoc._id, parentCollectionDoc);
                }
          },
          removed: function (id) {
                // find the doc from Collection that has a reference to the new SubCollection doc
                var parentCollectionDoc = Collection.findOne({objectId: id});

                // only do something if one exists
                if (parentCollectionDoc) {
                    // remove the previously published doc since the SubCollection doc no longer exists (if it was previously published)
                self.removed('collection_name', parentCollectionDoc._id);
                }
          }
        });

    self.ready();
    self.onStop(function () {
        if (handler) handler.stop();
                if (subhandler) subhandler.stop();
    });
  });
}

With that said, if you are only trying to achieve reactive joins then you really should look into the Meteor Publish Composite package. It handles reactive joins very easily and will keep your publication up to date with the parent collection changes or any of the child collections change.

Here is what a publication would look like (based on your example) using publish composite.

const Collection = new Meteor.Collection('collection_name');
const SubCollection = new Meteor.Collection('sub_collection_name');

Meteor.publishComposite('collection', function(query, options) {
  query = query == undefined ? {} : query;
  options = options == undefined ? {} : options;

  return {
    find: function() {
      return Collection.find(query,options);
    },
    children: [{
      find: function(collectionDoc) {
        return SubCollection.find({_id: collectionDoc.objectId});
      }
    }],
  };
});

With this example, anytime Collection or associated SubCollection docs change they will be sent to the client.

The only gotcha with this approach is that it publishes the docs into their respective collections. So you would have to perform the join (SubDocument lookup) on the client. Assuming we have subscribed to the above publication and we wanted to get a SubCollection doc for a certain Collection doc on the client, then it would look like this.

// we are on the client now
var myDoc = Collection.findOne({ //..search selector ..// });
myDoc.object = SubCollection.findOne({_id: myDoc.objectId});

The composite publication ensures that the latest SubCollection doc is always on the client. The only problem with the above approach is that if your SubCollection doc changes and is published to the client, your data will be stale because you have stored an static (and unreactive) version of the SubCollection doc in myDoc.object.

The way around this is to only perform your join when you need it and don't store the results. Or, another option is to use the Collection Helpers package and create a helper function that dynamically does the join for you.

// we are on the client now
Collection.helpers({
  object: function() {
    return SubCollection.findOne({_id: myDoc.objectId});
  },
});

With this helper in place, anytime you need access to the joined SubCollection doc you would access it like this.

var myDoc = Collection.findOne({ //..search selector ..// });
console.dir(myDoc.object);

Under the covers, the collection helper does the SubCollection lookup for you.

So long story short, take your pick (roll your own reactive join publication or use Publish Composite + Collection Helpers). My recommendation is to use the packages because it's a tried and true solution that works as advertised out of the box (fyi...I use this combination in several of my Meteor apps).

Upvotes: 1

Related Questions