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);
});
Related
I have a Vue component that builds the below into a blog form field. The writer is allowed to creatively add/slot any field of choice in between each other when building a blog post ...(like: title, paragraph, blockquote, image) in an object like:
{"post":{"1":{"title":{"name":"","intro":""}},"2":{"paragraph":{"text":"","fontweight":"default-weight","bottommargin":"default-bottom-margin"}},"3":{"image":{"class":"default-image-class","creditto":""}},"4":{"subheading":{"text":"","size":"default"}}}};
I've tried using jQuery each to iterate and add it up into a makedo "dataObj" object and inject it back on the data:
data: { treeData: myUserData.post },
injectFieldType: function(type, position){
var storeObj = {};
var dataObj = this.treeData;
var crntKey;
$.each( dataObj, function( key, value ) {
if(key < position)
{
//remain same as key is not to change
}
else if(key == position)
{
dataObj[''+(parseInt(key)+1)] = dataObj[key]; /*push key further right with +1*/
dataObj[key] = /*add injected field here*/;
}
else if(key > position)
{
dataObj[''+(parseInt(key)+1)] = dataObj[key]; /*push the rest*/
}
});
and inject it back with (this.treeData = dataObj;) when it has injected the desired key and has shifted the rest by adding 1 to their keys when this is clicked:
<button type="button" v-on:click="injectFieldType('image','2')">
I need to have {"post":{"1":{"title":{"name":"","intro":""}},"2":{"image":{"class":"default-image-class","creditto":""}},"3":{"paragraph":{"text":"","fontweight":"default-weight".... When I try to inject the image field in-between the existing "name" and "paragraph" fields and make the paragraph key now 3 (instead of the old 2).
I want "{1:{foo}, 2:{bar}"} to become => {"1:{foo}, 2:{moo}, 3:{bar}" }(notice 3 changed key)
NOTE: the number order is needed to align them reliably in publishing. And data: { treeData: myUserData.post } needs to agree with the changes to allow creating the field and updating each form "name" attribute array.
There are a few problems to address here.
Firstly, trying to use var dataObj = this.treeData; and then this.treeData = dataObj isn't going to help. Both dataObj and this.treeData refer to the same object and that object has already been processed by Vue's reactivity system. You could address the reactivity problems by creating a totally new object but just creating an alias to the existing object won't help.
Instead of creating a new object I've chosen to use this.$set in my example. This isn't necessary for most of the properties, only the new one added at the end really needs it. However, it would have been unnecessarily complicated to single out that one property given the algorithm I've chosen to use.
Another potential problem is ensuring all numbers are compared as numbers and not as strings. In your example you're passing in the position as the string '2'. Operators such as < will give you the expected answer for numbers up to 9 but once the number of items in treeData reaches 10 you may start to run into problems. For string comparision '2' < '10' is false.
The next problem is the order you're moving the entries. In your current algorithm you're overwriting entry key + 1 with entry key. But that means you've lost the original value for entry key + 1. You'll end up just copying the same entry all the way to the end. There are two ways you could fix this. One would be to use a new object to hold the output (which would also help to address the reactivity problem). In my solution below I've instead chosen to iterate backwards through the keys.
new Vue({
el: '#app',
data () {
return {
newEntry: 'Yellow',
newIndex: 4,
treeData: {
1: 'Red',
2: 'Green',
3: 'Blue'
}
}
},
computed: {
treeDataLength () {
return Math.max(...Object.keys(this.treeData))
}
},
methods: {
onAddClick () {
const newIndex = Math.round(this.newIndex)
if (newIndex < 1 || newIndex > this.treeDataLength + 1) {
return
}
this.injectFieldType(this.newEntry, newIndex)
},
injectFieldType (type, position) {
const list = this.treeData
for (let index = this.treeDataLength + 1; index >= position; --index) {
if (index === position) {
this.$set(list, index, type)
} else {
this.$set(list, index, list[index - 1])
}
}
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<ul>
<li v-for="index in treeDataLength">
{{ index}}. {{ treeData[index] }}
</li>
</ul>
<input v-model="newEntry">
<input v-model="newIndex">
<button #click="onAddClick">Add</button>
</div>
The decision to use an object with number keys seems very strange. This would all be a lot easier if you just used an array.
I'm writing a tiny reactive framework where I need to find out which subscriber needs updating. I'm implementing deep binding and I'm running into a wall how to find subscribers in an effective manner.
A stored variable can be an object, so for example
{
"user": {
"preferences": {
"food": "vegetarian"
}
}
}
You can get content to any level of this variable like this
getVar("user_preferences_food");
getVar("user_preferences");
However, you can also update it like that
setVar("user_preferences_food", "meat");
setVar("user_preferences", {"food": "meat"});
But in case of the first setVar (user_preferences_food) how can I find the subscriber using getVar("user_preferences"); or even getVar("user"); most effectively.
I already got it working by splitting the var on _ and then one by one concatting the next level and merging all the resulting arrays. But this is very resource intensive. Especially if there are a lot of subscribers. There must be a better way to find them that is less resource intensive.
Edit: I left out part of the explanation.
There is a subscribe method too
subscribe("user", cb);
subscribe("user_preferences", cb);
subscribe("user_preferences_food", cb);
These subscriptions are stored in an array in the framework.
As soon as "user_preferences_food" is updated for example, all subscriptions above should be triggered. But obviously not subscribe('othervar');
simplification of the subscribe method:
var subscriptions = [];
function subscribe(var, callback){
subscriptions.push({var: var, cb: callback});
}
Simplification of getVar
vars = {};
getVar(var){
// find var in vars with this logic: https://stackoverflow.com/a/18937118/249710
// current exact match on subscribers, but need the "parents, grandparents etc here
var toUpdate = _.where(subscriptions, {
"var" : var
});
_.each(toUpdate, function(sub){ sub.cb();});
}
Storing or getting data as part of the key I've already got covered. It is just finding the subscribers in the most effective manner
ps: this is in an environment where I cannot rely on ES6 yet (not all users have it enabled), there is no DOM but I do have underscore included. (Titanium app development platform)
I would try to make a list for the callbacks, so you loop trough one list so you dont have to search, because you know the list is there with all the callbacks.
So if you call setVar('user_prefs') you set a seperate list with the root var. in this case its the user.
if any object is changed with setVar (in depth or not) you go to you're root var, get the list and loop trough this list with the callbacks.
The beauty of this is you can set a list with the root var,
var cbList[FIRSTVAR] this contains all the callbacks. No searching just loop.
Its the mongoDb principle, the data is ready to go, you don't search because you know the list is already there.
You could split the string and use it for reduceing the object.
function getVar(object, path) {
return path
.split('_')
.reduce(function (o, k) {
return (o || {})[k];
}, object);
}
function setVar(object, path, value) {
var keys = path.split('_'),
last = keys.pop();
keys.reduce(function (o, k) {
return o[k] = o[k] || {};
}, object)[last] = value;
}
var object = { user: { preferences: { food: "vegetarian" } } };
console.log(getVar(object, "user_preferences_food"));
console.log(getVar(object, "user_preferences"));
setVar(object, "user_preferences_food", "meat");
console.log(object);
setVar(object, "user_preferences", {"food": "meat"});
console.log(object);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I ended up doing this:
var options = [];
var parts = key.split('_');
var string = parts[0];
_.each(parts, function(p, i){
if (i > 0) string += '_' + p;
options.push(string);
});
var toUpdate = _.filter(subscribers, function(sub){
if (sub.var.indexOf(key + '_') === 0) return true;
if (options.indexOf(sub.var) > -1) return true;
return false;
});
So checking with indexOf on the string to see if there are children. And building an array with parents so any layer is a match, and doing an indexOf on that as well. I think this is the least complicated method of implementing it
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.
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();
}
this.col = Backbone.Collection.extend({
model: M,
comparator: function(item) {
return item.get("level");
}
});
This above code sorts items by level. I want to sort by level, then by title. Can I do that? Thanks.
#amchang87's answer definitely works, but another that I found worked is simply returning an array of the sortable fields:
this.col = Backbone.Collection.extend({
model: M,
comparator: function(item) {
return [item.get("level"), item.get("title")]
}
});
I haven't tested this in multiple browsers yet as I think it relies on JS' behavior in sort order for arrays (based on their contents). It definitely works in WebKit.
String concatenation works fine when sorting multiple fields in ascending order, but it didn't work for me because 1) I had to support asc/desc per field and 2) certain fields were number field (i.e., I want 10 to come after 2 if it is ascending). So, below was a comparator function I used and worked OK for my needs. It assumes the backbone collection has a variable assigned with 'sortConfig', which is an array of JSON objects with field name and sort order direction. For example,
{
"sort" : [
{
"field": "strField",
"order": "asc"
},
{
"field": "numField",
"order": "desc"
},
...
]
}
With the JSON object above assigned as 'sortConfig' to the collection, the function below will make Backbone sort by strField in ascending order first, then sort by numField in descending order, etc. If no sort order is specified, it sorts ascending by default.
multiFieldComparator: function(one, another) {
// 'this' here is Backbone Collection
if (this.sortConfig) {
for (var i = 0; i < this.sortConfig.length; i++) {
if (one.get(this.sortConfig[i].field) > another.get(this.sortConfig[i].field)) {
return ("desc" != this.sortConfig[i].order) ? 1 : -1;
} else if (one.get(this.sortConfig[i].field) == another.get(this.sortConfig[i].field)) {
// do nothing but let the loop move further for next layer comparison
} else {
return ("desc" != this.sortConfig[i].order) ? -1 : 1;
}
}
}
// if we exited out of loop without prematurely returning, the 2 items being
// compared are identical in terms of sortConfig, so return 0
// Or, if it didn't get into the if block due to no 'sortConfig', return 0
// and let the original order not change.
return 0;
}
Returning an array is not consistent if you need to sort descending and some ascending...
I created a small set of functions which can be used to return the relevant comparison integer back to Backbone Comparator function:
backbone-collection-multisort
The main thing is that Backbone sorts by a single relative value of one item to another. So it's not directly possible to sort twice in a single collection but I'd try this.
this.col = Backbone.Collection.extend({
model: M,
comparator: function(item) {
// make sure this returns a string!
return item.get("level") + item.get("title");
}
});
What this will do is return a string of like "1Cool", "1title", "2newTitle" ...
Javascript should sort the strings by the numerical character first then each character afterwards. But this will only work as long as your levels have the same amount of digits. IE "001title" vs "200title". The main idea though is that you need to produce two comparable objects, line a number or string, that can be compared to each other based on one criteria.
Other solution would be to use underscore to "groupby" your level then use "sortby" to manually sort each level group then manually replace the underlying collection with this newly created array. You can probably setup a function to do this whenever the collection "changes".
"inspired" in hyong answer.
This also allows you to change the data before compare it, valueTransforms is an object, if there is an attribute in that object that has a function, it will be used.
/*
* #param {Object} sortOrders ie:
* {
* "description": "asc",
* "duedate": "desc",
* }
* #param {Object} valueTransforms
*/
setMultiFieldComparator: function(sortOrders, valueTransforms) {
var newSortOrders = {}, added = 0;
_.each(sortOrders, function(sortOrder, sortField) {
if (["asc", "desc"].indexOf(sortOrder) !== -1) {
newSortOrders[sortField] = sortOrder;
added += 1;
}
});
if (added) {
this.comparator = this._multiFieldComparator
.bind(this, newSortOrders, valueTransforms || this.model.prototype.valueTransforms || {});
} else {
this.comparator = null;
}
},
_multiFieldComparator: function(sortOrders, valueTransforms, one, another) {
var retVal = 0;
if (sortOrders) {
_.every(sortOrders, function(sortOrder, sortField) {
var oneValue = one.get(sortField),
anotherValue = another.get(sortField);
if (valueTransforms[sortField] instanceof Function) {
oneValue = valueTransforms[sortField](oneValue);
anotherValue = valueTransforms[sortField](anotherValue);
}
if (oneValue > anotherValue) {
retVal = ("desc" !== sortOrder) ? 1 : -1;
} else if (oneValue < anotherValue) {
retVal = ("desc" !== sortOrder) ? -1 : 1;
} else {
//continue
return true;
}
});
}
return retVal;
},