Reevaluating a Knockout computed which depends just on an observable array - javascript

My Appmodel consists of an observable array of comments
self.comments=ko.observableArray([]); // Default Value is an empty array
/*
Here comes some code to initially fill the observable array
with items from an JSON Response
*/
Furthermore I have two computeds which should represent the very first comment and the last comment
self.firstComment = ko.computed(function () {
var sorted = self.comments.sort(function (left, right) {
return left.Id() - right.Id();
});
return sorted[0];
});
self.lastComment = ko.computed(function () {
var sorted = self.comments.sort(function (left, right) {
return left.Id() - right.Id();
});
return sorted[sorted.length - 1];
});
This works perfectly on initializing the application (loading the JSON from Server, build up App model...), but when I add a comment to the array, the computeds do not recognize that the number of array items has changed (as I understood it, an observable array is just an observable where the array properties themselves are observed). So when I do:
self.comments.push(aNewCommentObject);
self.lastComment is still bound to the array item, that it was when the app loaded initially.
I have found this blog post how to force computation by introducing a dummy observable, but I don't like the approach. For what purpose is an observableArray used then and how?
Additional Challenge: I would like to keep the observableArray Items sorted under every circumstance (because its a comment feed which should be just sorted chronologically). I tried to do this whith an computed commentsSorted but also have problems that this does not update when the observableArray has new items, so same problem here. Thats the reason, why I am sorting everytime in firstComment and lastComment.

Try unwrapping the comments to trigger Knockout's dependency tracking:
self.firstComment = ko.computed(function () {
var sorted = self.comments().sort(function (left, right) {
// -------------------^^ !
return left.Id() - right.Id();
});
return sorted[0];
});
or (same thing):
self.firstComment = ko.computed(function () {
var sorted = ko.unwrap(self.comments).sort(function (left, right) {
return left.Id() - right.Id();
});
return sorted[0];
});
Of course you can abstract this into a separate computed.
self.commentsSorted = ko.computed(function () {
return self.comments().sort(function (left, right) {
return left.Id() - right.Id();
});
});
self.firstComment = ko.computed(function () {
return ko.unwrap(self.commentsSorted)[0];
});
Since computeds cache their return values (just like every other observable does) you don't need to worry about calling self.commentsSorted multiple times. It will re-calculate only when the underlying observable array chances.

How can you "keep the observableArray items sorted under every circumstance"? My advice is to not try to sort the original array (I'll describe the pitfalls in more detail below) but to use a computed observable that returns a new, sorted array:
self.commentsSorted = ko.computed(function () {
return self.comments.slice(0).sort(function (left, right) {
return left.Id() - right.Id();
});
});
An important thing to understand about the JavaScript Array sort() method is that it changes the original array. Knockout's observableArray wraps many of the array functions so they can be used directly on the observableArray object. That's why you can use myObservableArray.sort() instead of having to do myObservableArray().sort(). But the two aren't equivalent because Knockout wraps array mutator methods differently, approximately like this:
sort: function (sortFunction) {
this.valueWillMutate();
var result = this.peek().sort(sortFunction);
this.valueHasMutated();
return result;
}
So what's wrong with automatically changing the original observableArray?
It's hard to grasp what's going on. It's easy to either not get a dependency on the array and thus never update past the initialization, or to not notify of the change and thus break other elements from getting the correct view of the array.
Even if you do set it up correctly, you've now created a circular dependency because you have something that's updated whenever the array changes, which then updates the array again. Although Knockout currently disables this for synchronous updates of a computed observable (but that might change in the future), it will result in unbounded recursion if using a manual subscription or a computed with the throttle option.

Alternative solution, using subscribe:
self.comments = ko.observableArray([]);
self.comments.subscribe(function(comments) {
var sorted = comments.sort(function (left, right) {
return left.Id() - right.Id();
});
self.firstComment(sorted[0]);
self.lastComment(sorted[sorted.length - 1]);
});

Related

Sort array based on intermediate model's attribute

I have three models (I am using Vue/Vuex-ORM on the frontend). Category, CategoryItem and Item.
I'm able to fetch an array of categories, and within each category is an array of items. An intermediate join model defines the relationships of these two models, and I am able to access as such:
// array of categories, each category has array of items
const categories = Category.query().where('pack_id', this.selectedPack.id).with('items').get();
categories.map(category => {
category.items.forEach(item => {
console.log('item.pivot: ', item.pivot); // pivot refers to join model
// how to order items based on item.pivot?
})
})
Within the .forEach, I can access the join model with item.pivot. What I am looking to do however, is sort each category's items based on item.pivot.position.
I started going down a path where the first line inside of the .map I defined a new empty array, and would then theoretically push in a new value based on whether the position was higher or lower, but I couldn't quite wrap my head around how to accomplish this.
Thanks!
Well just my luck. A half hour after posting this question, I figure it out! Here was what I did, in case anyone is curious.
categories() {
const categories = Category.query().where('pack_id', this.selectedPack.id).with('items').get();
categories.forEach(category => category.items.sort(this.compare));
return cats;
}
compare(a, b) {
let comparison = 0;
if (a.pivot.position > b.pivot.position) comparison = 1;
else if (a.pivot.position < b.pivot.position) comparison = -1;
return comparison;
},

SAPUI5 Javascript - Get first and last elements of array for each unique property

SAPUI5 - I have an array of objects and one of the properties in those is 'Category'.
For example say I have 2 different types of Category, 'Front Shop' and 'Production Area', what I need to do is to be able to get the first value of each and the last value of each, and then set the enabled property of a button as enabled/disabled.
I'm currently using undercore js (_.each) to loop through to perform some other logic, so can include additional logic here.
Not sure if Underscore has a built in function for this?
Or could someone point me in the right direction on how to do this?
I've got my first pass at what was wanted where I get the very first result and the last result, but now need to set this for each unique category.
Example code below:
// Set view data
oViewData.Questions = oQuestions.results;
oViewData.Questions.TotalNumberOfQuestions = oQuestions.results.length;
// Loop Questions, to get Category Desc and Competency Desc values from relevant Sets
_.each(oViewData.Questions, function (result, index) {
// Read and set Category Desc
this.getView().getModel("Survey").read("/CategorySet", {
filters: [new Filter("CategoryId", FilterOperator.EQ, result.CategoryId)],
success: function (oData) {
oViewData.Questions[index]._CategoryDesc = oData.results[0].CategoryDesc;
this.setViewData(oViewData);
}.bind(this),
error: function (oError) {}.bind(this)
});
// Read and set Competency Desc
this.getView().getModel("Survey").read("/CompetencySet", {
filters: [new Filter("CompetencyId", FilterOperator.EQ, result.CompetencyId)],
success: function (oData) {
oViewData.Questions[index]._CompetencyDesc = oData.results[0].CompetencyDesc;
this.setViewData(oViewData);
}.bind(this),
error: function (oError) {}.bind(this)
});
// Set all move up / down buttons to enabled
oViewData.Questions[index]._MoveUpBtn = true;
oViewData.Questions[index]._MoveDownBtn = true;
// if category id is the first one in the list
}.bind(this));
// Overwrite first move up button and last move down btn to disabled
oViewData.Questions[0]._MoveUpBtn = false;
oViewData.Questions.slice(-1)[0]._MoveDownBtn = false;
// Set view data
this.setViewData(oViewData);
First, you can iterate through arrays with native JavaScript.
_.each(array, function(item) {}) is the same as array.forEach(function(item) {}).
Second, you can use the built-in filter function for your actual question:
const aFrontShopItems = oViewData.Questions.filter(function(oItem) {
return oItem.Category === "Front Shop";
}
If oViewData.Questions is an array then the function passed to filter is applied to every element. If the condition (e.g. oItem.Category === "Front Shop") is true then the element is added to the new array aFrontShopItems. Obviously you need to call filter a second time to get the Production Area items. You can then apply your logic to the first and last items of your new arrays.

dgrid custom sort issue

I'm trying to override the sort logic in dgrid as suggested by kfranqueiro in this link - https://github.com/SitePen/dgrid/issues/276.
I get the data from the server in sorted order and just want to update the UI of column header. I'm doing this -
On(mygrid, 'dgrid-sort', lang.hitch( this,function(event){
var sort = event.sort[0];
var order = this.sort.descending ? "descending" : "ascending";
console.log("Sort "+ this.sort.property + " in " +order+" order.");
event.preventDefault();
mygrid.updateSortArrow(event.sort, true);
myFunctionToRefreshGrid();
}));
...
myFunctionToRefreshGrid: function() {
...//get data from server in sorted order
var mystore = new Memory({data: sortedDataFromServer, idProperty: 'id'});
mygrid.set("collection", mystore);
...
}
Memory here is "dstore/Memory". I'm using dgrid 0.4, dstore 1.1 and dojo 1.10.4
Before calling set('collection',...) I see that sortedDataFromServer is in the desired sorted order. But for some reason, the order in the grid is different. For example, when sorted in descending order, I see that the values starting with lower case appear first in descending order and then the values starting with upper case appear in sorted order. It looks like dstore is doing something more.
What could be going on? Am I doing something wrong? Is there a different/better way to do custom sorting?
Thanks,
This is how I ended up addressing the situation -
As suspected, the collection/store was further sorting my data and hence the inconsistency. I customized the store (Memory) as shown below and use the custom store when setting data to my grid.
var CustomGridStore = declare([Memory],{
sort: function (sorted) {
sorted = [];//Prevent the collection from sorting the data
return this.inherited(arguments);
}
});
I think you are doing the right thing, only problem here is that you are not resetting sort property of grid, one you re-initiate memory with sorted order, it get's sorted automatically
after you are calling
event.preventDefault();
call this
mygrid.set("sort", null);
I am doing custom sorting in my one of grid as following
self.xrefGrid.on("dgrid-sort", function (event) {
var sort = event.sort[0];
event.preventDefault();
self.xrefGrid.set('sort', function (a, b) {
var aValue,bValue;
if (a[sort.attribute] && typeof a[sort.attribute] == "string")
aValue = a[sort.attribute].toLowerCase();
if (b[sort.attribute] && typeof b[sort.attribute] == "string")
bValue = b[sort.attribute].toLowerCase();
var result = aValue > bValue ? 1 : -1;
return result * (sort.descending ? -1 : 1);
});
self.xrefGrid.updateSortArrow(event.sort, true);
});

Ember store adding attributes incorrectly

I'm using the latest version of ember-cli, ember-data, ember-localstorage-adapter, and ember.
I have a Node object which has a parent and children. Since I had issues with creating multiple relationships with the same type of object, I decided to store the parentID in a string, and the childIDs in an array of strings. However, when I create a new Node and try to add the new Node's to the parents array of IDs, the ID ends up being added to the correct parent, but also other parents.
level 1 0
/ \
level 2 1 2
| |
level 3 3 4
In a structure like this, 0, 1, and 2 all have correct child and parent IDs. However, after adding 3 and 4, node 1 and node 2's childIDs are [3, 4], instead of [3], [4] respectively.
The Array attribute:
var ArrayTransform = DS.Transform.extend({
serialize: function(value) {
if (!value) {
return [];
}
return value;
},
deserialize: function(value) {
if (!value) {
return [];
}
return value;
}
});
The insertNode code:
insert: function(elem) {
var i,
_store = elem.node.store,
newNodeJSON = elem.node.serialize();
newNodeJSON.childIds = [];
newNodeJSON.level = getNextLevel();
_store.filter('node', function(node) {
return node.get('level') === newnodeJSON.level-1;
}).then(function(prevLevelNodes) {
// if no other nodes yet
if (prevLevelNodes.toArray().length === 0) {
makeNewNode(_store, newNodeJSON, elem.node);
}
// else, generates however many nodes that are in the previous level
else {
prevLevelNodes.toArray().forEach(function(node, idx) {
newNodeJSON.parentId = node.get('id');
makeNewNode(_store, newNodeJSON, elem.node);
});
}
});
}
var makeNewNode = function(_store, newNodeJSON, node) {
console.log(newNodeJSON.parentId); // returns correct value
var newNode = _store.createRecord('node', newNodeJSON);
newNode.save();
var newNodeId = newNode.get('id');
if (newNode.get('parentId')) {
_store.find('node', newNode.get('parentId')).then(function(n) {
var cids = n.get('childIds');
console.log(newNodeId); // returns expected value
console.log(cids); // **DOESN'T RETURN AN EMPTY ARRAY**: returns array with [3,4]
cids.push(newNodeId);
console.log(n.get('childIds')); // returns array with [3,4]
n.save();
});
}
To top this off, this error happens 90% of the time, but 10% of the time it performs as expected. This seems to suggest that there's some sort of race condition, but I'm not sure where that would even be. Some places that I feel like might be causing issues: the ember-cli compilation, passing the entire _store in when making a new node, ember-data being weird, ember-localstorage-adapter being funky... no clue.
For anyone else who may have this problem in the future: the problem lies in two things.
In ArrayTransform, typically I am returning the value sans modification.
In my insert code, I'm passing the same JSON that I defined at the top of the function to makeNewNode.
This JSON contains a reference to a single childIds array; therefore, each new node that gets created uses this same reference for its childIds. Although this doesn't quite explain why the cids array wasn't empty before the push executed (perhaps this is some sort of compiler oddity or console printing lag), it explains why these both Level 3 children were in both Level 2 parents' childIds array.
tl;dr: pass by value vs pass by reference error

Updating $scope values affects it's previous usage points

Updating $scope values affects it's previous usage points.
After addPhrase call I use sayPhrase to update $scope
function PhrasesCtrl($scope) {
$scope.trail = [0];
$scope.addPhrase = function() {
$scope.phrases.push({
trail: $scope.trail
});
}
$scope.sayPhrase = function(id) {
// id = 1
$scope.trail.push(id);
}
}
Newly created Phrase have it's trail equal to [0], after sayPhrase call it becomes [0, 1]
After $scope.trail.push(id); my new element updates it's trail value.
How to keep used trail value away from changes?
This is because JS objects (and arrays) are passed by reference only. When you push the trail into phrases, you are pushing the reference to the same array that is referenced by $scope.trail.
The easiest solution is to break the reference on $scope.trail, by creating a new array:
$scope.addPhrase = function() {
$scope.phrases.push({
trail: $scope.trail
});
$scope.trail = [0]; // I assume the `0` is on purpose
}
Now $scope.trail will start over every time addPhrase() is called.
Alternatively, if you need to keep the current contents of trail, you should copy the array into a new one. Angular conveniently provides a method just for this:
$scope.addPhrase = function() {
$scope.phrases.push({
trail: angular.copy($scope.trail)
});
}

Categories

Resources