How do I do a parameter based publication in Meteor and remove old subscription document? - meteor

I want to implement a parameter based publication in Meteor but I am running into some problems.
Here is what I have.
As the user types the keyup event that subscribes to publication and passes the value of the input.
'keyup #customerSearch': function(event, template){
var keyword = template.find('#customerSearch').value;
if(keyword){
if(keyword.length >= 3){
Meteor.subscribe('sessioncustomers', keyword);
}
}
}
The publication uses this keyword to return the records.
Meteor.publish("sessioncustomers", function(keyword){
if(keyword ){
if(keyword.length >= 3){
query.name = new RegExp(regExpQuoted(keyword), 'i' );
Customers.find(query);
} else {
return null;
}
}else{
return null;
}
});
The problem.
It works and documents are received except when the client changes the keyword or rather as the keywords changes the publication publishes additional documents that match the keywords but the client collection never removes the old documents.
How do I get the old documents that no longer match out of the client collection?
I thought that because the parameters of the subscription had changed that the non-matching documents would be unsubscribed and only the new matching documents would be subscribed.

In your keyup callback you need to "unsubscribe" to the previous publication,
otherwise you'll keep the old documents.
var sessionCustomersHandler = false;
'keyup #customerSearch': function(event, template) {
var keyword = template.find('#customerSearch').value;
if (keyword && keyword.length >= 3)
var newSessionCustomersHandler = Meteor.subscribe('sessioncustomers', keyword);
if (sessionCustomersHandler)
sessionCustomersHandler.stop();
sessionCustomersHandler = newSessionCustomersHandler;
}
Moreover, don't forget to check(keyword, String) in your publish function, for security.
Meteor.publish("sessioncustomers", function(keyword){
check(keyword, String)
if (keyword.length >= 3)
return Customers.find({
name: new RegExp(regExpQuoted(keyword), 'i' )
});
});

Make a local unnamed client collection
this.SessionCustomers = new Meteor.Collection(null);
Call a server method to get the results you want. Make the callback clear (remove all) and then insert to that local collection.
return Meteor.call('sessioncustomers', query, function(err, data) {
if (err) {
return console.log(err.message);
} else {
SessionCustomers.remove({});
var item, _i, _len;
for (_i = 0, _len = data.length; _i < _len; _i++) {
item = array[_i];
SessionCustomers.insert(item);
}
}
});

Related

Meteor subscription is not stopping

I've got what should be a relatively simple issue. I set a session, then a subscribe to a collection using the string stored in the session. But when that session changes, I need to clear the subscription data and start again.
My code is as follows:
let subscriptionReady;
let filteredResults = [];
let rawResults = [];
let county = Session.get('county');
let type = Session.get('type');
This is mostly just prep work to create some empty objects to populate later. This all gets set on a click event. After we set these placeholder objects we go and subscribe by those sessions:
if (county && !type) {
return function() {
if (subscriptionReady) {
subscriptionReady.stop();
}
filteredResults = [];
rawResults = [];
subscriptionReady = Meteor.subscribe('resourcesearch', county, {
onReady: () => {
rawResults = resourceCollection.find({}, { sort: {score: -1} }).fetch();
rawResults.forEach((result) => {
if (result.score) {
filteredResults.push(result);
}
});
}
});
}
At the third line I run a check to see if subscriptionReady exists, then it will have the stop method available. So then I run it. But, it doesn't actually stop anything.
What am I missing?
After trial and error, I've got it solved. The issue was the placement of the stop call. I no longer have to check if subscriptionReady exists, instead I stop the subscription inside of the onReady method:
return function() {
filteredResults = [];
rawResults = [];
subscriptionReady = Meteor.subscribe('resourcesearch', county, {
onReady: () => {
rawResults = resourceCollection.find({}, { sort: {score: -1} }).fetch();
rawResults.forEach((result) => {
if (result.score) {
filteredResults.push(result);
}
});
subscriptionReady.stop();
}
});
It's .stop() not .stop docs
Also you can probably avoid your filtering loop by including score in your query. Are you looking for documents where the score key exists {score: {$exists: true}} or just where it is non zero {$score: {$ne: 0}}?
Also you shouldn't need to clear the subscription and start again. If you make your subscription parameter resourcesearch a reactive data source then the subscription will automatically update to give you the documents you need. Starting/stopping a subscription in response to a search would be an anti-pattern.

In Meteor, how to choose a collection based on a variable?

Let's say you want to dynamically insert into different collections. Right now I am using a switch statement:
switch (i) {
case "dog":
Dog.insert({
name: "Skippy"
});
break;
case "cat":
Cat.insert({
name: "Skippy"
});
break;
}
But this is messy, and if I need to support future collections, it fails. Is there a way to choose the collection based on "i" in the example above?
Correct me if I am wrong but I think this is what you are trying to do:
var Dog = {
insert: function(props) {
console.log(props);
}
}
var insertArbitraryDocument = (function(collectionType, props) {
window[collectionType].insert(props)
}).bind(this);
insertArbitraryDocument('Dog', {name: 'skippy'}); //=> {name: 'skippy'}
In this snippet you are accessing the window object and getting the property of whatever name you are passing in (must be exactly the same as the collection). Then you can call your usual function calls.
I don't think there is a meteor built-in way of doing this, but it's pretty easy to just create a directory of collections manually:
JS in common to client and server:
var collections = {};
function myColl(name) {
var coll = new Meteor.Collection(name);
collections[name] = coll;
return coll;
}
// and now just use myColl instead of new Meteor.Collection
Dog = myColl('dog');
And then, to do what you want to do:
collections[i].insert(data);
Here's a complete working example:
Posts = new Mongo.Collection('posts');
Comments = new Mongo.Collection('comments');
var capitalize = function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
};
var nameToCollection = function(name) {
// pluralize and capitalize name, then find it on the global object
// 'post' -> global['Posts'] (server)
// 'post' -> window['Posts'] (client)
var root = Meteor.isClient ? window : global;
return root[capitalize(name) + 's'];
};
var insertSomething = function(name, data) {
var collection = nameToCollection(name);
collection.insert(data);
}
Meteor.startup(function() {
// ensure all old documents are removed
Posts.remove({});
Comments.remove({});
// insert some new documents
insertSomething('post', {message: 'this a post'});
insertSomething('comment', {message: 'this a comment'});
// check that it worked
console.log(Posts.findOne());
console.log(Comments.findOne());
});
Note this is nearly identical to this question but I simplified the answer for more generic use.

Getting a username from ID without autopublish

I just got done with the rough draft of my app, and thought it was time to remove autopublish and insecure mode. I started transfering all the stray update and insert methods I had been calling on the client to methods. But now I'm having trouble returning a username from an ID.
My function before: (that worked, until I removed autopublish)
challenger: function() {
var postId = Session.get('activePost');
var post = Posts.findOne(postId);
if (post.challenger !== null) {
var challenger = Meteor.users.findOne(post.challenger);
return challenger.username;
}
return false;
}
Now what I'm trying:
Template.lobby.helpers({
challenger: function() {
var postId = Session.get('activePost');
var post = Posts.findOne(postId);
if (post.challenger !== null) {
var userId = post.challenger;
Meteor.call('getUsername', userId, function (err, result) {
if (err) {
console.log(err);
}
return result;
});
}
return false;
},
Using:
Meteor.methods({
getUsername: function(userId) {
var user = Meteor.users.findOne({_id: userId});
var username = user.username;
return username;
},
...
})
I have tried blocking the code, returning values only once they're defined, and console.logging in the call-callback (which returned the correct username to console, but the view remained unchanged)
Hoping someone can find the obvious mistake I'm making, because I've tried for 3 hours now and I can't figure out why the value would be returned in console but not returned to the template.
Helpers need to run synchronously and should not have any side effects. Instead of calling a method to retrieve the user, you should ensure the user(s) you need for that route/template are published. For example your router could wait on subscriptions for both the active post and the post's challenger. Once the client has the necessary documents, you can revert to your original code.

Method call that fetches data. Can it be made reactive?

I want to get data from a number of queries on the same collection, and unfortunately this is not yet supported on meteor. That's why I tried to do something like this:
Common
Dep = new Deps.Dependency;
Server
Meteor.methods({
fetch: function(){
var results = Data.find(dataQuery).fetch();
var otherResults = Data.find(queryThatCannotBeCombinedWithPrevious).fetch();
return results.concat(otherResults);
},
save: function(data){
Data.insert(data);
Dep.changed();
}
update: function(data){
Data.update({_id: data._id}, data);
Dep.changed();
}
});
Client
Session.setDefault('combinedData', []);
Template.demo.data = function(){
Dep.depend();
Meteor.call('fetch',function(error, data){
Session.set('combinedData', data);
});
return Session.get('combinedData');
};
This doesn't work though, propably because the Dep variable on the client is different from the Dep on the server. Is there a way to make the method call reactive when the contents of the Data collection change?
Notes
I am currently using Meteor 0.8.1.1, which doesn't allow subscriptions that return multiple cursors of the same collection yet.
This requires a small hack and you're close. First, you need a client–only dependence, the server just passes a data returned from the method and doesn't share variables (also there's nothing on the server that requires deps). Second, you only want to fetch the actual variable once, otherwise you'll end up with an infinite loop.
Example implementation:
var value = null;
var valueInitialized = false;
var valueDep = new Deps.Dependency();
Template.demo.data = function() {
valueDep.depend();
if(!valueInitialized) {
valueInitialized = true;
Meteor.call('fetchData', function(err, result) {
value = result;
valueDep.changed();
});
}
return value;
};

Detecting which reactive query was triggered

I have an probably not so unique issue of having a complicated meteor app.
I have several actions which are causing parts of the page to refresh which really aren't needed. But I'm having trouble locating which find() (or multiple find()'s ) is the one being triggered. I know the Collection in question, just not which find().
I could use observeChanges on every find I use, but that would be a lot of extra code.
Is there an easy way to see what is being triggered and by what?
Thanks!
Here is a render logging function you might find useful. It logs the number of times each template is rendered to the console. You know if a template is re-rendered after initial page load, it's because a reactive data source that it relies on has changed. Either this reactive data source could have been accessed in a helper method, or the template is a list item (i.e. inside an {{#each ...}} block helper) and a list item was added/moved/removed/changed. Also keep in mind that child templates call their parent's rendered callback when the child is rendered or re-rendered. So, this might confuse you into thinking the parent has actually been taken off the DOM and put back, but that's not true.
So, you can call this function at the end of your client code to see the render counts:
function logRenders () {
_.each(Template, function (template, name) {
var oldRender = template.rendered;
var counter = 0;
template.rendered = function () {
console.log(name, "render count: ", ++counter);
oldRender && oldRender.apply(this, arguments);
};
});
}
EDIT: Here is a way to wrap the find cursor to log all changes to a cursor to the console. I just wrote a similar function to this for a new package I'm working on called reactive-vision. Hopefully released soon.
var wrappedFind = Meteor.Collection.prototype.find;
Meteor.Collection.prototype.find = function () {
var cursor = wrappedFind.apply(this, arguments);
var collectionName = this._name;
cursor.observeChanges({
added: function (id, fields) {
console.log(collectionName, 'added', id, fields);
},
changed: function (id, fields) {
console.log(collectionName, 'changed', id, fields);
},
movedBefore: function (id, before) {
console.log(collectionName, 'movedBefore', id, before);
},
removed: function (id) {
console.log(collectionName, 'removed', id);
}
});
return cursor;
};
Thank you #cmather for the idea.
Her is Meteor 1.3 adapted and more advanced version of logRenders
// Log all rendered templates
// If filter is set, only templates in filter will be logged
// #params filter - name or names of template to filter
logRenders = function logRenders (filter) {
for (name in Object(Template)){
if (filter && !Array.isArray(filter)) filter = [filter];
var template = Template[name];
if (!template) continue;
if (filter && filter.indexOf(name) == -1){
// Clear previous logRenders
if ('oldRender' in template) template.rendered = template.oldRender;
delete template.oldRender;
continue;
}
var t = function(name, template){
if (!('oldRender' in template)) template.oldRender = template.rendered;
var counter = 0;
template.rendered = function () {
console.log(name, ++counter, this);
this.oldRender && this.oldRender.apply(this, arguments);
};
}(name, template);
};
};

Resources