Updating ViewModel knockout with a nested loop - javascript

This may not be the completely ideal optimized way of accomplishing the task. I'm open for suggestions on any better ways. So far the loads work fine performancewise.
I have my knockout app working via ajax load. Inside the binding calls, I have a nested loop that includes a function that updates points based on a setting value.
When I attempt to add a new item, no errors are thrown, however the UI does not update and I can't seem to figure out why.
Here's a fiddle of what I'm trying to do.
http://jsfiddle.net/hjchvawr/2/
The addCombatant method does work, but for whatever reason the table will not rebind. You can see the added value in the VM json outputed to the console.
self.addCombatant = function(combatant){
ko.utils.arrayForEach(self.divisions(), function(d){
if(d.name() == combatant.division){
d.combatants().push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
}
console.log(ko.toJSON(self.divisions));
}
)}.bind(this);
EDIT:
I've applied some updates suggested below and added another list to sort. It binds and updates however, when I add a combatant, it only binds to one event and the sorting is off. If I can't use sortDivision(combatants, 'swords'), how do would I make the automatic sorting work? In this fiddle (http://jsfiddle.net/4Lhwerst/2/) I want the event sorted by kills, then time. Is it possible to get this multilevel sorting done client side without creating another observeableArray?

This is the foreach binding in your table.
<!-- ko foreach: $root.sortDivision(combatants, 'swords') -->
sortDivision is defined:
self.sortDivision = function (div, evt) {
return div.sortBy(evt, 'time', 'asc').sortBy(evt, 'kills', 'desc');
};
Your sortBy function creates a new observableArray. That is not the same observableArray as is being pushed to.
ko.observableArray.fn.sortBy = function (evt, fld, direction) {
var isdesc = direction && direction.toLowerCase() == 'desc';
return ko.observableArray(this.sort(function (a, b) {
a = ko.unwrap(evt ? a[evt][fld]() : a[fld]());
b = ko.unwrap(evt ? b[evt][fld]() : b[fld]());
return (a == b ? 0 : a < b ? -1 : 1) * (isdesc ? -1 : 1);
}));
};
You should use computeds (or pureComputeds) for things that are a re-presentation or re-combination of data. Store any data item in one place.

You are pushing into the underlying combatants array and thus bypassing change tracking. Either remove the parentheses (d.combatants.push) or call valueHasMutated after you are done.
You need either:
if(d.name() == combatant.division){
d.combatants.push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
}
Or:
if(d.name() == combatant.division){
d.combatants().push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
d.combatants.valueHasMutated();
}

Related

RxJS pattern for initializing a toggle between two values

I am trying to figure out a nice RxJS pattern for a toggle-style button/link that basically limits a view of an array.
It toggles between two values, one of which is static, the other comes from an Observable (is result of an http request actually, this.itemsArray$).
ts
this.toggleClicks$ = Observable.fromEvent(this.viewAllToggle.nativeElement, 'click')
.scan(prev => !prev, false);
this.itemsArray$ = Observable.of([ /* array of any */ ]);
this.viewLimit$ = this.itemsArray$
.withLatestFrom(this.toggleClicks$, (items, viewAll) => {
return viewAll
? { nowLimit: items.length, otherLimit: 5 }
: { nowLimit: 5, otherLimit: items.length };
})
.share();
html
Showing <span>{{(viewLimit$ | async)?.nowLimit}}</span>.
<span #viewAllToggle>Show {{(viewLimit$ | async)?.otherLimit}}</span>
It basically works, but the obvious problem is the initial value, which is not populated until the first click. Even if I add .startWith()
this.itemsArray$ = Observable.of([ /* array of any */ ]);
it does not help.
What am I missing?
I did not get what you said about startWith. My understanding is that this should work :
this.toggleClicks$ = Observable.fromEvent(this.viewAllToggle.nativeElement, 'click')
.scan(prev => !prev, false)
.startWith (false); // OR TRUE, dont know what behaviour you seek
this.itemsArray$ = Observable.of([ /* array of any */ ]);
this.viewLimit$ = this.itemsArray$
.withLatestFrom(this.toggleClicks$, (items, viewAll) => {
return viewAll
? { nowLimit: items.length, otherLimit: 5 }
: { nowLimit: 5, otherLimit: items.length };
})
.share();
Is that what you tried?
The tricky thing here (and pretty much undocumented) with the withLatestFrom(obs$) operator is that nothing will be emitted till obs$ emits its first value. That is a logical behaviour as there is no value to be passed to the selector function (here in viewAll), and passing undefined by default might not necessarily be a sensible choice. If you know about the distinction between events and behaviours, the rationale behind this is that withLatestFrom is to be used with behaviours, which always have a value at any point of time.

Aurelia: always call method in the view (problems after upgrade)

We've upgraded Aurelia (in particular aurelia-framework to 1.0.6, aurelia-bindong to 1.0.3) and now we're facing some binding issues.
There's a list of elements with computed classes, and we had a method int the custom element that contained the list:
getClass(t) {
return '...' +
(this.selected.indexOf(t) !== -1
? 'disabled-option' :
: ''
) + (t === this.currentTag
? 'selected-option'
: ''
);
}
And class.one-way="$parent.getClass(t)" for the list element, everything was OK.
After the upgrade it simply stopped to work, so whenever the selected (btw it's bindable) or currentTag properties were modified, the getClass method just wasn't called.
I partially solved this by moving this logic to the view:
class="${$parent.getClass(t) + (selected.indexOf(t) !== -1 ? 'disabled-option' : '') (t === $parent.currentTag ? 'selected-option' : '')}"
I know that looks, well... bad, but that made t === $parent.currentTag work, but the disabled-option class still isn't applied.
So, the question is:
How do I force Aurelia to call methods in attributes in the view?
P.S.
I understand that it might cause some performance issues.
Small note:
I can not simply add a selected attribute to the list element since I don't to somehow modify the data that comes to the custom element and I basically want my code to work properly without making too many changes.
UPD
I ended up with this awesome solution by Fabio Luz with this small edit:
UPD Here's a way to interpret this awesome solution by Fabio Luz.
export class SelectorObjectClass {
constructor(el, tagger){
Object.assign(this, el);
this.tagger = tagger;
}
get cssClass(){
//magic here
}
}
and
this.shown = this.shown(e => new SelectorObjectClass(e, this));
But I ended up with this (defining an extra array).
You have to use a property instead of a function. Like this:
//pay attention at the "get" before function name
get getClass() {
//do your magic here
return 'a b c d e';
}
HTML:
<div class.bind="getClass"></div>
EDIT
I know that it might be an overkill, but it is the nicest solution I found so far:
Create a class for your objects:
export class MyClass {
constructor(id, value) {
this.id = id;
this.value = value;
}
get getClass() {
//do your magic here
return 'your css classes';
}
}
Use the above class to create the objects of the array:
let shown = [];
shown[1] = new MyClass('someId', 'someValue');
shown[2] = new MyClass('someId', 'someValue');
Now, you will be able to use getClass property:
<div repeat.for="t of shown" class.bind="t.getClass">...</div>
Hope it helps!
It looks pretty sad.
I miss understand your point for computing class in html. Try that code, it should help you.
computedClass(item){
return `
${this.getClass(item)}
${~selected.indexOf(item) ? 'disabled-option': ''}
${item === this.currentTag ? 'selected-option' : ''}
`;
}
Your code not working cause you miss else option at first if state :/
Update:
To toggle attribute state try selected.bind="true/false"
Good luck,
Egor
A great solution was offered by Fabio but it caused issues (the data that was two-way bound to the custom element (result of the selection) wasn't of the same type as the input and so on). This definitely can be fixed but it would take a significant amount of time and result in rewriting tests, etc. Alternatively, yeah, we could put the original object as some property blah-blah-blah...
Anyway:
There's another solution, less elegant but much faster to implement.
Let's declare an extra array
#bindable shownProperties = [];
Inject ObserverLocator
Observe the selected array
this.obsLoc.getArrayObserver(this.selected)
.subscribe(() => this.selectedArrayChanged);
Update the shownProperties
isSelected(t) {
return this.selected.indexOf(t) !== -1;
}
selectedArrayChanged(){
for(var i = 0; i < this.shown.length; i++){
this.shownProperties[i] = {
selected: this.isSelected(this.shown[i])
}
}
}
And, finally, in the view:
class="... ${shownProperties[$index].selected ? 'disabled-option' : '')} ..."
So, the moral of the story:
Don't use methods in the view like I did :)

Pouchdb query not filtering KEY when turning on the device

Hello I have this pouchdb query:
function(test, key){
var temp = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
var day = [];
$q.when(cigdb.query('learnIndex/by_data_type',{key: key, include_docs : true})).then(function(res){
$q.all(res.rows.map(function(row){
console.log(row.doc);
day.push(row.doc.day);
return temp[row.doc.hour]++;
}));
}).then(function(te){
day = day.unique();
console.log(day);
test.splice(0,24);
for(var i = 0; i<24; i++){
if(day.length > 0){
test.push(temp[i]/day.length);
}else{
test.push(temp[i]);
}
}
console.log(test);
return test;
}).catch(function(err){
console.log(err);
});
},
which works well on the browser but when debugging it on the device (android)
it jumps part of the code.
On the device it executes until the
$q.all(...) then it ignores completely the block :
console.log(row.doc);
day.push(row.doc.day);
return temp[row.doc.hour]++;
And keeps going executing the promise .then(function(te) as nothing was wrong
obs: my first work with js angular and ionic not really familiar with that
Thanks for any help
edit:
I already did try whith Promise.all(...)
and putting a return before $q.all(...) and Promise.all(...)
and on all of then did work on the browser but on the device the problem was the same.
edit2 : so after diging a bit if i send on console.log(res) just before $q.all() it returns :
Object {total_rows: 32, offset: 0, rows: Array[0]}
offset: 0
rows: Array[0]
total_rows: 32
__proto__: Object
while on the browser i have:
Object {total_rows: 11, offset: 0, rows: Array[10]}
offset: 0
rows: Array[10]
total_rows: 11
__proto__: Object
for some reason pouchdb is not populating the row
edit3:
changing the code:
q.when(cigdb.query('learnIndex/by_data_type',{key: key, include_docs : true})).then(function(res){
$q.all(res.rows.map(function(row){
day.push(row.doc.day);
return temp[row.doc.hour]++;
}));
for :
$q.when(cigdb.query('learnIndex/by_data_type',{include_docs : true})).then(function(res){
return $q.all(res.rows.map(function(row){
if(row.doc.data_type === key){
day.push(row.doc.day);
return temp[row.doc.hour]++;
}
}));
makes it work but now i dont get why the key is not filtering as supposed on de device
what makes the query useless as i could use a simple alldocs if i have to implement the filtering any way.
As others have said, you need a return before the $q.all(). You might want to read this article on promises to catch up on common anti-patterns: We have a problem with promises.
As for the key issue, it depends on what your map function for by_data_type is doing. Whatever is the first argument to emit(), that's your key. If you need to debug, then you can omit the key parameter and check the rows object on the result. Each row will contain a key object so you can see what the key is.
You may also want to check out pouchdb-find. It's a lot easier, especially if your map function is pretty straightforward.
I had the same issue using the sqlite-legacy plugin and solved it using the { androidDatabaseImplementation: 2 } flag when instantiating/creating the database.

Javascript Array Crossbrowser issues

I am working on a simple Web App, where Users can comment articles chronologically (like comments on an blog article). Every comment has a timestamp. I use KnockoutJS for a client side view model and due to problems with the date object in IE9 I use MomentJS for crossbrowser timestamp parsing (the Timestamp Property of every comment is in fact a MomentJS generated object). Data comes from an REST Endpoint as JSON to the client where it is instantiated in the Knockout view model. The constructor of my article model looks like this (shortened):
GC.ko.modelArticle = function (a) {
this.Id = ko.observable(a.Id);
this.Title = ko.observable(a.Title).extend({ required: true, minLength: 3 });
: //some more Properties
this.Comments = ko.observableArray();
if (util.isDefined(a.Comments)) {
for (var i = 0; i < a.Comments.length; i++) {
this.Comments.push(new GC.ko.modelComment(a.Comments[i]));
}
}
this.Comments.sort(function (left, right) {
return left.Timestamp == right.Timestamp ? 0 : (left.Timestamp < right.Timestamp ? -1 : 1);
});
};
As you can see, if the JSON (a) contains comments, these are pushed onto a Knockout observableArray. Afterwards I am sorting the Array chronologically ascending, so that newer Comments appear after older ones in the UI.
In Firefox and Chrome the array gets sorted ascending, as it should.
In IE9 it is sorted descending.
Does this happen because of
crossbrowser issues of the Array().push() function
or the Array().sort() function,
or because Knockout observable Arrays have issues with sorting,
or is it because of some error in my code?
EDIT: comment.Timestamp is an Knockout Observable. I tried two variants:
First returning a plain Javascript Date Object (which had Timestamp parsing issues in IE, so I had to modify this):
this.Timestamp = ko.observable(c.Timestamp)
Second returning a moment Object:
this.Timestamp = ko.observable(moment(c.Timstamp)
'c' is the JSON for a comment
EDIT 2: It turns out, that the sort() function of observableArray() in Knockout 2.2.1 seems to be the problem. I modified my code to the following (first sorting the plain javascript array, then pushing the elements to the KO Observable Array) and everything works as it should now. Here's the code:
GC.ko.modelArticle = function (a) {
this.Id = ko.observable(a.Id);
this.Title = ko.observable(a.Title).extend({ required: true, minLength: 3 });
: //some more Properties
this.Comments = ko.observableArray();
if (util.isDefined(a.Comments)) {
a.Comments.sort(function(left,right) {
return left.Timestamp == right.Timestamp ? 0 : (left.Timestamp < right.Timestamp ? -1 : 1);
});
for (var i = 0; i < a.Comments.length; i++) {
this.Comments.push(new GC.ko.modelComment(a.Comments[i]));
}
}
};
Given that .Timestamp is a moment you should use the .isBefore() and .isSame() comparison methods as follows:
this.Comments.sort(function (left, right) {
return left.Timestamp.isSame(right.Timestamp) ? 0 : (left.Timestamp.isBefore(right.Timestamp) ? -1 : 1);
});

How to watch an array for changes in AngularJS

I basically want the equivalent to binding to 'add' and 'remove' events in Backbone's Collections. I see basically no way of doing this in AngularJS, and the current workaround we've settled for is $watch()ing the array's length and manually diffing/recalculating the whole thing. Is this really what the cool kids do?
Edit: Specifically, watching the array's length means I don't easily know which element has been changed, I need to manually "diff".
I think using $watch is a good solution, but $watchCollection can be better for you. $watchCollection doesn't perform deep comparison and just watchs for array modification like insert, delete or sort (not item update).
For exemple, if you want to keep an attribut order synchronize with the array order :
$scope.sortableItems = [
{order: 1, text: 'foo'},
{order: 2, text: 'bar'},
{order: 3, text: 'baz'}
];
$scope.$watchCollection('sortableItems', function(newCol, oldCol, scope) {
for (var index in newCol) {
var item = newCol[index];
item.order = parseInt(index) + 1;
}
});
But for your problem, I do not know if there is a better solution than manually browse the array to identify the change.
The way to watch an array in Angular is $watch(array, function(){} ,true)
I would create child scopes and watch them individually.
here is an example:
$scope.myCollection = [];
var addChild = function()
{
var Child = $scope.$new();
Child.name = 'Your Name here';
Child.$watch('name', function(newValue) {
// .... do something when the attribute 'name' is changed ...
});
Child.$on('$destroy', function() {
//... do something when this child gets destroyed
});
$scope.myCollection.push(Child); // add the child to collection array
};
// Pass the item to this method as parameter,
// do it within an ngRepeat of the collection in your views
$scope.deleteButtonClicked = function(item)
{
var index = $scope.myCollection.indexOf(item); //gets the item index
delete $scope.myCollection[index]; // removes the item on the array
item.$destroy(); // destroys the original items
}
Please tell more about your usecase. One of the solutions of tracking element persistance is using ngRepeat directive with custom directive that listening element's $destroy event:
<div ng-repeat="item in items" on-delete="doSomething(item)">
angular.module("app").directive("onDelete", function() {
return {
link: function (scope, element, attrs) {
element.on("$destroy", function () {
scope.$eval(attrs.onDelete);
});
}
}
});
Perhaps the solution is to create the collection class ( like backbone does ) and you can hook into events pretty easily as well.
The solution I have done here isnt really comprehensive, but should give you a general guidance on how this could be done perhaps.
http://beta.plnkr.co/edit/dGJFDhf9p5KJqeUfcTys?p=preview

Categories