Issue with subscriptions on multiple instances of a template - javascript

Here is the scenario. I have a template that contains a #each loop and renders an instance of a particular template, setting the data context on each template as per the docs.
<template name = 'live'>
<div class = 'row'>
{{#each runways}}
<div class = 'col-md-2'>
{{> runway_panel}}
</div>
{{/each}}
</div>
</template>
And this is the helper backing it:
Template.live.helpers({
runways: function(){
return Runway_Data.find();
}
});
This works, my issue is as follows. Each live_event_log instance has a template level subscription that subscribes to a publication that takes the _id parameter of the data context, like so:
Template.runway_panel.onCreated(function(){
var instance = this;
instance.autorun(function(){
var subscription = instance.subscribe('runway_status', this.data._id);
});
instance.status = function(){
return Runway_Status.find();
}
});
This is the publication:
Meteor.publish('runway_status', function(runway){
if(this.userId){
//Retrieve the last know status for the given runway
return Runway_Status.find({runway: runway});
}
});
This is when it all falls apart, I get this on the browser console:
[Log] Exception in queued task: http://localhost:3000/client/views/live/runway_panel.js?4efaac87b39527d3dfd3d65a174520f9bce3c565:4:73 (meteor.js, line 888)_withTemplateInstanceFunc#http://localhost:3000/packages/blaze.js?a5c324925e5f6e800a4c618d71caf2848b53bf51:3476:16
http://localhost:3000/packages/blaze.js?a5c324925e5f6e800a4c618d71caf2848b53bf51:1864:54
_withCurrentView#http://localhost:3000/packages/blaze.js?a5c324925e5f6e800a4c618d71caf2848b53bf51:2197:16
As soon as I comment out the subscription line everything else works, am I missing something really obvious here? Could it have something to do with multiple subscriptions to the same publication?
Thank you! :)
SOLUTION
Thanks to Jeremy S. input and some sleep after a night shift i've finally figured it out without an autorun. So here it goes for posterity:
Template.runway_panel.onCreated(function(){
var self = this;
self.subscribe('runway_status', this.data._id);
});
Should probably have tried getting some sleep before trying again!

The problem is with this.data._id in your subscription, which right now is scoped to the innermost function. You want instance.data._id (which is nonreactive so you wouldn't need an autorun) or Template.currentData() (which is reactive).
Template.runway_panel.onCreated(function(){
var instance = this;
instance.autorun(function(){
var data = Template.currentData();
var subscription = instance.subscribe('runway_status', data._id);
});
});
Also note that in your publication, you should mark it as this.ready() if this.userId is undefined. But that's not the source of the error.

First, userId is a function and userId() would return current user id. And you could check here to learn more detail of instance.subscribe.
I think your problem may happen in getting runway._id in Meteor.publish
Template.runway_panel.onCreated(function(){
var instance = this;
// instance subscribe data whose _id is runwayPanelId
instance.autorun(function(){
var dataContext = Template.currentData();
var subscription = instance.subscribe('runway_status', dataContext.runwayPanelId);
});
instance.status = function(){
return Runway_Status.find();
}
});
// publication
Meteor.publish('runway_status', function(runway){
// using Meteor.userId() to get current user id
if(Meteor.userId()){
//Retrieve the last know status for the given runway
return Runway_Status.find({runway: runway});
}
});
<template name = 'live'>
<div class = 'row'>
{{#each runways}}
<div class = 'col-md-2'>
{{> runway_panel runwayPanelId=_id}}
</div>
{{/each}}
</div>
</template>

Related

Collection empty when accessed from rendered event?

I have a collection that I am subscribing to, but when I attempt to access it from my onRendered event it always returns as an empty array. Below is the method I am using:
FlightCounts = new Mongo.Collection("flightCounts");
if (Meteor.isClient) {
Meteor.subscribe('flightCounts');
Template.hello.rendered = function(){
var counts = FlightCounts.find().fetch()
console.log(counts)
}
}
if (Meteor.isServer) {
Meteor.startup(function () {
// code to run on server at startup
Meteor.publish('flightCounts', function(){
return flightCounts.find();
})
});
}
Can anyone see why my collection would always be empty here? Any suggestions on how to get it populated?
The underlying issue is that the publish function should reference Meteor's Mongo.Collection name, FlightCounts, and not the raw db.collection name flightCounts:
Meteor.publish('flightCounts', function(){
return FlightCounts.find();
});
I also agree with the prior answer that your template should check to ensure the subscription is ready before logging the data, otherwise it may not have arrived yet:
Template.hello.onRendered(function(){
this.autorun(() => {
let ready = Template.instance().subscriptionsReady();
if (!ready){ return; }
let counts = FlightCounts.find().fetch();
console.log(counts);
});
});
(Note that the newer syntax for Template.name.rendered is Template.name.onRendered().)
if (Meteor.isClient) {
Meteor.subscribe('flightCounts');
Template.hello.rendered = function() {
var template = this;
template.autorun(function () {
var counts = FlightCounts.find().fetch()
console.log(counts)
});
}
}
Your collection is not populated because, it is still getting data from server. So you should wrap your code inside an autorun, so that it will re-run whenever data is updated.
P.S: I am on mobile, so formatting may not be proper. Sorry about it.
Alternatively, you can use subscriptions of FlowRouter or waitOn of IronRouter to put the subscribe there
Update Surprisingly, IronRouter now has the subscriptions option as well

Angular, Promises, ngResource

So Im having a hard time trying to get my head wrapped around promises in angularJs. I have mixed around my code to try to do some brute force/reverse engineering understanding of it but nothing is coming out to any viable conclusion.
My Code:
Is is making a call back to get a list of repositories that I manage. These are just stored in the database as basic objects with an id and url.
Here is my view. It allows to me delete, view, and clear metadata in my database about these repos.
<div class="container" ng-controller="adminCtrl as vm">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<label class="control-label" >Repos:</label>
<div ng-repeat="repo in vm.repos">
<div class="clearfix">{{repo.URL}}<br>
<button class="btn btn-primary" ng-click='vm.listFiles(repo.URL)'>View Files</button>
<button class="btn btn-primary" ng-click='vm.clearFiles(repo.URL)'>Clear Files</button>
<button class="btn btn-primary" ng-click='vm.clearRepo(repo.URL)'>Delete Repo</button>
</div>
<br>
</div>
<label class="control-label" >Files:</label>
<div ng-repeat="file in vm.files">
<li>{{file.FullPath}}</li>
</div>
</div>
</div>
<!-- /.row -->
</div>
Here is my controller with some basic functions
(function (angular) {
'use strict';
var ngModule = angular.module('myApp.adminCtrl', []);
ngModule.controller('adminCtrl', function ($scope, $resource) {
//"Global Variables"
var File = $resource("/api/file/:repoUrl");
var Repo_del = $resource("/api/repo/:repoUrl");
var Repo = $resource("/api/repo");
var vm = this;
vm.files = [];
vm.repos = [];
vm.clearFiles = clearFiles;
vm.listFiles = listFiles;
vm.clearRepo = clearRepo;
init();
//Anything that needs to be instantiated on page load goes in the init
function init() {
listRepos();
}
function listRepos() {
vm.repos = Repo.query();
}
//Lists all files
function listFiles(url) {
vm.files = File.query({repoUrl: url});
}
function clearRepo(url) {
Repo_del.remove({repoUrl: url});
}
function clearFiles(url) {
File.remove({repoUrl: url});
}
});
}(window.angular));
Now this works fine and dandy. It brings back the repos and list them. I can delete, view, and remove with all the functions.
My issue came up with when I was trying to make a list item disappear on delete (instead of needing a page load). To do this I needed to find the index of the item being deleted in the array and remove it. I was gonna use some lodash to do this.Seemed simple enough. My problem is, my vm.repos array is not available within the controller.
For example. When I try to print out vm.repos with a console log within the listRepos function like so
function listRepos() {
vm.repos = Repo.query();
console.log(vm.repos);
}
I get nothing back from console.log. So this is telling me its not being assigned. Which to me is peculiar because the list is showing up in the ng-repeat on the view using vm.repos.
I have also ran into a problem when I am able to print out the array. It has TONS of promise information in it. For example if I put the console.log in the init() function I will get an array back that is jammed packed with information under a Resource object.
Im not sure how to go about and parse this down to be a manageable object. Looking at basic guides I have found some examples but nothing I can translate into my situation.
How do I properly handle api/resource promises?
Another problem im having is being able to mock out all of the api responses in my tests. This is my next feat. I do not care if it gets addressed here but I feel its stemming from the same problem.
Here is my only test I was able to write for this controller.
'use strict';
describe('adminCtrl', function () {
beforeEach(function () {
module('myApp.adminCtrl');
module('myApp');
});
describe('listRepos()', function () {
it('should return a json object representing a repository',
inject(function (_$httpBackend_, $rootScope, $controller) {
var scope = $rootScope.$new();
var mockBackend = _$httpBackend_;
var expectedResponse = {id: 12345, url: "https://github.com/myuser/myrepo.git"};
mockBackend.expectGET('/api/repo').respond([expectedResponse]);
var ctrl = $controller('adminCtrl', {$scope: scope});
mockBackend.flush();
expect(ctrl.repos.length).toEqual(1);
console.log(ctrl.repos[0]);
expect((angular.equals(ctrl.repos[0], expectedResponse)));
}));
});
});
Sorry if this is alot. Hopefully this isnt a repeated question.
EDIT to show what im trying now.
function clearRepo(url) {
$http.delete('/api/repo/', {params: {repoUrl: url}}).then(function (){
//DO THINGS
});
Express:
app.delete('/api/repo/:repoUrl', repoCtrl.clear);
repoCtrl.clear
module.exports.clear = function (req, res) {
var repoURL = req.params.repoUrl;
//console.log(repoURL);
Repo.remove({URL: repoURL}, function(err, results) {
if (err) {
console.log("ERR: " + err);
} else {
console.log('\n' + repoURL + ' repo deleted... \n');
}
});
Error im getting:
DELETE http://localhost:3000/api/repo/?repoUrl=https:%2F%2Fgithub.com%2Fuw34%2Fmyrepo.git 404 (Not Found)
First, the promise:
Used by $http
Allow chaining async request
Works like this :
var promise = $http.get('/api/values');
promise.then(function(response) {
$scope.displayData = response.data;
});
It is the new way to avoid simple callback (why avoid callback ?? check this CallbackHell :))
Nevertheless, callback can be complicated, hard to follow for debug and everyone prefer write sync code.
To simplify, Angular allow you to code something which look like sync code (but internally, it is async). To do it, $resource encapsulate a promise.
// this code return an empty array, then after received server respond, it will populate the empty array with data.
var values = VALUES.query();
// A simple version of it can be code like this
function simpleQuery() {
var arrayReference = [];
$http.get('api/values').then(function(response) {
// populate array reference with data received from server
angular.forEach(response.data, function(value) {
arrayReference.push(value);
});
// after the return, angular run a $digest
// which will display all newly received data thank to biding on your view
});
return arrayReference ;
}
By doing this, I return an empty array which will be populate on server response.
It is possible to get the promise from a $resource if you prefer :
var promise = Repo.query().$promise;
promise.then(function(response) {
$scope.displayData = response.data;
});
In 2020, you will probably use Async/Await instead $resource ;)
If you want more information, don't hesitate.

$firebaseArray losing functions after saving a child element

I am trying to implement a list-details view. The list is generated with $firebaseArray(ref). When an item on the list is clicked, the list controller uses list.$getRecord(item.$id) to get the particular record. Puts it in a global service(just and empty object with set and get) and in my detail controller i assign that service(already set to the selected item) to a scope variable in the detail controller and display it.
The information in the detail view is editable. and when it is editted, a save button appears which when clicked saves the edit using this code
item = editItem; //editItem contains the new changes made on the detail page
list.$save(item).then(function(ref){
//ref.key() === item.$id; //
console.log("Your Edit has been saved');
});
This works. The edits are reflected on the remote firebase data.
But the problem occurs when i navigate back to the list view and try to click another item. It gets an error which says list.$getRecord() is not a function. Now this error doesn't occur when you don't save an edit on the details view.
I printed out the list array before and after i save and i realised this
List array before an item is saved (contains AngularFire methods)
List array after an item is saved (no longer contains AngularFire methods)
I have no idea why $firebaseArray is reacting this way. Is there something i am missing? is this a normal behaviour?
PS: i am using ionic and angularFire.
I can post more code if neccesary
EDIT
Here is an abstraction of the code
List.html
<ion-list>
<ion-item href="#/lead/info" ng-click="vm.selectItem(l.$id)" ng-repeat="l in vm.list" >
<h3>{{l.firstName}} {{l.lastName}}</h3>
<h4 class="">
<p>{{l.topic}}</p>
</h4>
</ion-item>
</ion-list>
list.js (controller function)
function ListCtrl(selectedItem, $firebaseArray) {
/* jshint validthis: true */
var vm = this;
vm.list= {};
vm.selectItem = selectItem;
loadList(); //Loads the List array
function loadList() {
var fireList = new Firebase("https://xxxxx.firebaseio.com/list");
var r = $firebaseArray(fireList);
r.$loaded(function () {
vm.list = r;
});
console.log(vm.list); //Outputs the first image(above). But after an item edit and i go back, outputs the second image(above)
}
function selectItem(index) {
var sItem = vm.list.$getRecord(index);
selectedItem.setList(vm.list);
selectedItem.setItem(sItem);
}
}
The selectedItem service is simple. i use it to set a single object or array of objects
function selectedItem() {
var sItem = {};
var List = {};
return {
getItem: function () {
return sItem;
},
setItem: function (authObject) {
sItem = authObject;
},
getList: function(){
return List;
},
setList: function(al){
List = al;
}
};
};
The detail view controller is ass so:
item.js(controller function)
function ItemCtrl(selectedItem, $scope, $firebaseObject) {
/* jshint validthis: true */
var vm = this;
$scope.selectedItem = selectedItem.getItem();
$scope.listArray = selectedItem.getList();
//vm.item = $scope.selectedItem;
function saveEdit() {
var t = $scope.selectedItem;
$scope.listArray.$save(t).then(function(ref){
//console.log(ref);
});
}
};
UPDATE
After serious cross checking throughout my code i realised the issue is not from AngularFiire array. Even the workaround i did with the r.$watch and r.$loaded was unnecessary. the need for the work around was cause by another part of my code i didnt think was relevant.
I apologise for the mistake. I'd be deleting this question and a related one soon
Try using a watcher to reload the data:
var fireList = new Firebase("https://xxxxx.firebaseio.com/list");
var r = $firebaseArray(fireList);
r.$watch(function() {
r.$loaded(function () {
vm.list = r;
});
});
This is a common way of dealing with updates in an easy way, might solve your problem.

Meteor: Lazyload, load after rendering. Best practise

i have a Meteor Application which is very "slow" as there are a lot of API-Calls.
What i try to do is to break apart the loading/calls.
What i just did is:
i have loading template via iron-router
i waitOn for the first API-Call has finished
then i start the next API-calls in the Template.myTemplate.rendered - function
This was already a big benefit for the speed of my Application, but i want to break it up even more as the second call is in fact more like 5-25 API-calls.
So what i try to do now is inside the rendered function is a self-calling function which calls itself as long as there are no more to do and saves the response inside a session. (Until now it just rewrites, but even to this point i can´t get)
Template.detail.rendered = function(){
//comma separated list of numbers for the API-Call
var cats = $(this.find(".extra")).attr('data-extra').split(',');
var shop = $(this.find(".extra")).attr('data-shop');
var counter = 0;
var callExtras = function(_counter){
var obj = {
categories : [cats[_counter]],
shop : shop
};
if(_counter <= cats.length){
Meteor.subscribe('extra', obj,function(result){
//TODO dickes todo... nochmal nachdenken und recherchieren
//console.log(_counter);
Session.set('extra',Extra.find('extra').fetch()[0].results);
counter++;
callExtras(counter);
});
}
};
callExtras(counter);
Session.set('loading_msg', '' );
};
Now i have again problems with my reactive parts of the app desscribed here - Meteor: iron-router => waitOn without subscribe As i can´t find a proper way to update my client-side per user base collection. Also in the docs it is described the publish method also creates a new collection. (The new document´s ID) here - http://docs.meteor.com/#/full/publish_added
here is the publish from server
Meteor.publish('extra', function(obj){
var that = this;
Meteor.call('extra', obj, function(error, result){
if (result){
//console.log(result);
that.added("extra", "extra", {results: result});
//that.changed('extra','extra',{results: result});
that.ready();
} else {
//that.ready();
}
});
});
So my question is: Is there from scratch a better way to structuring my code means solving the problem somehow different? If not how can i achive it the cleanest way? Because for my understanding this is just strange way to do it.
EDIT:
For example.
Can i do a per-user-collection (maybe only client-side like now) and push data from the server and just subscribe to this collection? But then how can i check when the async API-Call has finshed to start the next round. So the view gets data piece by piece. I am just confused right now.
My fault was simple as i thaught: You don´t need to use subscribe.
I just added "error,result" in the callback of Meteor.call
Only "result" leads to the result is always undefined.
var cats = $(this.find(".extra")).attr('data-extra').split(',');
var shop = $(this.find(".extra")).attr('data-shop');
var counter = 0;
var callExtras = function(_counter){
var obj = {
categories : [cats[_counter]],
shop : shop
};
if(_counter <= cats.length){
Meteor.call('extra', obj,function(error,result){
var actual_session = Session.get('extra');
if(actual_session === false){
actual_session = [];
}
actual_session = actual_session.concat(result);
Session.set('extra',actual_session);
counter++;
callExtras(counter);
});
}
};
callExtras(counter);
Then in the template helper
"extra" : function(){
return Session.get('extra');
},

Meteor Users collection and Deps.autorun problems

I'm still struggling to understand how to access Meteor.users as a foreign key from another collection query. I understand that only the current user is published by default so I have a publication on the server as
Meteor.publish('itemOwner', function(userId) {
check(userId, String);
var user = Meteor.users.find({id: userId});
return user;
// return Meteor.users.find({id: userId}, {
// fields: {'profile': 1} });
});
I then have a Deps.autorun on the client..
Deps.autorun(function () {
var itemOwnerId = Session.get("itemOwnerID");
if (itemOwnerId) {
debugger
var ID = Session.get("itemOwnerID");
Meteor.subscribe('itemOwner', Session.get("itemOwnerID"));
}
});
I set the session ID on a modal form load, and display it in the template by calling the ownerProfile helper (or try to)
Template.showQuoteModalInner.helpers({
getQuote: function () {
// Get the quote ID from the session var
var quote = Session.get("quoteID");
if(quote) {
debugger;
var ID = quote.user._id;
Session.set("itemOwnerID", quote.user._id);
return quote;
}
},
ownerProfile: function() {
debugger;
var quote = Session.get("quoteID");
if(quote) {
var ID = quote.user._id;
var theUser = Meteor.users.find({_id: quote.user._id});
return theUser;
};
}
});
Now, I can trace the user ID at each stage and see it getting correctly passed to the autorun and the helpers. If I stop the program at the debugger in the ownerProfile helper and in the console put in Meteor.user.fetch({_id: "the id here"}).fetch() I get the correct user back.. but, in the handler itself the Meteor.users.find returns null??? What am I missing?
Two possibilities I noticed.
First, you are missing an underscore in the find in your publish function.
.find({id: userId}) should be .find({_id: userId}).
But this probably isn't the issue if you are seeing the user (other than the logged in user) in the console.
Second, if you are not seeing the user from your Template.showQuoteModalInner.ownerProfile helper, it is probably because you are returning a find() instead of a findOne().
find() returns a cursor whereas findOne() returns the record. Try findOne() if you want to display that single user's attributes.

Categories

Resources