how to waitOn data ready using iron-router and publish-composite - meteor

I have the following route :
this.route('groupPage', {
path: '/group/:_groupId',
waitOn: function(){
return Meteor.subscribe("groupPage", this.params._groupId);
},
data: function() {
var group = Groups.findOne({_id: this.params._groupId});
var members = Meteor.users.find({_id : {$in: group.memberIds}}); ******** ISSUE HERE******
return {
group: group,
members: members,
}; }});
and the following publication :
Meteor.publishComposite('groupPage', function(groupId, sortOrder, limit) {
return {
// return the group
find: function() {
if(this.userId){
var selector = {_id: groupId};
var options = {limit: 1};
return Groups.find(selector, options);
}
else{
return ;
}
},
children: [
{ // return the members
find: function(group) {
var selector = {_id: {$in: group.memberIds} };
return Meteor.users.find(selector);
}
}
]}}) ;
Now my issue is that : when the related page renders for the first there is no problems but when i actualize the group Page view the line : var members = Meteor.users.find({_id : {$in: group.memberIds}}); gives me the error : undefined object don't have memberIds property. i guess it's because the subscription is not yet ready when doing group.memberIds , isn't it ? Please a hint.
Thanks.

The data function doesn't wait for the subscription to be ready. Further more, subscriptions in the router are considered an anti-pattern for the most part, and should be done in the template: https://www.discovermeteor.com/blog/template-level-subscriptions/
I would pass to the template the groupId, and then get the group and members in the template, like so:
this.route('groupPage', {
path: '/group/:_groupId',
data: function() {
return {
_groupId: this.params._groupId,
}
}
});
and then in the template file:
Template.groupPage.onCreated(function(){
this.subscribe("groupPage", this.data._groupId);
})
Template.groupPage.helpers({
members(function(){
tempInst = Template.instance()
var group = Groups.findOne({_id: tempInst.data._groupId});
return Meteor.users.find({_id : {$in: group.memberIds}});
})
})

The general pattern of your route and publication are all solid. I suspect it's something simple such as:
There is no group with the _id you're using
You're not logged in when you load the route
Here's a version of your code that guards against the error. Note that the publication executes this.ready() instead of just returning if the user is not logged in.
this.route('groupPage', {
path: '/group/:_groupId',
waitOn: function(){
return Meteor.subscribe("groupPage", this.params._groupId);
},
data: function() {
var group = Groups.findOne({_id: this.params._groupId});
var members = group && Meteor.users.find({_id : {$in: group.memberIds}});
return { group: group, members: members };
}
});
Meteor.publishComposite('groupPage', function(groupId,sortOrder,limit) {
return {
find: function() {
if (this.userId) return Groups.find(groupId);
this.ready()
}
},
children: [
find: function(group) {
var selector = {_id: {$in: group.memberIds} };
return Meteor.users.find(selector);
}
]
});

Related

Vue doesn't update when computed data change

Context: I have a list of posts with tags, categories from wordpress api. I display these posts with Vue and using computed with a search box to filter the result based on titre, description, tags, and categories
Problem: I am trying to update a computed list when user click on a list of tag available. I add the get and set for computed data like this:
var vm = new Vue({
el: '#blogs',
data: {
search: '',
posts: [],
filterPosts: []
},
beforeMount: function() {
// It should call the data and update
callData();
},
computed: {
filterPosts: {
get: function() {
var self = this;
return self.posts.filter(function(post){
var query = self.search.toLowerCase();
var title = post.title.toLowerCase();
var content = post.content.toLowerCase();
var date = post.date.toLowerCase();
var categories = '';
post.categories.forEach(function(category) {
categories += category.name.toLowerCase();
});
var tags = '';
post.tags.forEach(function(tag){
tags += tag.name.toLowerCase();
});
return title.indexOf(query) !== -1 ||content.indexOf(query) !== -1 || date.indexOf(query) !== -1 || categories.indexOf(query) !== -1 || tags.indexOf(query) !== -1;
});
},
set: function (newValue) {
console.log(newValue);
this.filterPosts = Object.assign({}, newValue);
}
}
},
methods: {
filterByTag: function(tag, event) {
event.preventDefault();
var self = this;
self.filterPosts = self.posts.filter(function(post){
var tags = '';
post.tags.forEach(function(tag){
tags += tag.name.toLowerCase();
});
return tags.indexOf(tag.toLowerCase()) !== -1;
});
}
}
}); // Vue instance
The console.log always output new data based on the function I wrote on methods but Vue didn't re-render the view. I think I didn't do the right way or thought like Vue. Could you please give some insight?
Edit 1
Add full code.
I tried to add filterPosts in data but I received this error from Vue: The computed property "filterPosts" is already defined in data.
Your setter is actually not setting anything, it only logs the new value. You need to store it somewhere.
For example you can store it in the component's data:
data: {
value: 'foo',
},
computed: {
otherValue: {
get() { /*...*/ },
set(newVal) { this.value = newVal },
},
},
But this is definitely not the only possibility, if you use Vuex, the setter can dispatch an action that will then make the computed value get updated. The component will eventually catch the update and show the new value.
computed: {
value: {
get() {
return this.$store.getters.externalData;
},
set(newVal) {
return this.$store.dispatch('modifyingAction', newVal);
},
},
},
The bottomline is you have to trigger a data change in the setter, otherwise your component will not be updated nor will it trigger any rerender.
EDIT (The original answer was updated with full code):
The answer is that unless you want to manually change the list filteredPosts without altering posts, you don't need a get and set function for your computed variable. The behaviour you want can be acheived with this:
const vm = new Vue({
data() {
return {
search: '',
posts: [],
// these should probably be props, or you won't be able to edit the list easily. The result is the same anyway.
};
},
computed: {
filteredPosts() {
return this.posts.filter(function(post) {
... // do the filtering
});
},
},
template: "<ul><li v-for='post in filteredPosts'>{{ post.content }}</li></ul>",
});
This way, if you change the posts or the search variable in data, filteredPosts will get recomputed, and a re-render will be triggered.
After going around and around, I found a solution, I think it may be the right way with Vue now: Update the computed data through its dependencies properties or data.
The set method didn't work for this case so I add an activeTag in data, when I click on a tag, it will change the activeTag and notify the computed filterPost recheck and re-render. Please tell me if we have another way to update the computed data.
var vm = new Vue({
el: '#blogs',
data: {
search: '',
posts: [],
tags: [],
activeTag: ''
},
beforeMount: function() {
// It should call the data and update
callData();
},
computed: {
filterPosts: {
get: function() {
var self = this;
return self.posts.filter(function(post){
var query = self.search.toLowerCase();
var title = post.title.toLowerCase();
var content = post.content.toLowerCase();
var date = post.date.toLowerCase();
var categories = '';
post.categories.forEach(function(category) {
categories += category.name.toLowerCase();
});
var tags = '';
post.tags.forEach(function(tag){
tags += tag.name.toLowerCase();
});
var activeTag = self.activeTag;
if (activeTag !== '') {
return tags.indexOf(activeTag.toLowerCase()) !== -1;
}else{
return title.indexOf(query) !== -1 ||content.indexOf(query) !== -1 || date.indexOf(query) !== -1 || categories.indexOf(query) !== -1 || tags.indexOf(query) !== -1;
}
});
},
set: function (newValue) {
console.log(newValue);
}
}
},
methods: {
filterByTag: function(tag, event) {
event.preventDefault();
var self = this;
self.activeTag = tag;
}
}
}); // Vue instance
Try something like:
data: {
myValue: 'OK'
},
computed: {
filterPosts: {
get: function () {
return this.myValue + ' is OK'
}
set: function (newValue) {
this.myValue = newValue
}
}
}
More:
https://v2.vuejs.org/v2/guide/computed.html#Computed-Setter

Publish users using id from a different collection

I'm trying to access the userIds stored in a collection and then use them to publish the details of all of the meteor.users. My publish function doesn't isn't return anything?
Meteor.publish('allUsersWithOffers', function () {
var user = Offers.find({}, {fields: {"UserId": 1}});
return Meteor.users.find({_id: user});
});
Give this a try:
Meteor.publish('allUsersWithOffers', function () {
var offers = Offers.find({}, { fields: { UserId: 1 } }).fetch();
var ids = _.pluck(offers, 'UserId');
// This is critical - you must limit the fields returned from
// the users collection! Update this as needed.
options = { fields: { username: 1, emails: 1 } };
return Meteor.users.find({ _id: { $in: ids } }, options);
});
find returns a cursor - you need to call fetch to actually get the documents.

Meteor.js : SearchSource + Publish composite

I'm currently creating a research engine for my app.
Until now, I used Publish composite + iron router : The user could had filters to search for some specific set of users.
Now, I want him to be able to look for some keywords too. For that I downloaded and tested the great SearchSource package.
The problem is that the SearchSource server side definition only seems to allow to return one cursor.
How could I combine the two logics ? Even if it's tricky, please, share.
Of course I could make an autorunned subscription where I look for every users loaded on the client and then subscribe to the additionnal documents, but it is not really the most performant and beautifull thing to do.
Some data :
Here is my current Publish Composite for filters :
Meteor.publishComposite("findTalkers", function(page, langs){
//console.log("Find Talkers");
//console.log("page : " + page);
//console.log("langs : " + langs);
if (langs.length)
{
return ({
find: function()
{
if (langs && langs.length)
{
var test = {$in: langs};
preSelectedUsers = [],
selector = {
_id: {$ne: this.userId},
"profile.completed": true,
"profile.firstName": {$exists: true},
"profile.languages.native": {$exists: false},
"profile.languages.lang": test
};
Counts.publish(this, "nbUsers", Meteor.users.find(selector, {
fields: {_id: 1}
}), {noReady: false, nonReactive: true});
if (page > 1)
{
preSelectedUsers = Meteor.users.find(selector, {
sort: {'profile.talkname': 1},
limit: 25,
skip: (25 * (page || 1)),
fields: {_id: 1}
}).fetch();
var i = -1;
while (preSelectedUsers[++i])
preSelectedUsers[i] = preSelectedUsers[i]._id;
}
if (page > 1)
selector._id = {$in: preSelectedUsers};
return Meteor.users.find(selector, {
fields: userFields,
sort: {'profile.talkname': 1},
limit: 25
});
}
},
children: [
{
// Finding user's profile picture if it is not url
find: function(user)
{
if (user && user.profile && user.profile.avatar.type != "url")
return Images.find({_id: user.profile.avatar.data}, {sort: {uploadedAt: -1}, limit: 1});
}
}
]
});
}
else
{
return ({
find: function()
{
return Meteor.users.find({_id: "flush"});
}
});
}
});
Here is my research with SearchSource :
Client :
var searchOptions = {
keepHistory: 1000 * 60 * 5,
localSearch: true
},
SearchSources = {
talkersSearch: new SearchSource('users', ['profile.talkname'], searchOptions)
};
Router.map(function(){
this.route('talkers/:page?',
{
template: "talkers",
onBeforeAction: function(pause){
(Meteor.user() && Meteor.user().profile.completed)
? this.next()
: this.render('/profile');
},
waitOn: function(){
var filters = MatesFilter.find().fetch(),
i = -1;
while (filters[++i])
filters[i] = filters[i].value;
if (filters.length)
{
return Meteor.subscribe("findTalkers", (this.params.page || 1), filters, function(){
Session.set('numberuser', Counts.get("nbUsers"));
});
}
return Meteor.subscribe('myself');
}
});
}
Template.talkers.helpers({
getPackages: function() {
return SearchSources.talkersSearch.getData({
transform: function(matchText, regExp) {
return matchText.replace(regExp, "<b>$&</b>")
},
sort: {isoScore: -1}
});
}
}
Template.talkers.events({
"keyup #header-search": _.throttle(function(e) {
Session.set("matesSearch", $(e.target).val().trim());
console.log("Searching for : " + text);
SearchSources.talkersSearch.search(Session.get("matesSearch"), {
page: (this.params.page || 1),
filters: filters
});
}, 200)
}
SERVER :
SearchSource.defineSource('users', function(searchText, options) {
var options = {sort: {"profile.talkname": -1}, limit: 25};
if(searchText)
{
var regExp = buildRegExp(searchText);
selector = { $or: [
{ "profile.talkname": regExp },
{ "profile.bio": regExp }
] };
return Meteor.users.find(selector, options).fetch();
}
return ;
});
All this Gives me two sources from which I can get users. I'd want to get a mean to merge the two ides (a composition of publication INSIDE the search, for example).
Thanks you.

meteor iron:router Blaze.ReactiveVar calling multiple times

I have a route I call many times. I have to subscribe two collections for having all datas, here's a snapshot:
var one = new Blaze.ReactiveVar(false);
var two = new Blaze.ReactiveVar(false);
this.route('stopIndex', {
path: '/stop/:section/:stop_id',
waitOn: function() {
Meteor.call('getTripIdsForStop', {
stop_id: this.params.stop_id,
from: fromNow(),
to: toMax(),
db: prefix
}, function(err, ids) {
DEBUG && console.log('TRIP_IDS:', ids);
Meteor.subscribe(prefix + '_trips', {
trip_id: {$in: ids}
}, function() {
one.set(true);
});
Meteor.subscribe(prefix + '_stop_times', {
trip_id: {$in: ids}
}, function() {
two.set(true);
});
});
return [
function () { return one.get(); },
function () { return two.get(); }
];
},
The first time I call the route, all goes fine. The second time, the one and two vars are already setted to true so the waitOn doesn't wait and I get a no data message on my template for some seconds, until collections responds. I've tried putting on the first lines of waitOk method:
one.set(false);
two.set(false);
but this makes the waitOn to wait forever. Am I doing something wrong or missing something? Thanks for the help.
I've solved this way:
Router.onStop(function() {
one.set(false);
two.set(false);
});
that invalidates ReactiveVars and will wait. I've also moved all code from waitOn to data. Now the waitOn is like this:
return [
function () { return one.get(); },
function () { return two.get(); }
];

Internal Server Error trying to update server database in Meteor.js

I've been modifying the example meteor app at http://meteor.com/examples/leaderboard. As you can see in the code bellow, I'm trying to update the score of players upon someone hitting the reset button. This updated fine on the client side but in my console I noticed the error "update failed: 500 -- Internal server error". Upon further inspection I saw that indeed, the server side database was not being updated. Any thoughts? (relevant code is in the reset function but I've posted the rest here just in case)
// Set up a collection to contain player information. On the server,
// it is backed by a MongoDB collection named "players."
Players = new Meteor.Collection("players");
var SORT_OPTIONS = {
name: {name: 1, score: -1},
score: {score: -1, name: 1}
}
var NAMES = [ "Ada Lovelace",
"Grace Hopper",
"Marie Curie",
"Carl Friedrich Gauss",
"Nikola Tesla",
"Claude Shannon" ];
function reset(options) {
if (options && options['seed'] === true) {
for (var i = 0; i < NAMES.length; i++) {
Players.insert({ name: NAMES[i], score: Math.floor(Math.random()*10)*5 });
}
}
if (options && options['restart'] === true) {
Players.update( {},
{ $set: { score: Math.floor(Math.random()*10)*5 } },
{multi: true});
}
}
if (Meteor.is_client) {
Template.leaderboard.players = function () {
var sort_by = SORT_OPTIONS[Session.get("sort_by")]
return Players.find({}, {sort: sort_by});
};
Template.leaderboard.selected_name = function () {
var player = Players.findOne(Session.get("selected_player"));
return player && player.name;
};
Template.player.selected = function () {
return Session.equals("selected_player", this._id) ? "selected" : '';
};
Template.leaderboard.events = {
'click input.inc': function () {
Players.update(Session.get("selected_player"), {$inc: {score: 5}});
},
'click input.sort': function () {
Session.get("sort_by") == "score" ? Session.set("sort_by", "name") : Session.set("sort_by", "score");
},
'click input.reset': function () {
reset({'restart': true});
}
};
Template.player.events = {
'click': function () {
Session.set("selected_player", this._id);
}
};
}
// On server startup, create some players if the database is empty.
if (Meteor.is_server) {
Meteor.startup(function () {
if (Players.find().count() === 0) {
reset({'seed': true});
}
});
}
This also happened to me, but checking the server log, the problem I had was that the $inc modifier requires a number for the argument for the update method, so I made sure it got it with
Number()
Time went by and it now works :) I guess it was some server issue on their demo deploy site.

Resources