Reputation: 4007
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
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