Trouble filtering angular-meteor collection with infinite-scroll support - angularjs

I'm trying to create an interactive photo database where the user can sort through photos and filter by user, check multiple categories he or she is interested in, and sort the filtered data by date, number of favorites, etc. The filter/sorting criteria are selected by the user on the DOM and are stored from the client side to $scope.model. The quantity of data visible to the user would be controlled with infinite scroll.
I've created a repository of this example, and I've deployed it here. I've reproduced some of the relevant code from scroll.controller.js below:
Code
// infinite-scroll logic & collection subscription //
$scope.currentPage = 1;
$scope.perPage = 12;
$scope.orderProperty = '-1';
$scope.query = {};
$scope.images = $scope.$meteorCollection(function() {
query={};
// if filtered by a user...
if ($scope.getReactively('model.shotBy')) {
query.shotBy = $scope.model.shotBy;
};
// if category filter(s) are selected...
if ($scope.getReactively('model.category', true)) {
if ($scope.model.category.length > 0){
var categories = [];
for (var i=0; i < $scope.model.category.length; i++){
categories.push($scope.model.category[i]);
}
query.category = {$in: categories};
}
};
$scope.currentPage = 1; // reset
return Images.find(query, { sort: $scope.getReactively('model.sort')});
});
$meteor.autorun($scope, function() {
if ($scope.getReactively('images')){
$scope.$emit('list:filtered');
}
});
$meteor.autorun($scope, function() {
$scope.$meteorSubscribe('images', {
limit: parseInt($scope.getReactively('perPage'))*parseInt(($scope.getReactively('currentPage'))),
skip: 0,
sort: $scope.getReactively('model.sort')
});
});
$scope.loadMore = function() {
// console.log('loading more');
$scope.currentPage += 1;
};
Problem
I can scroll through the images fine, and the infinite-scroll feature seems to work. However, when I attempt to filter the images from the DOM, the only filtered results are those which were initially visible before scrolling, and scrolling doesn't make the rest of the images that meet the criteria show, despite using $scope.$emit to signal ngInfiniteScroll to load more (documentation).
EDIT: If the initial filtered list does, in fact, reach the bottom, scrolling will append properly. It seems to not append only if the initial filtered list doesn't reach the bottom of the screem.
Question
What can I change to make ngInfiniteScroll behave as I would expect on a filtered collection?
Any help, thoughts, or suggestions would be appreciated, and let me know if there's anything else you'd like to see. Thank you!

Well, it took almost all day to figure out, but I now have a working example at this Github repository and deployed here.
To summarize, I found I need to filter at both the collection and subscription levels to cause perPage to apply properly and for ngInfiniteScroll functioning. Also, I needed to send an event via $scope.$emit to ngInfiniteScroll to tell it to fire again in case the images array was too small and didn't reach the edge of the screen. See the Github repository for more details, if you're interested.
Updated relevant code
// infinite-scroll logic & collection subscription //
$scope.currentPage = 1;
$scope.perPage = 12;
$scope.query = {};
function getQuery(){
query={};
// if filtered by a user...
if ($scope.getReactively('model.shotBy')) {
query.shotBy = $scope.model.shotBy;
};
// if category filter(s) are selected...
if ($scope.getReactively('model.category', true)) {
if ($scope.model.category.length > 0){
var categories = [];
for (var i=0; i < $scope.model.category.length; i++){
categories.push($scope.model.category[i]);
}
query.category = {$in: categories};
}
};
return query;
}
$meteor.autorun($scope, function(){
$scope.images = $scope.$meteorCollection(function() {
$scope.currentPage = 1; // reset the length of returned images
return Images.find(getQuery(), { sort: $scope.getReactively('model.sort')});
});
$scope.$emit('filtered'); // trigger infinite-scroll to load in case the height is too small
});
$meteor.autorun($scope, function() {
$scope.$meteorSubscribe('images', {
limit: parseInt($scope.getReactively('perPage'))*parseInt(($scope.getReactively('currentPage'))),
skip: 0,
sort: $scope.getReactively('model.sort'),
query: getQuery()
});
});
$scope.loadMore = function() {
// console.log('loading more');
$scope.currentPage += 1;
};
I'm not sure if I've used best practices with this answer, so please feel free to chime in with suggestions.

Related

ui grid returning rows in the same order they are selected?

Hi am using UIgrid in an angularjs project and when I call the method gridApi.selection.getSelectedRows() to get the selected rows it returns an array with all the rows but in a random order. Ideally I want to get the rows in the same order they were selected (as if gridApi.selection.getSelectedRows() is backed by a queue) . Any idea how to achieve this please ?
This link to plunker shows the issue http://plnkr.co/edit/gD4hiEO2vFGXiTlyQCix?p=preview
You can implement the queue by yourself. Something like
$scope.gridOnRegisterApi = function(gridApi) {
gridApi.selection.on.rowSelectionChanged($scope, function(row) {
var selections =gridApi.selection.getSelectedRows();
// add sorted
selections.forEach(function(s){
if ($scope.mySelections.indexOf(s) === -1) {
$scope.mySelections.push(s);
}
});
// remove the ones that are not selected (use for to modify collection while iterating)
for (var i = $scope.mySelections.length; i >0; i--) {
if (selections.indexOf($scope.mySelections[i]) === -1) {
$scope.mySelections.splice(i, 1);
}
}
console.log($scope.mySelections);
row.entity.firstSelection = false;
if (row.isSelected) row.entity.firstSelection = (gridApi.selection.getSelectedCount() == 1);
});
};
I think there is a bug with that version of angular I was using there , if you upgrade the version in the plnkr to 1.6.1 it will behave as expected

Filter data by user with angularfire

I'm using angular-ui-fullcalendar to show and edit events. Users can log in and have unique uid when logged in. I want to use this to distinguish events made by current user from other events. I want to give current user events another backgroundColor.
What is the best way to do this??
I tried several things. My data looks like this:
```
database
bookings
-KWnAYjnYEAeErpvGg0-
end: "2016-11-16T12:00:00"
start: "2016-11-16T10:00:00"
stick: true
title: "Brugernavn Her"
uid: "1f17fc37-2a28-4c24-8526-3882f59849e9"
```
I tried to filter all data with current user uid like this
var ref = firebase.database().ref().child("bookings");
var query = ref.orderByChild("uid").equalTo(currentAuth.uid);
var bookings = $firebaseArray(query);
$scope.eventSources = [bookings];
This doesn't return anything. If I omit the filter in line 2 it returns all bookings as expected. But even if the filter worked it would not solve my problem, because I want to fetch both current user events and all other events. Firebase does not have a "not equal to" filter option...
I tried to loop through each record and compare uids and setting backgroundColor if condition was met:
var ref = firebase.database().ref().child("bookings");
var bookings = $firebaseArray(ref);
bookings.$ref().on("value", function(snapshot) {
var list = snapshot.val();
for (var obj in list) {
if ( !list.hasOwnProperty(obj) ) continue;
var b = list[obj];
if (b.uid === currentAuth.uid) {
b.className = "myBooking";
b.backgroundColor = "red";
}
}
});
$scope.eventSources = [bookings];
But this causes asynchronous problems so the 'bookings' array assigned to $scope.eventSources wasn't modified. I tried to move the $scope.eventSources = [bookings] inside the async code block but FullCalendar apparently can't handle that and renders nothing.
I also tried this but no luck either:
bookings.$loaded()
.then(function(data) {
$scope.eventSources = [data];
})
.catch(function(error) {
console.log("Error:", error);
});
What is the best solution to my problem?
If you're looking to modify the data that is loaded/synchronized from Firebase, you should extend the $firebaseArray service. Doing this through $loaded() is wrong, since that will only trigger for initial data.
See the AngularFire documentation on Extending $firebaseArray and Kato's answer on Joining data between paths based on id using AngularFire for examples.

Query from minimongo of large number of records stucks and hangs browser

I am building a page for admin in angular-meteor.
I have published all the records from a collection: "posts" and have taken the subscription of all the records on front end.
$meteor.subscribe('posts');
In the controller, if I select the cursors of all records from minimongo it works fine like:
$scope.posts = $meteor.collection(Posts);
But I want to display pagination, so for that I want limited records at a time like:
$scope.posts = $meteor.collection(function(){
return Posts.find(
{},
{
sort: {'cDate.timestamp': -1},
limit: 10
}
);
});
It stucks with the query in minimongo. And the browser hangs.
"posts" collection contains only 500 records. It was working fine when I had 200 records.
Can anyone give me an idea whats wrong with my code and concepts?
EDIT:
Okay! It worked fine when I commented the $sort line from query like this:
$scope.posts = $meteor.collection(function(){
return Posts.find(
{},
{
//sort: {'cDate.timestamp': -1},
limit: 10
}
);
});
But I need to sort the records. So what should I do now?
EDIT:
Also tried adding index to the sort attribute like this:
db.Posts.ensureIndex({"cDate.timestamp": 1})
Still same issue.
Change your publication to accept a parameter called pageNumber like this
Meteor.publish('posts', function (pageNumber) {
var numberOfRecordsPerPage = 10;
var skipRecords = numberOfRecordsPerPage * (pageNumber - 1);
return Post.find({
"user_id": user_id
}, {
sort: { 'cDate.timestamp': -1 }
skip: skipRecords,
limit: numberOfRecordsPerPage
});
});
On client side, I didn't work with angular-meteor much. You can create a pageNumber property under your current scope using this.pageNumber or $scope.pageNumber. Update this pageNumber variable whenever your pagination page is clicked. Whenever this variable is changed, subscribe using the current page number.
If it is using standard blaze template, I would do it using a reactive var or session var in an autorun like this.
In template html:
<template name="postsTemplate">
<ul>
<!-- you would want to do this list based on total number of records -->
<li class="pagination" data-value="1">1</li>
<li class="pagination" data-value="2">2</li>
<li class="pagination" data-value="3">3</li>
</ul>
</template>
In template js:
Template.postsTemplate.created = function () {
var template = this;
Session.setDefault('paginationPage', 1);
template.autorun(function () {
var pageNumber = Session.get('paginationPage');
Meteor.subscribe('posts', pageNumber);
});
}
Template.postsTemplate.events(function () {
'click .pagination': function (ev, template) {
var target = $(ev.target);
var pageNumber = target.attr('data-value');
Session.set('paginationPage', pageNumber);
}
});
This way, you will have a maximum of 10 records at any point in time on the client, so it will not crash the browser. You might also want to limit the fields that you send to client using something like this
Meteor.publish('posts', function (pageNumber) {
var numberOfRecordsPerPage = 10;
var skipRecords = numberOfRecordsPerPage * (pageNumber - 1);
return Post.find({
"user_id": user_id
}, {
sort: { 'cDate.timestamp': -1 }
skip: skipRecords,
limit: numberOfRecordsPerPage,
fields: {'message': 1, 'createdBy': 1, 'createdDate': 1 } //The properties inside each document of the posts collection.
});
});
And finally you will need the total number of records in posts collection on client side, to show the pagination links. You can do it using a different publication and using the observeChanges concept as mentioned in the official documentation here
// server: publish the current size of a collection
Meteor.publish("posts-count", function () {
var self = this;
var count = 0;
var initializing = true;
// observeChanges only returns after the initial `added` callbacks
// have run. Until then, we don't want to send a lot of
// `self.changed()` messages - hence tracking the
// `initializing` state.
var handle = Posts.find({}).observeChanges({
added: function (id) {
count++;
if (!initializing)
self.changed("postsCount", 1, {count: count});
},
removed: function (id) {
count--;
self.changed("postsCount", 1, {count: count});
}
// don't care about changed
});
// Instead, we'll send one `self.added()` message right after
// observeChanges has returned, and mark the subscription as
// ready.
initializing = false;
self.added("postsCount", 1, {count: count});
self.ready();
// Stop observing the cursor when client unsubs.
// Stopping a subscription automatically takes
// care of sending the client any removed messages.
self.onStop(function () {
handle.stop();
});
});
// client: declare collection to hold count object
PostsCount = new Mongo.Collection("postsCount");
// to get the total number of records and total number of pages
var doc = PostsCount.findOne(); //since we only publish one record with "d == 1", we don't need use query selectors
var count = 0, totalPages = 0;
if (doc) {
count = doc.count;
totalPages = Math.ceil(count / 10); //since page number cannot be floating point numbers..
}
Hope this helps.
Browser crashing because there is only so much data that it can load in it's cache before itself cashing out. To your question about what happens when you need to demand a large number of documents, take that process away from the client and do as much on the server through optimized publish and subscribe methods / calls. No real reason to load up a ton of documents in the browser cache, as minimongo's convenience is for small data sets and things that don't need to immediately sync (or ever sync) with the server.
you should sort on server side, you can find what you are looking for here
meteor publish with limit and sort
You need to consider sort and limit strategies:
Sort on the server if you're extracting top values from a large set used by all clients. But usually better first filter by User who needs the data, and sort on the filtered collection. That will reduce the dataset.
Then Publish that sorted / limited subset to the client, and you can do more fine grained / sorting filtering there.
You should use server side limit instead of client side. That will make your app faster and optimize.
For more please check this link.
link here

ng-table reset page to 1 on sort

I am using ng-table in my application, I was looking to reset current page to 1 when user changes sort order. I gone through ng-table documentation, but no use.
You should be able to do this via the page() function of NgTableParams:
$scope.tableParams.page(1);
For that, you need to subscribe to the ngTableEventsChannel.afterReloadData(); that is fired after sorting changes. There's an example that logs the events and shows how to subscribe.
This works for me:
var vm = this;
vm.tableParams = new NgTableParams();
vm.recentPage = 1;
ngTableEventsChannel.onAfterDataSorted(function() {
vm.tableParams.page(1);
vm.tableParams.reload();
}, $scope, function(tableParams) {
var reset = tableParams._params.page !== 1 && vm.recentPage === tableParams._params.page;
vm.recentPage = tableParams._params.page;
return reset;
});

Backbone - trying to make a filter on a collection with nested object

i'm trying to filter a collection which has models with some nested object. Unfortunately, my result are always empty.
So my models returned in the collection are build like this:
My goal is simple:
I have a view with a list of tag and a content view with all the questions. When a user click on tag, for example, "c#", i want to filter my collection to just return questions with tag "c#"
Before i was doing a fetch on my server and it was working fine, but it was not optimize.
I already have a collection with all the questions so why make a new call, a filter is a better solution i think.
But i didn't succeded with my filter and i don't know if it's possible to do. For now i put my filter in my router because it's more easy to test.
i can't make a filter like this because i have an array of object
getQuestionsByTags: function(query) {
var test = this.questionsCollection.filter(function(model) {
return model.attributes.tags.name == query;
})
console.log('result');
console.log(test);
},
So i was thinking to make a loop but my result is always an empty array.
getQuestionsByTags: function(query) {
var test = this.questionsCollection.filter(function(model) {
_.each(model.attributes.tags, function(tag) {
return tag.name == query;
})
})
console.log('result');
console.log(test);
},
It's maybe simple, but i don't know what to do.
Thanks in advance :)
i've just found a solution that work.
getQuestionsByTags: function(query) {
var flag;
var test2 = this.questionsCollection.filter(function(model) {
flag = false;
_.each(model.attributes.tags, function(tag) {
if(tag.name == query) {
flag = true;
}
})
if(flag) {
return model.attributes;
}
})
console.log('result');
console.log(test2);
},
i put a flag. If he turn true inside the loop, the model has this tag so i return it.
I think it's not very conventional, so if someone have another solution, feel free to post it :)

Resources