I'm trying to create a infinite scroll feature in my application but it feels a bit abstract. I want to use ui-scroll and this fiddle shows a simple example of how it works.
I've read the readme and looked through some examples also I've integrated the example in my project and got it working, but I can't figure out on how to combine this with data from my own database.
I have a database table called movies. The movies have a few values such as title, release_date, image_url
How would I insert that data into the $scope.movieDataSource so I can use it in my view?
$http.get(('/movies.json'), {
cache: true
})
.success(function(data, status, headers, config) {
if (status == 200) {
$scope.userMovies = data;
} else {
console.error('Error happened while getting the user list.')
}
$scope.movieDataSource = {
get: function(index, count, callback) {
var i, items = [$scope.userMovies], item;
var min = 1;
var max = 1000;
for (i = index; i < index + count; i++) {
if (i < min || i > max) {
continue;
}
item = {
title: $scope.userMovies.title,
imageURL: $scope.userMovies.poster_path
};
items.push(item);
}
callback(items);
}
}
});
I've tried to create an example of what I'm trying to get at. I use a http.get to fill my userMovies scope with records from my database and I want to use those records as items in the movieDataSource.
But when I visit the page I that ui-scroll does add results in the container, but it does not show content.
<div class="imageCell ng-scope" ui-scroll="item in movieDataSource">
<img title="">
</div>
If I console.log("movieDataSource" + $scope.movieDataSource) it shows me movieDataSource[object Object].
You are making this more complex than necessary. The uiScroll directive is a replacement for ngRepeat, which takes a Data Source with 3 properties:
index indicates the first data row requested
count indicates number of data rows requested
success function to call when the data are retrieved. The implementation of the service has to call this function when the data are retrieved and pass it an array of the items retrieved. If no items are retrieved, an empty array has to be passed.
in your case, you have an array of items. Each time the index or count changes, the success fires, and this function should return a subset of your array from index to index + count. There are multiple ways to accomplish this. The example you posted uses a for loop to iteratively push items into the array. You could also use the Array.slice() method.
Option 1:
$scope.movieDataSource = {
get: function(index, count, callback) {
var i, items = [], item;
for (i = index; i < index + count; i++) {
item = $scope.userMovies[i];
items.push(item);
};
callback(items);
}
}
Option 2:
$scope.movieDataSource = {
get: function(index, count, callback) {
var items = $scope.userMovies.slice(index, index + count);
callback(items);
}
}
As for your HTML, it should be identical to if you were using ng-repeat:
<div ui-scroll="item in movieDataSource">
{{item.title}}
<img title="{{item.title}}" ng-src="{{item.poster_path}}"></img>
</div>
Apparently ui-scroll calls the given object "movieDataSource" with an index and a count. It then expects the function to push all items between index and index + count in the returned array.
That means that you have to implement code that fetches the respective items from your DB (via REST or however you access your data) and insert the returned records in the items array.
Related
I am building an application in NodeJS and AngularJS.
I am building a multi-column search functionality where the user can type in search keywords into separate searchboxes (at the top of each column) and retrieve the results based on the column.
So far I have a single searchbox that searches all attributes at the same time.
How can I implement multiple individual searchboxes that will return results based on multiple attributes?
Note: I want to implement this on the server-side for performance reasons. (I know that I can simply use HTML attributes | filter:column1 | filter:column2 but want to avoid this technique if possible).
Here is the code I have so far. I am thinking that I need to pass in some sort of "searchBy" variable that is set on the view and then update the search method to search by multiple query/attribute pairs.
//Search service factory
//Initialize filtered items and get search results
function search(items, query) {
this.filteredItems = $filter('filter')(items, function (item) {
for(var attr in item) {
if (searchMatch(item[attr], query))
return true;
}
return false;
});
return this.filteredItems;
}
function searchMatch(haystack, needle) {
if (!needle) {
return true;
}
return haystack.toString().toLowerCase().indexOf(needle.toLowerCase()) !== -1;
};
//Controller
vm.filteredItems = vm.search(vm.unfilteredItems, vm.query);
//View
input(type='text', ng-model='vm.query', ng-change='vm.search(vm.unfilteredItems, vm.query)', placeholder='Search')
I was able to solve this by first creating an array of objects for each search box then repeating those boxes in the view with the ng-repeat attribute.
//Controller
var vm = this;
var vm.unfilteredItems; //data source query removed for brevity
//Initialize search inputs
vm.search_by_inputs = [
{search_column: 'id', search_query: ''},
{search_column: 'requester', search_query: ''},
{search_column: 'dataowner', search_query: ''}
];
function initSearch() {
vm.filtered_items = vm.search(vm.unfiltered_items, vm.search_by_inputs);
}
//View
input.input-large.search-query(type='text', value='{{search_by.search_query}}', ng-model='search_by.search_query' ng-change='vm.initSearch()', placeholder='Search')
The next step is to loop over the search_by_inputs object in the controller and create a new object with only the inputs that have search values entered into the searchboxes in the view. Then in the search method the built-in "filter" component iterates each item, and inside that loop each of the search terms is checked against that value with the column name that matches the property.
/*
* Create new array of objects with only elements that have search values to optimize loop inside filter
* #search_by_inputs array of objects each has a key search_column and a value search_query
*/
function optimizeSearchProperties(search_by_inputs) {
search_by_properties = [];
for (var i = 0, len = search_by_inputs.length; i < len; i++) {
//If this column input box has query text
if (search_by_inputs[i].search_query) {
search_by_properties.push(search_by_inputs[i]);
}
}
return search_by_properties;
}
/*
* #haystack search item
* #needle search term
*/
function searchMatch(haystack, needle) {
if (!needle) {
return true;
}
return haystack.toString().toLowerCase().indexOf(needle.toLowerCase()) !== -1;
}
/*
* Create filtered items object by filtering search results
* #items original array of objects returned by database query result
* #search_by_inputs array of objects each has a key search_column and a value search_query
*/
function search(items, search_by_inputs) {
var search_by_properties = optimizeSearchProperties(search_by_inputs);
//If there are no search properties input by requester then return all items
if (search_by_properties.length === 0) {
this.filtered_items = items;
return this.filtered_items;
}
this.filtered_items = $filter('filter')(items, function (item) {
var search_result = true;
//Loop over all search by input textboxes
for (var n = 0, len = search_by_properties.length; n < len; n++) {
//If there is no query text
if (!search_by_properties[n].search_query) {
//Continue to next element in array
continue;
//Else if element has a property that matches search input column name
} else if (item[search_by_properties[n].search_column]) {
if (!searchMatch(item[search_by_properties[n].search_column], search_by_properties[n].search_query)) {
search_result = false;
break;
}
}
}
return search_result;
});
return this.filtered_items;
}
I would be glad to have some feedback on this solution in terms of optimization, performance, technique, etc. Thanks!
I have written this custom filter for AngularJS, but when it runs, I get the infinite digest loop error. Why does this occur and how can I correct this?
angular.module("app", []).
filter('department', function(filterFilter) {
return function(items, args) {
var productMatches;
var output = [];
var count = 0;
if (args.selectedDepartment.Id !== undefined && args.option) {
for (let i = 0; i < items.length; i++) {
productMatches = items[i].products.filter(function(el) {
return el.Order__r.Department__r.Id === args.selectedDepartment.Id;
});
if (productMatches.length !== 0) {
output[count] = {};
output[count].products = productMatches;
output[count].firstProduct = items[i].firstProduct;
count++;
}
}
}
return output;
};
}).
This is the relevant HTML:
<tr class='destination' ng-repeat-start='pickupAccount in pickupAccounts | department : {"selectedDepartment": selectedDepartment, "option": displayExclusive }'>
<!-- td here -->
</tr>
displayExclusive is boolean.
I have written this custom filter for AngularJS, but when it runs, I get the infinite digest loop error.
Keep in mind that filter should return array of the same object structure. When we activate filter, it fires digest cycle that will run over our filter again. If something changed in output list - fires new digest cycle and so on. after 10 attempts it will throw us Infinite Digest Loop Exception
Testing
This empty filter will works (100%). Actually we do nothing here but return the same object that filter receives.
filter('department', function(filterFilter) {
return function(items, args) {
var output = items;
return output;
};
})
Now the main idea is: write some condition to push to output objects from input list a.e. items based on some if statement, a.e.
var output = [];
if (args.selectedDepartment.Id !== undefined && args.option) {
angular.forEach(items, function(item) {
if(<SOME CONDITION>) {
output.push(item);
}
});
}
By this way it will work too.
our case:
we have this logic:
productMatches = items[i].products.filter(function(el) {
return el.Order__r.Department__r.Id === args.selectedDepartment.Id;
});
if (productMatches.length !== 0) {
output[count] = {};
output[count].products = productMatches;
output[count].firstProduct = items[i].firstProduct;
count++;
}
Here we completely modified object that has been stored in output.
So next digest cycle our items will change again and again.
Conclusion
The main purpose of filter is to filter list and not modify list object content.
Above mentioned logic you wrote is related to data manipulation and not filter. The department filter returns the same length of items.
To achieve your goal, you can use lodash map or underscorejs map for example.
This happens when you manipulate the returned array in a way that it does not match the original array. See for example:
.filter("department", function() {
return function(items, args) {
var output = [];
for (var i = 0; i < items.length; i++) {
output[i] = {};
output[i] = items[i]; // if you don't do this, the next filter will fail
output[i].product = items[i];
}
return output;
}
}
You can see it happening in the following simplified jsfiddle: https://jsfiddle.net/u873kevp/1/
If the returned array does have the same 'structure' as the input array, it will cause these errors.
It should work in your case by just assigning the original item to the returned item:
if (productMatches.length !== 0) {
output[count] = items[i]; // do this
output[count].products = productMatches;
output[count].firstProduct = items[i].firstProduct;
count++;
}
output[count] = {};
Above line is the main problem. You create a new instance, and ng-repeat will detect that the model is constantly changed indefinitely. (while you think that nothing is changed from the UI perspective)
To avoid the issue, basically you need to ensure that each element in the model remains the 'same', i.e.
firstCallOutput[0] == secondCallOutput[0]
&& firstCallOutput[1] == secondCallOutput[1]
&& firstCallOutput[2] == secondCallOutput[2]
...
This equality should be maintained as long as you don't change the model, thus ng-repeat will not 'wrongly' think that the model has been changed.
Please note that two new instances is not equal, i.e. {} != {}
I am trying to delete the one or more selected items in the list in angular js.
I tried to add the items in the following way:
$scope.selectedNodes = [];
$scope.addItem = function(e) {
$scope.selectedNodes.push({
id :$scope.selectedNodes.length + 1,
Nodeid: e.item.id,
title: e.item.text});
$scope.$apply();
}
html is as below:
<select ng-model="selectedItems" multiple ng-multiple="true">
<option ng-repeat="node in selectedNodes" value="{{node}}">{{node}}</option>
</select>
<button ng-click="remove(selectedItems)" type="submit">
Remove
</button>
The above html is listing fine with all the items.
Now I am trying to delete one or more items from the list, so the code I have written is:
$scope.remove = function (nodes) {
alert(nodes); // it's giving the selected records info, no problem with it
angular.forEach(nodes, function (node) {
var index = $scope.selectedNodes.indexOf(node);
alert(index) //problem here, it's always -1
$scope.selectedNodes.splice(index, 1);
});
};
The above code is removing the last item if one item is selected. And if more than one is selected, let's say two, it's then removing the last two records.
The index value is always -1 for any no. of iterations in the foreach loop.
Could anyone please help with the above code to delete one or more selected records and the list should get refreshed. No problem with refreshing for the above code.
I tried as you mentioned below, but no luck.
$scope.remove = function (nodes) {
alert(nodes); // it's dispalying correct results
for(var i = 0; i< nodes.length; i++)
{
alert(nodes.length); // correct result, no problem
alert(nodes[i]); //correct result, no problem
alert(nodes[i].Nodeid); // problem, value Nodeid is Undefined
for (var idx = 0; idx < $scope.selectedNodes.length; idx++) {
alert($scope.selectedNodes[idx].Nodeid);
if ($scope.selectedNodes[idx].Nodeid == nodes[i].Nodeid) {
$scope.selectedNodes.splice(idx, 1);
break;
}
}
};
};
You're trying to locate the node using indexOf, which compares values using the strict '===' operator, and you're trying to compare objects.
I think it would be easier for you to use one of the many libraries outthere for collection manipulation (lodash, underscore, etc) but if you want to do it as you were doing then this is the code:
$scope.remove = function (nodes) {
angular.forEach(nodes, function (node) {
for (var idx = 0; idx < $scope.selectedNodes.length; idx++) {
if ($scope.selectedNodes[idx].Nodeid == node.Nodeid) {
$scope.selectedNodes.splice(idx, 1);
break;
}
}
});
};
If the number of nodes to delete is very high and you're concerned about optimization then you can iterate the selectedNodes array in inverse order and be deleting the node if it's in the nodesToDelete collection. The code structure is similar, just iterating outside the selectedNodes and inside the nodesToDelete.
My filter runs 3 times, the first execution gives the results the I want, but it runs again 2 times returning nothing. My question is why it is calling the filter again 2 times?
This is my code to my filter
.filter("range", function ($filter) {
return function (data, page, size) {
if (angular.isArray(data) && angular.isNumber(page) && angular.isNumber(size)) {
var start_index = (page - 1) * size;
if (data.length < start_index) {
return [];
} else {
var results = $filter("limitTo")(data.splice(start_index), size);
console.log(results);
return results;
}
} else {
return data;
}
}});
this is how I apply my filter to my view
<tr ng-repeat="product in products | range:selectedPage:pageSize" ng-click="productClicked(product)">
<td>{{product.prod_name}}</td>
<td>{{product.company}}</td>
</tr>
The report from my console is like this
[Object, Object, Object, Object, Object]
[]
[]
You have a bug there - in the filter you're doing data.splice(start_index) - this changes the product array itself and ends up with your data being completely removed from the array...
Change it to data.slice(start_index) and everything should work.
By the way, beware of doing paging via ngRepeat+filters - if your array becomes large, you will have performance issues.
Is there a way to configure/work with the angular $filter so that you can pass data to it gradually an not have it always iterate over the whole data but just the previous data filtered ?
I am currently trying to split the a user provided string to run multiple sequential filers on the data. So if I have a data like so: [{name:foo, city:bar},...x250] I could type in the search box "Paris" from that point I should be iteration only on the place in Paris and then type Hotel and then Hilton.... The problem is the filter runs each and every time on the whole data set... Does anyone know how to run the filter sequentially in angular ?
updates...
http://jsfiddle.net/Bretto/ZM4Qa/12/
.filter('searchFilter', function ($filter) {
return function (array, q) {
console.log('data-entry: ', array.length);
var filter = $filter('filter');
var lookFor = function (word, data, cb) {
var dataRes = filter(data, word);
cb(dataRes);
}
var search = function (index, words, data, cb) {
var word = words[index];
lookFor(word, data, function (dataRes) {
console.log('data-current: ', dataRes.length);
if (index < words.length - 1) {
index += 1;
search(index, words, dataRes, cb);
} else {
cb(dataRes);
}
});
}
if (q && q.text) {
var words = q.text.split(' ');
var index = 0;
if(array){
//console.log(array.length);
}
search(index, words, array, function (dataRes) {
array = dataRes;
});
}
return array;
}
})
So this is an attempt to create a filter that gradually iterates over the previous filtered results (type multiple words separated by a space in the input...) the issue is that the filter always starts from the whole data.
is this clearer now ?
Please use multiple filters, so you don't have to go tweaking everything
Take this example as a starting point
http://jsfiddle.net/ed9A2/1/
<tr ng-repeat="player in playersFound = (players | filter:{id: player_id, name:player_name, age:player_age})">