I'm having problem displaying a JSON-object named "currentSet" from session-storage. When I instead use a JSON-file from a folder everything works fine, which means the HTML-code is probably fine. I have narrowed the problem down to either the $scope.set or the loadQuiz-function and have tried several things, but can't get it to work. This is the relevant parts from the controller:
$scope.set = angular.fromJson(sessionStorage.getItem('currentSet')); //doesn't work
//$scope.set = 'data/konflikter.js'; //works
$scope.defaultConfig = {
'autoMove': true,
'pageSize': 1,
'showPager': false
}
$scope.onSelect = function (question, choice) {
question.choices.forEach(function (element, index, array) {
question.Answered = choice;
});
if ($scope.defaultConfig.autoMove == true && $scope.currentPage < $scope.totalItems) {
$scope.currentPage++;
}
else {
$scope.onSubmit();
}
}
$scope.onSubmit = function () {
var answers = [];
$scope.questions.forEach(function (question, index) {
answers.push({'questionid': question._id, 'answer': question.Answered});
});
$http.post('https://fhsclassroom.mybluemix.net/api/quiz/submit', answers).success(function (data, status) {
$location.path('/');
});
}
$scope.pageCount = function () {
return Math.ceil($scope.questions.length / $scope.itemsPerPage);
};
$scope.loadQuiz = function (data) {
$http.get(data)
.then(function (res) {
$scope.name = res.data.name;
$scope.questions = res.data.questions;
$scope.totalItems = $scope.questions.length;
$scope.itemsPerPage = $scope.defaultConfig.pageSize;
$scope.currentPage = 1;
$scope.$watch('currentPage + itemsPerPage', function () {
var begin = (($scope.currentPage - 1) * $scope.itemsPerPage),
end = begin + $scope.itemsPerPage;
$scope.filteredQuestions = $scope.questions.slice(begin, end);
});
});
}
$scope.loadQuiz($scope.set);
Ok I figured out what's wrong, you are loading file so you need to get file - or import it to get the contents that's why it works with your:
$scope.set = 'data/konflikter.js'; //works but combined with http request
If you wish to pass data from dataStorage you will need to change your loadQuiz not to have http request like this:
$scope.loadQuiz = function (res) {
$scope.name = res.data.name;
$scope.questions = res.data.questions;
$scope.totalItems = $scope.questions.length;
$scope.itemsPerPage = $scope.defaultConfig.pageSize;
$scope.currentPage = 1;
$scope.$watch('currentPage + itemsPerPage', function () {
var begin = (($scope.currentPage - 1) * $scope.itemsPerPage),
end = begin + $scope.itemsPerPage;
$scope.filteredQuestions = $scope.questions.slice(begin, end);
});
}
With the help of #pegla I was able to solve the problem like this:
$scope.loadQuiz = function () {
$scope.set = angular.fromJson(sessionStorage.getItem('currentSet'));
$scope.questionsetid = $scope.set.questions[0].questionsetid;
$scope.name = $scope.set.name;
$scope.questions = $scope.set.questions;
$scope.totalItems = $scope.set.questions.length;
$scope.itemsPerPage = $scope.defaultConfig.pageSize;
$scope.currentPage = 1;
$scope.$watch('currentPage + itemsPerPage', function () {
var begin = (($scope.currentPage - 1) * $scope.itemsPerPage),
end = begin + $scope.itemsPerPage;
$scope.filteredQuestions = $scope.questions.slice(begin, end);
});
}
$scope.loadQuiz();
Related
I'm trying to put pagination with ng-repeat. Getting result but by the time showing old data and suddenly hide and show new set data, like jerking. My angularjs version is "1.5.8".
$scope.gap = 5;
$scope.filteredItems = [];
$scope.groupedItems = [];
$scope.itemsPerPage = 5;
$scope.pagedItems = [];
$scope.currentPage = 0;
var resultData = [...];
var searchMatch = function (haystack, needle) {
if (!needle) {
return true;
}
return haystack.toLowerCase().indexOf(needle.toLowerCase()) !== -1;
};
// init the filtered items
$scope.search = function () {
$scope.filteredItems = $filter('filter')(resultData, function (item) {
for(var attr in item) {
if (searchMatch(item[attr], $scope.query))
return true;
}
return false;
});
// take care of the sorting order
$scope.currentPage = 0;
// now group by pages
$scope.groupToPages();
};
$scope.groupToPages = function () {
$scope.pagedItems = [];
for (var i = 0; i < $scope.filteredItems.length; i++) {
if (i % $scope.itemsPerPage === 0) {
$scope.pagedItems[Math.floor(i / $scope.itemsPerPage)] = [ $scope.filteredItems[i] ];
} else {
$scope.pagedItems[Math.floor(i / $scope.itemsPerPage)].push($scope.filteredItems[i]);
}
}
console.log($scope.pagedItems)
};
$scope.range = function (size,start, end) {
var ret = [];
//console.log(size,start, end);
if (size < end) {
end = size;
start = size-$scope.gap;
}
for (var i = start; i < end; i++) {
ret.push(i);
}
// console.log(ret);
return ret;
};
$scope.prevPage = function () {
if ($scope.currentPage > 0) {
$scope.currentPage--;
}
};
$scope.nextPage = function () {
if ($scope.currentPage < $scope.pagedItems.length - 1) {
$scope.currentPage++;
}
};
$scope.setPage = function () {
console.log($scope.pagedItems[$scope.currentPage]);
$scope.currentPage = this.n;
};
$scope.search();
html
<tr ng-repeat="user in pagedItems[currentPage]" >
<td>{{user.name}} {{$index}}</td>
</tr>
As above code, that table row update as 5 row. but when I click next or page numbers showing 10 rows and hide 5 rows. I hope you understand guys! Help me.
In my opinion you are thinking wrong, it's simpler to use custom filter and break up your business logic like this
app.filter('startFrom', function() {
return function(input, start) {
start = +start;
return input.slice(start);
}
});
then in the controller you can do
$scope.showResults = function() {
$scope.list = $filter('startFrom')($scope.initialList, $scope.page * $scope.itemsPerPage);
$scope.list = $filter('limitTo')($scope.list, $scope.itemsPerPage);
}
};
now you need only to update $scope.page and reuse $scope.showResults() to update the view
<tr ng-repeat="user in list" >
<td>{{user.name}} {{$index}}</td>
</tr>
I don't think that your problem is cause by ng-repeat. I just take your code, add some dummy data and do a test and it's totally work fine.
You can check it here https://codepen.io/Cushdrive/pen/zvPPXd
Because angular is two way binding, so I'm wonder that could it be some paging info was triggered somewhere. Could you double check on some variable like currentPage itemPerPages or pagedItems to see if it being used somewhere else?
I have a pagination code which I should use repeatedly inside the same controller and thought of putting it inside a function. But it is not working as I expected. Always sends an error saying some value is undefined.
How do I achieve this.
(function () {
'use strict';
angular
.module('efutures.hr.controllers.creation', [])
.controller('UserCreateController', UserCreateController);
UserCreateController.$inject = ['$scope', '$location', '$rootScope', '$http', 'deptService', 'DeptNameService','EmployeeService'];
function UserCreateController($scope, $location, $rootScope, $http, deptService, DeptNameService, EmployeeService) {
(function initController() {
deptService.getdepts(function (res) {
$scope.depts = JSON.parse(res.data);
});
EmployeeService.GetEmpDetails(function (res) {
$scope.FilterDetails = JSON.parse(res.data); //This is the variable that I need inside the function.
$scope.PaginationTrigger($scope.FilterDetails); //CODE APPLIED HERE
});
})();
$scope.reset = function () {
$('#depts').val('').trigger('change.select2');
EmployeeService.GetEmpDetails(function (res) {
$scope.FilterDetails = JSON.parse(res.data);
});
};
$scope.paginationTrigger =function(details){ //This method is used to control the pagination
$scope.nums = ["10", "20", "30"];
$scope.viewBy = $scope.nums[0];
$scope.totalEmployees = $scope.details.length;
$scope.currentPage = 1;
$scope.itemsPerPage = $scope.viewBy;
$scope.maxSize = 5;
$scope.setPage = function (pageNo) {
$scope.currentPage = pageNo;
};
$scope.pageChanged = function () {
console.log('Page changed to: ' + $scope.currentPage);
}
$scope.setEmployeesPerPage = function (num) {
$scope.itemsPerPage = num;
$scope.currentPage = 1;
}
}
$scope.DropdownSelected = function (value) {
console.log(value.DeptName + ' Selected');
var DeptNameChanged = {
'Name': value
};
DeptNameService.DeptNameValue(DeptNameChanged, function (res) {
$scope.FilterDetails = JSON.parse(res.data);
$scope.PaginationTrigger($scope.FilterDetails); //CODE APPLIED HERE
});
};
}
})();
According to the above code the ERROR IS: angular.js:13642 TypeError:
Cannot read property 'length' of undefined
So how can I achieve this? help would be appreciated. Thanks
test = {"a" : 1}
details = "a"
alert(test[details])
Use, $scope[details].length since details is a parameter there.
$scope.paginationTrigger =function(details){ //This method is used to control the pagination
$scope.nums = ["10", "20", "30"];
$scope.viewBy = $scope.nums[0];
$scope.totalEmployees = $scope[details].length;
$scope.currentPage = 1;
$scope.itemsPerPage = $scope.viewBy;
$scope.maxSize = 5;
$scope.setPage = function (pageNo) {
$scope.currentPage = pageNo;
};
$scope.pageChanged = function () {
console.log('Page changed to: ' + $scope.currentPage);
}
$scope.setEmployeesPerPage = function (num) {
$scope.itemsPerPage = num;
$scope.currentPage = 1;
}
}
Pls look the code snippet, you will get the error.
I finally figured it out.This is what I tried to accomplish and it was successful. Special thanks to #Sravan for repeatedly trying to help me out. Also thank you all for you help.
So here's the code. Thought of sharing for learning.
//Create a function in the controller
function paginationTrigger(value) {
$scope.nums = ["10", "20", "30"];
$scope.viewBy = $scope.nums[0];
$scope.totalEmployees = value.length;
$scope.currentPage = 1;
$scope.itemsPerPage = $scope.viewBy;
$scope.maxSize = 5;
$scope.setPage = function (pageNo) {
$scope.currentPage = pageNo;
};
$scope.pageChanged = function () {
console.log('Page changed to: ' + $scope.currentPage);
}
$scope.setEmployeesPerPage = function (num) {
$scope.itemsPerPage = num;
$scope.currentPage = 1;
}
}
/* Call the function in the desired palce.
In this case inside my service function */
EmployeeService.GetEmpDetails(function (res) {
$scope.FilterDetails = JSON.parse(res.data);
//pagination code
paginationTrigger($scope.FilterDetails); //Passing the value
});
I am using angularJs and I use this function in my controller to get data from database
this.callServer = function callServer(criteria) {
ctrl.searchParameters = criteria;
ctrl.isLoading = true;
var start = $scope.itemsPerPage * ($scope.currentPage - 1);
var limit = $scope.itemsPerPage;
service.getRandomsItems(criteria, start, limit).then(
function(result) {
var remainder = $scope.totalItems % $scope.itemsPerPage
if (remainder > 0)
$scope.numPages = parseInt($scope.totalItems / $scope.itemsPerPage) + 1;
else
$scope.numPages = parseInt($scope.totalItems / $scope.itemsPerPage);
ctrl.displayed = result.randomsItems;
$scope.totalItems = result.total;
ctrl.isLoading = false;
});
};
}
and I call this function in my controller too to handle the paging issue
$scope.pageChanged = function(currentPage) {
$scope.currentPage = currentPage;
ctrl.callServer($scope.criteria);
}
As you can see, my function callServer returns ctrl.displayed which is the rows that displayed in current page
Now I want to work with new page so I called ctrl.callServer to get new page then I called ctrl.selectCurrentPage() just like that
$scope.pageChanged = function(currentPage) {
$scope.currentPage = currentPage;
ctrl.callServer($scope.criteria);
ctrl.selectCurrentPage() // I want this function to be called when ctrl.callServer($scope.criteria) is finished
}
where
ctrl.selectCurrentPage = function() {
ctrl.selection.push(this.displayed[i].userId);
ctrl.selectionRow.push(this.displayed[i]);
}
in Simple English I want ctrl.selectCurrentPage to be called when ctrl.callServer is finsih and get the new data
but that not happen.
Just return a promise from callServer and use it in pageChanged.
The first step:
this.callServer = function callServer(criteria) {
ctrl.searchParameters = criteria;
ctrl.isLoading = true;
var start = $scope.itemsPerPage * ($scope.currentPage - 1);
var limit = $scope.itemsPerPage;
return service.getRandomsItems(criteria, start, limit).then(function(result) {
var remainder = $scope.totalItems % $scope.itemsPerPage
if (remainder > 0)
$scope.numPages = parseInt($scope.totalItems / $scope.itemsPerPage) + 1;
else
$scope.numPages = parseInt($scope.totalItems / $scope.itemsPerPage);
ctrl.displayed = result.randomsItems;
$scope.totalItems = result.total;
ctrl.isLoading = false;
});
};
Then:
$scope.pageChanged = function(currentPage) {
$scope.currentPage = currentPage;
ctrl.callServer($scope.criteria).then(function () {
ctrl.selectCurrentPage();
});
}
Try to avoid callback hell. Please read the article for some useful information.
pass it as a callback;
this.callServer = function callServer(criteria, callback) {
ctrl.searchParameters = criteria;
ctrl.isLoading = true;
var start = $scope.itemsPerPage * ($scope.currentPage - 1);
var limit = $scope.itemsPerPage;
service.getRandomsItems(criteria, start, limit).then(
function(result) {
var remainder = $scope.totalItems % $scope.itemsPerPage
if (remainder > 0)
$scope.numPages = parseInt($scope.totalItems / $scope.itemsPerPage) + 1;
else
$scope.numPages = parseInt($scope.totalItems / $scope.itemsPerPage);
ctrl.displayed = result.randomsItems;
$scope.totalItems = result.total;
ctrl.isLoading = false;
callback();
});
};
} ]);
$scope.pageChanged = function(currentPage) {
$scope.currentPage = currentPage;
ctrl.callServer($scope.criteria, ctrl.selectCurrentPage);
}
so heres my folder structure for the client:
https://s3.amazonaws.com/f.cl.ly/items/0I0S063e3U0A2o2s3k21/Image%202014-12-05%20at%206.42.17%20PM.png
The problem is I have two states for tournaments... One Live and One noLive.
The use all the exact same views ect but could potentially have very different functionality.
Is there a trick were I can use two completely different controllers for the same view based on the data the view needs to load in iron router or something?
-thanks
For reference here is my:
routes.js for tourneys:
/* Tournaments / Browse section */
Router.route('/tournaments/:_id', function () {
this.fastRender = true;
// add the subscription handle to our waitlist
this.wait(Meteor.subscribe('campaigns'));
// this.ready() is true if all items in the wait list are ready
// console.log("Tournaments.findOne({_id: this.params._id}:", Campaigns.findOne({_id: this.params._id}));
if (this.ready()) {
this.render('tournament', {
data: function () {
return Campaigns.findOne({_id: this.params._id});
}
});
} else {
this.render('loading');
}
});
tournaments.js:
/* Globals */
Template.tournament.rendered = function () {
var self = this;
var participants = $('.participant-id');
var currentParticipant;
var nextRound;
var thisMatch;
var nextMatch;
var bracket;
participants.map(function(index, value){
if ($(value).text() === Meteor.userId()) {
if ($(value).parent().find('.participant-status').text() === 'undetermined') {
nextRound = $(value).parent().find('.participant-round').text();
thisMatch = $(value).parent().find('.participant-match').text();
bracket = $(value).parent().parent().parent().find('.participant');
};
};
});
nextRound = parseInt(nextRound) + 1;
nextMatch = Math.round(parseInt(thisMatch)/2) - 1;
if (parseInt(thisMatch) % 2 != 0) {
currentParticipant = 0;
}else{
currentParticipant = 1;
}
var winnerOptions = '';
var winnerBox = $('<div class="select-winner">');
bracket.map(function(index, value) {
winnerOptions += '<span class="winner-option"> '+$(value).find('.participant-title').text()+' <div class="winner-info"> '+$(value).find('a').html()+' </div> </span>'
});
winnerBox.append(winnerOptions);
$($($('.round'+nextRound).find('li')[nextMatch]).find('.participant')[currentParticipant]).removeClass('loser').addClass('undetermined');
$($($('.round'+nextRound).find('li')[nextMatch]).find('.participant')[currentParticipant]).find('a').addClass('tooltip').html(winnerBox);
var tournamentStartTime = function(){
var d = new Date();
var n = d.getTime();
var currentTime = TimeSync.serverTime(n);
var startTime = self.data.card.startTime;
var difference = startTime - currentTime;
var hoursDifference = Math.floor(difference/1000/60/60);
difference -= hoursDifference*1000*60*60
var minutesDifference = Math.floor(difference/1000/60);
difference -= minutesDifference*1000*60
var secondsDifference = Math.floor(difference/1000);
/* if ends (make tournament live server side?) */
if (hoursDifference < 0 || minutesDifference < 0 || secondsDifference < 0) {
Meteor.clearInterval(tStartTime);
Session.set("tournamentStartTime", false);
}else{
if (hoursDifference < 10) {hoursDifference = "0"+hoursDifference;}
if (minutesDifference < 10) {minutesDifference = "0"+minutesDifference;}
if (secondsDifference < 10) {secondsDifference = "0"+secondsDifference;}
var formattedTime = hoursDifference + ':' + minutesDifference + ':' + secondsDifference;
Session.set("tournamentStartTime", formattedTime);
}
};
Session.set("tournamentStartTime", '00:00:00');
tournamentStartTime();
var tStartTime = Meteor.setInterval(tournamentStartTime, 1000);
};
Template.tournament.events({
// Select winner from 2 options in tooltip
// Then previous round is given winner class on correct person
'click .winner-option': function(event){
// var self = $(event.target)
// var winner = self.text()
// self.parent().hide()
// self.closest('.participant').removeClass('undetermined')
// self.parent().siblings('.participant-title').text(winner)
// var classes = self.closest('ul').prev().attr('class')
// $('.' + classes.substring(0, classes.indexOf(' ')) + ' .participant-title').each(function() {
// if ($(this).text() === winner) {
// $(this).parent().parent().removeClass('loser').addClass('winner')
// }
// // else {
// // $(this).parent().parent().removeClass('winner').addClass('loser')
// // }
// });
// // $(.previousULClass .
$('#theaterMode').show();
}
});
Template.tournament.helpers({
round: function() {
var tournament = this.tournament.brackets;
var rounds = tournament.length;
var results = [];
tournament.map(function(value, index){
var currentRound = index + 1;
results.push({rounds: rounds, currentRound: currentRound, matches: value});
});
// console.log("results:", results);
return results;
},
match: function(){
// console.log("matches:", this.matches);
return this.matches;
},
participant: function(){
var results = [];
// console.log("this:", this);
this.map(function (value, index) {
// console.log("value, index:", value, index);
var type = value['win'];
var obj = {
id: value['id'],
rank: value['id'].slice(0,3),
displayName: value['displayName'],
thisRound: value['round'],
thisMatch: value['match'],
status: type
};
if (type === true || type === 'undetermined') {
obj.winner = true;
}else{
obj.loser = true;
}
results.push(obj);
});
// console.log("results:", results);
return results;
},
tournamentStartTime: function(){
return Session.get('tournamentStartTime');
}
});
How do you recognize which state is current? You should post some code, routes.js, tournament.js and your view.blade, for better understanding what you really wanna do and for figure out, what the best pratice is. :)
I built out a custom pagination script to display data for my app. It works wonderfully. However, I am having a slight problem when it comes to trying to figure out how to grab a subset of the same paginated subscription.
Meteor.startup(function(){
Session.setDefault('page', 1);
Session.setDefault('recordCount', 0);
Session.setDefault('recordsPerPage', 10);
Session.setDefault('currentIndustry', null);
Session.setDefault('currentMapArea', null);
Session.setDefault('gmapLoaded', false);
});
Deps.autorun(function () {
Meteor.call('getJobsCount', Session.get('currentIndustry'), Session.get('currentMapArea'), function (err, count) {
Session.set('recordCount', count);
});
Meteor.subscribe('pagedRecords', Session.get('page'), Session.get('recordsPerPage'), Session.get('currentIndustry'), Session.get('currentMapArea'));
});
Template.gmap.rendered = function() {
if(!Session.get('gmapLoaded'))
gmaps.initialize();
}
var templateName = "jobs";
function plotCities(jobs) {
var addresses = _.chain(jobs)
.countBy('address')
.pairs()
.sortBy(function(j) {return -j[1];})
.map(function(j) {return j[0];})
.slice(0, 99)
.value();
gmaps.clearMap();
$.each(_.uniq(addresses), function(k, v){
var addr = v.split(', ');
Meteor.call('getCity', addr[0].toUpperCase(), addr[1], function(error, city){
if(city) {
var opts = {};
opts.lng = city.loc[1];
opts.lat = city.loc[0];
opts.population = city.pop;
opts._id = city._id;
gmaps.addMarker(opts);
}
});
});
}
Template[templateName].helpers({
selected: function(){
return Session.get('recordsPerPage');
}
});
Template[templateName].pages = function() {
var numPages = Math.ceil(Session.get('recordCount') / Session.get('recordsPerPage'));
var currentPage = Session.get('page');
var totalPages = Session.get('recordCount');
var prevPage = Number(currentPage) - 1;
var nextPage = Number(currentPage) + 1;
var html = '<div class="pagination-cont"><ul class="pagination">';
if (numPages !== 1) {
if (currentPage > 1) {
html += '<li>«</li>';
}
for (var i = currentPage; (i <= numPages) && (i - currentPage < 4); i++) {
if (i < 1) continue;
if (i !== currentPage)
html += '<li>' + i + '</li>';
else
html += '<li class="active">' + i + '</li>';
}
if (currentPage < numPages) {
html += '<li>»</li>';
}
}
html += '</ul></div>';
return html;
}
Template[templateName].jobs = function() {
var options = {};
var cursor;
if(!Session.get('currentMapArea')) {
cursor = Jobs.find({}, {limit: 500});
plotCities(cursor.fetch());
}
return Jobs.find({}, { limit: Session.get('recordsPerPage') });
}
Template[templateName].rendered = function(){
var select = $('#perPage');
var option = select.attr('_val');
$('option[value="' + option + '"]').attr("selected", "selected");
select.selectpicker({
style: 'btn-info col-md-4',
menuStyle: 'dropdown-inverse'
});
}
Template[templateName].events({
'click div.select-block ul.dropdown-menu li': function(e){
var selectedIndex = $(e.currentTarget).attr("rel");
var val = $('select#perPage option:eq(' + selectedIndex + ')').attr('value');
var oldVal = Session.get('recordsPerPage');
if(val != oldVal)
Session.set('recordsPerPage', Number(val));
},
'click .pageNum': function(e){
e.preventDefault();
var num = $(e.currentTarget).data('page');
Session.set('page', Number(num));
}
});
Currently, by default, only 10 records per page show up (unless the user selects from a drop-down a different amount). I have a plotCities function that I am using to try to plot the top 100 cities from the subset that is returned, however, I can't grab the top 100 because only 10 at a time show up.
Is there anyway to do what I am describing?
Ok, so the jobsPerCity and jobs are two totally different things, so I would use a separate on-fly-collection for the first one. Nothing will be stored in the database but the client will "think" that there is actually a jobsPerCity collection, which you can use to plot your map. The way you can achieve this is to define another named collection:
// only on the client !!!
var jobsPerCity = new Meteor.Collection("jobsPerCity");
On the server you will need to define a custom publish method:
Meteor.publish('jobsPerCity', function (options) {
var self = this;
var cities = new Meteor.Collection(null);
var jobToCity = {};
handle1 = Jobs.find({/* whatever condition you want */}).observeChanges({
added: function (id, fields) {
jobToCity[id] = fields.address.split(',')[0].toUpper();
cities.upsert({ _id: jobToCity[id] }, { $inc: { jobsCount: 1 } });
},
removed: function (id) {
cities.upsert({ _id: jobToCity[id] }, { $inc: { jobsCount: -1 } });
delete jobToCity[id];
},
changed: function (id, fields) {
// left as an exercise ;)
},
});
handle2 = cities.find({}, {sort: {jobsCount: -1}, limit: 100}).observeChanges({
added: function (id, fields) {
self.added('jobsPerCity', id, fields);
},
changed: function (id, fields) {
self.changed('jobsPerCity', id, fields);
},
removed: function (id) {
self.removed('jobsPerCity', id);
},
});
self.ready();
self.onStop(function () { handle1.stop(); handle2.stop(); });
});
and your good to go :)
EDIT (simple solution for more static data)
If the data is not going to be updated very often (as #dennismonsewicz suggested in one of his comments), the publish method can be implemented in a much simpler way:
Meteor.publish('jobsPerCity', function (options) {
var self = this, jobsPerCity = {};
Jobs.find({/* whatever condition you want */}).forEach(function (job) {
var city = job.address.split(',')[0].toUpper();
jobsPerCity[city] = jobsPerCity[city] !== undefined ? jobsPerCity[city] + 1 : 1;
});
_.each(jobsPerCity, function (jobsCount, city) {
self.added('jobsPerCity', city, { jobsCount: jobsCount });
});
self.ready();
});