knockout js force rebind after sorting array items - arrays

Having trouble with knockout js. But seems to me it's a bug. Maybe there are some workaround.
There is an example of sorting list here and it works. And there is another example and it doesn't. The only difference between them is version of KO.
Any help would be appreciated.
Update:
I don't know the reason but after calling splice method KO refreshes binding in some incorrect way. So the workaround I've found - force rebind array model.
The code I use to force rebinding is follows:
// newArray is ko.observableArray([...])
var original = newArray();
newArray([]);
newArray(original); // KO will rebind this array
Is there more elegant way to force rebinding?

I believe what is happening here is that Knockout 2.1 is correctly updating the list when you splice the new item into it, but the jQuery UI sortable implementation is also adding the item to the new list.
To get around this I added a 'dragged' class to the item that gets added by the sortable implementation, and then removed it once I updated the two arrays (which causes the UI update as expected).
$list
.data('ko-sort-array', array)
.sortable(config)
.bind('sortstart', function (event, ui) {
ui.item.data('ko-sort-array', array);
ui.item.data('ko-sort-index', ui.item.index());
ui.item.addClass('dragged'); // <-- add class here
})
.bind('sortupdate', function (event, ui) {
var $newList = ui.item.parent();
if($newList[0] != $list[0]){ return; }
var oldArray = ui.item.data('ko-sort-array');
var oldIndex = ui.item.data('ko-sort-index');
var newArray = $newList.data('ko-sort-array');
var newIndex = ui.item.index();
var item = oldArray.splice(oldIndex, 1)[0];
newArray.splice(newIndex, 0, item);
$list.find('.dragged').remove(); // <-- remove the item added by jQuery here
});
You can see this working here

Related

Adding views using for loop to scrollable view in appcelerator

Can anyone please tell me how to add an array of views using a for loop to a scrollable view in android. The answers that I surfed online are not giving me a clear idea and are way too confusing. Any suggestions?
You have (at least) two options. If you have a lot of work going on for each list item (e.g. showing a thumbnail for a picture) then you may want to use the second approach. Otherwise, you may just use this simple approach (where I show statistics for a number of species - layout controlled in a separate controller). stats is my list:
function showSpecies(stats) {
_.each(stats, function(record){
$.form.add(Alloy.createController('viewStatsRow', {record:record}).getView());
});
}
In this example I have more work going on for "building" every item. So to avoid locking the thread I use a list as a "queue" and just handles the first item - and then call the function with the remaining list until it is empty:
var work = [];
function showNextItem(work,first){
if(work && work.length > 0){
// Progressively show list....
if($.boastList && $.boastList.sections[0]){
var list = [];
list.push(buildOneItem(work.shift())); // Take first element
if(first){
$.boastList.sections[0].items = list; // Replace list
}else{
$.boastList.sections[0].appendItems(list); // append item
}
// Free queue to allow other actions
setTimeout(function(){
showNextItem(work); // Call recursively...
},30);
}
}else{
// All boasts shown...
}
}
function showBoastlist(){
work = [];
DataFactory.boasts.find({}, {$sort:{sortTime:-1}}, function(result){
result.forEach(function(record) {
work.push(record);
});
});
showNextItem(work,true);
}
The buildOneItem function just returns an item ready to be added to the view.
Not sure if this was what you asked for - but hope you can use it ;-)
Happy coding!
/John

Angular ui grid tooltip not working

I am having a problem to display header tooltip on angular-ui-grid.
Here is plunker demo.
Any idea how to make it work?
I have not been able to figure out how to make the directive work properly internally by setting the headerTooltips as strings. The directive developers are making it work using a different implementation than yours that can be seen in this Plunker.
This solution will patch the problem until a better or more permanent one can be found. Place it at the end of your service call inside of your controller like the following.
upareneStavkePromise.then(function(upareneStavkeData){
$log.debug(upareneStavkeData);
$scope.ucitaniUpareniPodaci = true;
$scope.gridOptionsUpareniPodaci.data = upareneStavkeData.grupe;
upareneStavkeTotals = upareneStavkeData.totals;
/*
* Patch for possible bug:
* Description: Setting headerTooltip property
* value as a string doesn't render the value at
* runtime.
*
*/
//class for the header span element
var headerClass = ".ui-grid-header-cell-primary-focus";
//the column definitions that were set by the developer
var colDefs = $scope.gridOptionsUpareniPodaci.columnDefs;
//Get the NodeList of the headerClass elements.
//It will be an array like structure.
var headers = document.querySelectorAll(headerClass);
//loop through the headers
angular.forEach(headers,function(value,key){//begin forEach
//Set the title atribute of the headerClass element to
//the value of the headerTooltip property set in the columnDefs
//array of objects.
headers[key].title = colDefs[key].headerTooltip;
});//end forEach
/****************END PATCH********************/
});

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().

Toggle cell editor after record programmatically added to grid in ExtJS

I'm using ExtJS 4.2.1 and have a fairly simple setup: JSON-based store and a gridpanel that reads from that store. An add button's click event calls out to the function below.
My goal is to add a blank row to the grid and immediately begin editing it using the Ext.grid.plugin.CellEditing plugin that's enabled on the gridpanel.
var addNewRow = function() {
// start add logic
var row = {
'name': '',
'email': '',
'description': ''
};
store.add(row);
// start auto-edit logic
var index = store.indexOf(row); // -1
var grid = Ext.ComponentQuery.query('gridpanel[itemId=upperPane]')[0];
var plugin = grid.getPlugin('upperPaneEditor');
plugin.startEdit( index, 0 );
};
While debugging this, index is set to -1 and that does not work. I tested the plugin.startEdit()'s functionality with (0, 0) to edit the first column of the first row and it works fine. I tried moving the auto-edit logic to various event handlers try to get it to work:
The store's add event fired after the add and reflected the correct index but the element wasn't present yet in the gridpanel for the plugin to grab it.
The gridpanel's afterrender event didn't fire after the add
The gridpanel's add event fired but only after double-clicking on a cell manually to edit it. It also ended up in a loop with itself.
I'm not sure of what else to try at this point.
Your row is a model config object, not a model instance, therefore store.indexOf returns -1.
Try:
var inst = store.add(row)[0];
...
var index = store.indexOf(inst);

Marionette.js - hijacking CompositeView functions to create streaming pagination

I am creating a streaming paginated list of views. We start the app with an empty collection and add items to the collection at regular intervals. When the size of the collection passes a page_size attribute then the rest of the models should not get rendered, but the compositeView should add page numbers to click on.
I am planning on creating a render function for my compositeView that only renders items based on the current page# and page size by have a function in my collection that returns a list of models like this:
get_page_results: function(page_number){
var all_models = this.models;
var models_start = page_number * this.page_size;
var models_end = models_start + this.page_size;
//return array of results for that page
return all_models.slice(models_start,models_end);
}
My question is, should I even be using Marionette's composite view for this? It seems like im overwriting most of the functionality of Marionette's collectionView to get what I want.
Every time the number of items in my collection changes two things need to be updated:
The itemViews in the collection view
The page numbers at the bottom of the composite view
My strong recommendation is not to do this in the view layer. You're going to add a ton of code to your views, and you're going to end up duplicating a lot of this code between multiple views (one for displaying the data, one for page list and counts, one for ...).
Instead, use a decorator pattern to build a collection that knows how to handle this. I do this for filtering, sorting and paging data, and it works very well.
For example, here's how I set up filtering (running in a JSFdiddle here: http://jsfiddle.net/derickbailey/vm7wK/)
function FilteredCollection(original){
var filtered = Object.create(original);
filtered.filter = function(criteria){
var items;
if (criteria){
items = original.where(criteria);
} else {
items = original.models;
}
filtered.reset(items);
};
return filtered;
}
var stuff = new Backbone.Collection();
var filtered = FilteredCollection(stuff);
var view = Backbone.View.extend({
initialize: function(){
this.collection.on("reset", this.render, this);
},
render: function(){
var result = this.collection.map(function(item){ return item.get("foo"); });
this.$el.html(result.join(","));
}
});
In your case, you won't be doing filtering like this... but the idea for paging and streaming would be the same.
You would track what page # you are on in your "PagingCollection", and then when your original collection is reset or has new data added to it, the PagingCollection functions would re-calculate which data needs to be in the final pagedCollection instance, and reset that collection with the data you need.
Something like this (though this is untested and incomplete. you'll need to fill in some detail and flesh it out for your app's needs)
function PagingCollection(original){
var paged = Object.create(original);
paged.currentPage = 0;
paged.totalPages = 0;
paged.pageSize = 0;
paged.setPageSize = function(size){
paged.pageSize = size;
};
original.on("reset", function(){
paged.currentPage = 0;
paged.totalPages = original.length / paged.pageSize;
// get the models you need from "original" and then
// call paged.reset(models) with that list
});
original.on("add", function(){
paged.currentPage = 0;
paged.totalPages = original.length / paged.pageSize;
// get the models you need from "original" and then
// call paged.reset(models) with that list
});
return paged;
}
Once you have the collection decorated with the paging info, you pass the paged collection to your CollectionView or CompositeView instance. These will properly render the models that are found in the collection that you pass to it.
Regarding CollectionView vs CompositeView ... a CompositeView is a CollectionView (it extends directly from it) that allows you to render a model / template around the collection. That's the majority difference... they both deal with collections, and render a list of views from that collection.
We have built a set of mixins for bakcbone.marionette that you may find usefull (https://github.com/g00fy-/marionette.generic/)
You could use PaginatedMixin, that allows a Backbone collection to be paginated + a PrefetchMixin, so you don't have to pass a prefetched collection to a view.
the only code you would have to do is:
YourListView = Generic.ListView.mixin(PaginatedMixin,LoadingMixin,PrefetchMixin).extend({
paginateBy:10,
template:"#your-list-template",
itemViewOptions:{template:"#your-itemview-template"},
fetchPage:function(page){
this.page = page;
return this.collection.refetch({data:{page:page}}); // your code here
},
hasNextPage:function(){
return true; // your code here
},
});
For a working example see https://github.com/g00fy-/stack.reader/blob/master/js/views.js

Resources