AngularJS - sorting/organizing a md-virtual-repeat - angularjs

Is there a way to organize/sort a md-virtual-repeat?
Im feeding it an array of data for example: {a: "123", b: "321"} and then I can do {{loop.a}} and {{loop.b}} for example.
But what if I wanted to make it so that it will show the highest value in B at the top, and its lowest value on the bottom. Descending Order. How would I achieve that?
My code for the actual ng-repeat process is following:
window.proxies=[];
$scope.nproxies={
_l:0,
_t:0,
getItemAtIndex: function(i) {
if (i>this._l) {
if (this._t<=i) {
this._t+=5;
this._l=this._t;
};
}
return window.proxies[i];
},
getLength: function() {
return window.proxies.length < 8 ? 8 : window.proxies.length;
}
};
Basically its setup to get only if there is actually more to get, but always have atleast 8 "rows" setup, so essentially my table will atleast always have 8 minimum rows (While those rows could be empty) its just a better way to make it look like a proper table.
It does everything essentially like the official demo.
But as you can see, there isnt exactly a way for me to change organization due to how it gets the values, by index :/
The only way I see of filtering anything here is by grabbing window.proxies and shuffling, ordering whatever and then returning it back to window.proxies. The issue with that is its relatively slow, will often UI block for large values and could cause race-conditions.
Any ideas appreciated!

The best way to have smooth UI with sorted md-virtual-repeat displaying large amount of sorted data is to sort your dataset on server side, otherwise you'll get very awkward user experience. Consider the case, when you load another portion of data, and the items of this data should be inserted into different locations of the sorted dataset. This will cause either your user looses actual position while scrolling or not see new data at all (or both).
If your dataset is not too large (less than 20k records in case of using AngularJS), you can load the whole dataset to client side, sort it and then feed it to md-virtual-repeat, which will display only the items that may be displayed in the container (and not the whole dataset) and reuse existing items while scrolling.
Here is an example of client-side sorting of 10K records, where proxies are sorted by proxy port in descending order: https://plnkr.co/edit/mbMjakZFeHoQeM5OLCfh?p=preview . It took 12ms on my machine. I don't know how much metadata on proxies you have, but in my case 10K records size was 50KB, which is smaller than size of most of the decorative images on the page...
HTML
<div ng-controller="ctrl as ctrl" ng-cloak="">
<p>Processing time: {{ctrl.sortTime}}ms</p>
<md-content layout="column">
<md-virtual-repeat-container id="vertical-container">
<div md-virtual-repeat="item in ctrl.items" md-on-demand="" class="repeated-item" flex="">
{{item.n}}:{{item.p}}
</div>
</md-virtual-repeat-container>
</md-content>
</div>
JavaScript
angular
.module('app', ['ngMaterial'])
.controller('ctrl', function($http, $scope) {
var ctrl = this;
Promise.all([ // emulate pagination
$http.get('data0.json'),
$http.get('data1.json')
]).then(function(responses) {
$scope.$apply(function() {
var t = Date.now();
ctrl.items = {
data: responses.reduce(function(a, r) {
return a.concat(r.data) // Combine all responses
}, []).sort(function(a, b) { // Sort by 'p' property in descending order
if(a.p > b.p) return -1;
if(a.p < b.p) return 1;
return 0;
}),
getItemAtIndex: function(idx) { return ctrl.items.data[idx]; },
getLength: function() { return ctrl.items.data.length }
};
ctrl.sortTime = Date.now() - t;
});
});
});
Note:
Also, browser has only one UI thread, and memory allocated to web workers is isolated, so the UI may be slow, but you can never get into situation of race-condition when using JavaScript (either server side or client side).

Related

Angularjs filtering JSON data based on date range

I have an angular app which will displayed data from database, the app will fetch the data as json from the server.
in the form i have a checkbox and 2 input type=date which is one is start date and the other is end date, the checkbox will enable or disable those input type=date.
So i will fetch all data from database, whenever the user enable the checkbox, they will have select the date and the app will filter based on their selection, if checkbox unchecked then the app will displayed all data.
This is how i populate the data
app.js
$http({
method: 'POST',
url: "api/Enquiry.php",
data: {
matcode:matcode,
location: location,
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
}).then(function(response){
$scope.Tablelist = response.data;
console.log(JSON.stringify(response.data));
}, function(response){
console.log("failed");
})
So when the input type date is change, how can i just display the data in Tablelist according to the selected start and end date >=startdate and <=enddate
Is it possible to use the builtin filterFilter? if yes, kinda point me to the right direction.
Here is how you filter between dates, the key is the filter you see in the docs it takes a function.
The filter function I created looks like:
vm.range = function (value, index, array) {
if (value.date < new Date('2016-05-12T12:00:00Z') && value.date > new Date('2016-05-12T09:00:00Z')) {
return true;
}
return false;
}
So the value is the current object to compare, I have chosen two arbitrary dates (these can be gotten from the UI). All I do is just use the logical operators.
If you return true from the function then you are saying keep it. if you return false then you are saying nope!
This can be simplified to:
vm.range = function (value, index, array) {
return (value.date < new Date('2016-05-12T12:00:00Z') && value.date > new Date('2016-05-12T09:00:00Z'));
}
Simple days.
Oh and UI is easy:
<li ng-repeat="f in vm.original | filter: vm.range">
{{f.date}}
</li>
It use the pipe symbol | to defined the next part is going to be a filter of some kind. We use the filter filter (Yes, that is how stupid angular 1 is....) Then we just pass the vm.range function expression to this filter and it will be executed against each object, very similar to a foreach
In relation to your example of if being returned in the $http response, you can still do the UI the same as mine, because of two-way binding as soon as $scope.Tablelist is updated, the filter is re-run over the top. Just remember this isn't very performant for large data sets, so you may want to pull the filter into the javascript:
$scope.Tablelist = $filter('filter')(response.data, $scope.range);
Why is this more performant then in the UI. Because it doesn't cause unnecessary UI reflow till the data is filtered, I am unsure as to whether the filter does this in the UI, but I know this won't.

Client error "Error: documentMatches needs a document" when removing an element from an array by index

I'm seeing an error when I remove an element from an array in a collection. I'm removing the element by index.
What's weird is that the server code works but in the console I see an error "Exception while simulating the effect of invoking '/patterns/update' Error: documentMatches needs a document".
I've spent ages trying to work out what the problem is and I'm stumped! See the code below for a minimal example that reproduces the problem.
(The only references I can find to this error don't seem relevant - I think this poster was identifying elements by attributes not index:
Removing an object from an array inside a Collection. And this post seems to relate to Angular, which I'm not using, and the question doesn't seem to have been answered: Minimongo errors when I try to update a document containing an array)
Any ideas why the query might fail on the client but not the server? The error happens at the point when I pull the element from the array with
Patterns.update({_id: pattern_id}, {$pull : {"my_array" : null}});
Full code:
HTML
<head>
<title>Array test</title>
</head>
<body>
{{> hello}}
</body>
<template name="hello">
<p>Values in the array: {{array}}</p>
<button class="add">Add item to array</button>
<button class="remove">Remove item from array</button>
</template>
JavaScript:
Patterns = new Mongo.Collection('patterns');
if (Patterns.find().fetch().length == 0)
{
Patterns.insert({name: "My document", my_array: [0] });
}
if (Meteor.isClient) {
Template.hello.helpers({
array: function () {
return Patterns.find().fetch()[0].my_array;
}
});
Template.hello.events({
'click button.add': function () {
pattern_id = Patterns.find().fetch()[0]._id;
var new_value = Patterns.findOne(pattern_id).my_array.length;
Patterns.update({_id: pattern_id}, {$push: {my_array: new_value}});
},
'click button.remove': function() {
pattern_id = Patterns.find().fetch()[0]._id;
var index = Patterns.findOne(pattern_id).my_array.length -1;
var obj = {};
obj["my_array." + index ] = "";
Patterns.update({_id: pattern_id}, {$unset : obj});
// THIS LINE CAUSES THE CONSOLE ERROR
Patterns.update({_id: pattern_id}, {$pull : {"my_array" : null}});
}
});
}
You are encountering a current limitation of Minimongo, that is mentioned in the source here
// XXX Minimongo.Matcher isn't up for the job, because we need
// to permit stuff like {$pull: {a: {$gt: 4}}}.. something
// like {$gt: 4} is not normally a complete selector.
// same issue as $elemMatch possibly?
However if you check your collection after that exception you will see that the item has been removed from your array. This would have happened on the server side (with the real MongoDB, then synchronised back with your client side collection), as this error is only impacting the client side latency compensation operation on the client collection, which is then corrected when the update arrives from the server.
My suggestion given that you are encountering minimongo limitations would be to move your updates into meteor methods where you always interact mongodb on the server, though you will need to add stub methods to handle any latency compensation you require on the client.

Angular ngRepeat not preserving order of Firebase priority items?

Pretty new to Angular & Firebase here, but noticed an odd behavior querying and presenting ordered data that isn't addressed anywhere else yet...
I'm pushing data to firebase and setting priority with a descending negative value so that newest data is on top of the list.
When retrieving the ref with child_added events, I can confirm the data is arriving in the correct order; however, when used with ngRepeat, the data is somehow getting reversed (newest data appears on bottom of ngRepeat list).
If I use something like .append() the data is correctly ordered. But would rather do it the 'Angular' way with ngRepeat.
// example html binding
// ====================================
<ul>
<li ng-repeat="(itemID, item) in list">{{itemID}}</li>
</ul>
// example controller code
// ====================================
var laApp = angular.module('laApp', ['firebase']);
laApp.controller('laAppCtrl', function ($scope, $timeout){
var ref = new Firebase('https://ngrepeatbug.firebaseio.com');
$scope.pushPriority = function(){
var uid = new Date().getTime();
var priority = 0 - uid;
// set with -priority so newest data on top
ref.push().setWithPriority(uid, priority);
}
$scope.list = {};
ref.orderByPriority().on('child_added', function(snap){
$timeout(function(){
var snapID = snap.key();
var snapVal = snap.val();
//repeat method
$scope.list[snapID] = snap.val();
//append method
$('ul.append').append("<li>" + snapVal + "</li>")
})
})
});
Pen comparing ngRepeat and append methods:
http://codepen.io/juddam/pen/dIiLz
I've read other solutions that either convert the $scope.list object into an array that is then used with $filter or reversing order on client, but this defeated the whole purpose of storing data by priority and having a straightforward method for querying and presenting ordered data.
Know orderByPriority is new to firebase v2.0 so wondering if bug or am I missing something obvious?
You're adding the children to an object with this:
$scope.list[snapID] = snap.val();
Even though this looks like you're adding to an array, you're actually adding to a regular object. And as #ZackArgyle says in his comment: the keys in an object have no guaranteed order.
If you want to maintain the order of the items, you should push them into an array.
$scope.list.push(snap.val());
This adds them with numeric indices, which will maintain their order.
If you want to both maintain the order of the items and their key, you will have to manage them in an array.
$scope.list.push({ $id: snap.key(), value: snap.val() });
That last approach is an extremely simplified version of what AngularFire does when you call $asArray().

Angularjs autoselect dropdowns from model

I'm trying display some data loaded from a datastore and it's not reflecting changes on the UI. I created an example to show a general idea of what I'm trying to achieve.
http://plnkr.co/edit/MBHo88
Here is the link to angularjs example where they show when on click then dropdowns are clear out. If you replace the expression with one of the colors of the list dropdowns are well selected. Does this type of selection only work on user events?
http://docs.angularjs.org/api/ng.directive:select
Help is appreciated!!!
Actually the problem is that ngSelect compares objects using simple comparition operator ('=='), so two objects with same fields and values are considered as different objects.
So you better use strings and numbers as values ('select' parameter in expression of ngSelect directive).
Here is kind of solution for your plunker.
Aslo there are some discussion about this topic on GitHub:
https://github.com/angular/angular.js/issues/1302
https://github.com/angular/angular.js/issues/1032
Also as I headred there is some work in progress about adding custom comparor/hashing for ngSelect to be able to use ngSelect more easier on objects.
One mistake in the initialization of your controller. You have to refer to the objects in your palette, since these are watched on the view:
$scope.selectedColors.push({col: $scope.palette[2]});
$scope.selectedColors.push({col: $scope.palette[1]});
Same with your result:
$scope.result = { color: $scope.obj.codes[2] };
Then you need to watch the result. In the below example, select 1 receives the value from the initiating select, the second receives the value below in the palette. I don't know if that's what you wanted, but you can easily change it:
$scope.$watch('result', function(value) {
if(value) {
var index = value.color.code -1;
$scope.selectedColors[0] = {col: $scope.palette[index] };
$scope.selectedColors[1] = {col: $scope.palette[Math.max(index-1, 0)] };
}
}, true);
See plunkr.
Ok, I think I figured this out but thanks to #ValentynShybanov and #asgoth.
According to angularjs example ngModel is initialized with one of the objects from the array utilized in the dropdown population. So having an array as:
$scope.locations = [{ state: 'FL', city: 'Tampa'}, {state: 'FL', city: 'Sarasota'} ....];
And the dropdown is defined as:
<select ng-options="l.state group by l.city for l in locations" ng-model="city" required></select>
Then $scope.city is initialized as:
$scope.city = $scope.locations[0];
So far so good, right?!!!.. But I have multiple locations therefore multiple dropdowns. Also users can add/remove more. Like creating a table dynamically. And also, I needed to load data from the datastore.
Although I was building and assigning a similar value (e.g: Values from data store --> State = FL, City = Tampa; Therefore --> { state : 'FL', city : 'Tampa' }), angularjs wasn't able to match the value. I tried diff ways, like just assigning { city : 'Tampa' } or 'Tampa' or and or and or...
So what I did.. and I know is sort of nasty but works so far.. is to write a lookup function to return the value from $scope.locations. Thus I have:
$scope.lookupLocation = function(state, city){
for(var k = 0; k < $scope.locations.length; k++){
if($scope.locations[k].state == state && $scope.locations[k].city == city)
return $scope.locations[k];
}
return $scope.locations[0]; //-- default value if none matched
}
so, when I load the data from the datastore (data in json format) I call the lookupLocation function like:
$scope.city = $scope.lookupLocation(results[indexFromSomeLoop].location.state, results[indexFromSomeLoop].location.city);
And that preselects my values when loading data. This is what worked for me.
Thanks

Showing a limited subset of (or individual record from) a store in an Ext.DataView

In Sencha Touch, I often need to have an Ext.DataView panel that contains a small sub-set records or even a single record from the collection in the store.
For example I might have a Model for Car which has thousands of car records in it's app.stores.cars store but I want to show a smaller subset of these items (say; just sports cars) in my listOfSportsCars DataView while also showing the larger complete set of cars in my listOfCars DataView.
My first thought was to use multiple stores. So I'd have one main store for the big list of all cars, and a second store with a filter for my subset of sportscars. However, now updating a model from one store does not automatically update the record in the other store, so this defeats the purpose of using a DataView as the changes are not updated everywhere in the page when updating records.
My second attempt was to overwrite the collectData method on the DataView, which sounded exactly like what I was after:
var card = new Ext.DataView({
store: app.stores.cars,
collectData: function(records, startIndex){
// map over the records and collect just the ones we want
var r = [];
for( var i=0; i<records.length; i++ )
if( records[i].data.is_sports_car )
r.push( this.prepareData(records[i].data, 0, records[i]) );
return r;
},
tpl: new Ext.XTemplate([
'<tpl for=".">',
'<div class="car">{name}</div>',
'</tpl>'
]),
itemSelector: 'div.car'
});
A full example can be found here.
But, although it's documented that I can/should override this method, Sencha Touch really doesn't like it when you mess around with the length of the array returned by collectData so this was a dead-end.
How do others deal with displaying/updating multiple collections of the same records?
UPDATE There was a bug preventing collectData from working as expected. The bug has since been fixed in Sencha Touch 1.1.0.
As written in the comment:
I've used your democode with the last Sencha Touch release and opened all with Google Chrome. In the current version the error is fixed. (Version 1.1)
you could use Filters in order to get a subset of the data asociated to that store.
yourstore.filter('name', 'Joseph');
Also you should define 'root' as a function so it will always return an array. Readers in sencha touch asume you're always going to get an array as response, but it's not true if you are having a JSON with a single entry, try something like this:
root: function(data) {
if (data) {
if (data instanceof Array) {
return data;
} else {
return [data];
}
}
The full code for the store could be like this:
YourApp.ViewName = new Ext.data.Store({
model: 'YourApp.models.something',
proxy: {
type: 'scripttag',
url: 'http://somerandomurl/service/json',
extraParams: {
param1: 'hello'
},
reader: {
type: 'json',
root: function(data) {
if (data) {
if (data instanceof Array) {
return data;
} else {
return [data];
}
}
}
}
},
});
Hope it helps.
I use the "filter" features in the Store. Not modifying the DataView (I use a List).
Here's a snippet where I will fiter out Programs with a catagory that fit's a regex. (I have Programs with a catagory field)
MyApp.stores.Programs.filter(function(object) {
var regex = new RegExp(filterValue, 'i');
return object.data.category.search(regex) >= 0; // found match
});
You can clear the filter like this:
MyApp.stores.Programs.clearFilter(false);
This will update the DataView (I use a List) immediately (it's amazing).
So within your filter you could just filter out sports cars, or cars of a certain price, or whatever.
Hope that helps...
For my understanding of Sencha Touch this is not the best approach.
If it can be still good for performance you shoud use a second "slave" store, with inline data (http://docs.sencha.com/touch/1-1/#!/api/Ext.data.Store) that you can populate automatically from main store with subset of information you want to show when an event occours on the master store, i.e. load event.
If you want to deal with just one store a solution I can imagine is to use an xtemplate with "tpl if" tag in the dataview where you want to show just some information
http://docs.sencha.com/touch/1-1/#!/api/Ext. to write empty records. Maybe, also better solution, could be to use a custom filter function inside xtemplate, in order to put a css with visibility hidden on the items you don't want to see.

Resources