This is a rather complex issue. I've tried to refine the code as much as possible to only that which is required to demonstrate the problem. This is a very long post - consider yourself warned!
A working demonstration of this issue can be seen at this jsFiddle.
What I'm trying to do is aggregate an array into groups based on a common property of the items in the array. Actually aggregating the arrays into groups is fairly trivial:
function ViewModel() {
var self = this;
this.groupProperty = ko.observable('groupId');
this.data = ko.observableArray();
this.view = ko.computed(function () {
var i, element, value, grouped = {};
for (i = 0; i < self.data().length; i++) {
element = self.data()[i];
if (!element.hasOwnProperty(self.groupProperty()))
continue; //Exclude objects that don't have the grouping property...
value = ko.isObservable(element[self.groupProperty()]) ? element[self.groupProperty()]() : element[self.groupProperty];
if (!grouped.hasOwnProperty(value))
grouped[value] = new Group(value);
grouped[value].items().push(element);
}
return transformObjectIntoArray(grouped);
});
}
transformObjectIntoArray can be seen in the jsFiddle example. It just turns an object into an array by getting rid of the properties basically.
This takes an array of data like:
[
Item { label: 'Item 1', groupId: 'A' },
Item { label: 'Item 2', groupId: 'A' },
Item { label: 'Item 1', groupId: 'B' },
Item { label: 'Item 2', groupId: 'B' }
]
Turns it into the following object:
{
'A': [
Item { label: 'Item 1', groupId: 'A' },
Item { label: 'Item 2', groupId: 'A' }
],
'B': [
Item { label: 'Item 1', groupId: 'B' },
Item { label: 'Item 2', groupId: 'B' }
]
}
Which is then transformed into the following array:
[
[
Item { label: 'Item 1', groupId: 'A' },
Item { label: 'Item 2', groupId: 'A' }
],
[
Item { label: 'Item 1', groupId: 'B' },
Item { label: 'Item 2', groupId: 'B' }
]
]
Everything works as intended up to this point, as can be seen in the jsFiddle.
The problem begins when I add a new element to the data array. vm is my ViewModel instance in the following code
vm.data.push(new Item('Item X', 'A')); //Add a new item called 'Item X' to the 'A' group
As you might have guessed, this causes the computed observable view to execute, which re-aggregates the underlying data array. All new Group objects are created, which means, any state information associated with them is lost.
This is a problem for me because I store the state of if they should be expanded or collapsed on the Group object:
function Group(label) {
this.expanded = ko.observable(false); //Expanded state is stored here
this.items = ko.observableArray();
this.label = ko.observable(label);
}
So, since they get recreated, that state information is lost. Thus, all groups revert to their default state (which is collapsed).
I know the problem I'm facing. However, I'm struggling to come up with a solution that isn't unwieldy. Possible solutions I've thought of include the following:
Maintaining a group state map
The first idea I had was to create an object that would serve as a map. It would use the groupId as the key name, and the state would be the value:
{
'A': GroupState { expanded: true },
'B': GroupState { expanded: false }
}
Even though the groups are getting recreated each time an element is added, the GroupState would persist. A problem I couldn't solve was how to remove groups that no longer exist from the GroupState map.
For example,
vm.data(someNewArray);
Where someNewArray was an array of items with no groupIds corresponding to those currently in the GroupState map. How would I remove the entries that no longer had a reference? This seems like it would be a memory leak in my application.
This problem can be seen demonstrated at this jsFiddle. Notice, after clicking the button, the group state size grows to 5 elements, but there are only 3 groups. This is because the original 2 groups aren't removed despite no longer being used.
Maintaining a viewState array and removing the computed observable
The second idea I had was to remove the computed observable view. Instead, I'd have a second observable array called viewState which would be the data array after it had been aggregated into the current "view". However, I quickly ran into problems with this idea.
First, I'd have to write a couple methods that would maintain state between the two arrays: add(), remove(), clear(), etc. Considering having to do this made me immediately start questioning if having a second array was a good idea at all.
Second, removing the computed array means linearly searching the viewState array to see if any current elements contain a groupId similar to the incoming item. While in practice, this would be blazing fast, I don't like the theory of iterating the entire array on each add (O(n) vs O(1)). Down the road, I might be working with thousands of items.
I feel like there is probably an easy solution to this, but I'm struggling to put my finger on it. Does anyone have any ideas that might help with this problem, or know an entirely better approach to accomplishing this with KnockoutJS? Perhaps, one of my ideas above will work with some added insight (I still feel like the GroupState map was on the right track)
Please let me know if I've left out some crucial information and I will do my best to add it
What about this proposition:
don't use any computed
during creation of a new item, create the group if it does not exist and add the item to the group
Note:
I am using arrayFirst to find if a group exist during the creation of an item, it is O(n) but you can instead store groups in properties so that the lookup should be O(log(n)) (not tested)
function Item(label, groupId) {
var self = this;
self.label = ko.observable(label);
self.groupId = ko.observable(groupId);
}
function Group(label) {
var self = this;
self.expanded = ko.observable(false);
self.items = ko.observableArray();
self.label = ko.observable(label);
self.addItem = function(item) {
self.items.push(item);
}
}
function ViewModel() {
var self = this;
self.groups = ko.observableArray();
self.addItem = function(label, groupId) {
var group = ko.utils.arrayFirst(self.groups(), function(gr) {
return groupId == gr.label();
});
if(!group) {
console.log('not group');
group = self.addGroup(groupId);
}
var item = new Item(label, groupId);
group.addItem(item);
}
self.addGroup = function(groupId) {
var group = new Group(groupId);
this.groups.push(group);
return group;
}
this.buttonPressed = function () {
vm.addItem("Item X", "A");
};
}
var vm = new ViewModel();
ko.applyBindings(vm);
vm.addItem("Item 1", 'A');
vm.addItem("Item 2", 'A');
vm.addItem("Item 1", 'B');
vm.addItem("Item 2", 'B');
JSFiddle link
Related
I want to display the data in a Tree View in Angular and need to transform an array of dot-notated elements into a collection of objects with children.
This is the array I'm working with. Notice the key field in every element.
So the structure I need is for example (for the first 4 elements in the array):
const data = [
{
key: 'bs',
children: [
{
key: 'ass',
children: [
{
key: 'fixAss',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 15542000,
children: [
{
key: 'intan',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 8536000,
children: [
{
key: 'concessionBrands',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 8536000,
children: [] // If there are no children in the element this can be empty or left out
}
]
},
{
key: 'tan',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 6890000,
children: []
}
]
}
]
}
]
}
];
That means elements are combined by having a key attribute which holds the notation for that level (i.e "bs", "ass", "fixAss", ...) and then children of the next level. An element can have values of its own ("decimals", "unitRef",...) and might additionally also have children that are made up the same way. There is no restriction on the amount of levels this can have.
I have the lodash and dot object libraries in my package.json. Any help is very much appreciated.
it seems the dot-object lib has no things to work with something like "children" that you have, so it seems custom code is required to build what you expected
// balanceData got somehow
let data = [];
const getOrBuildPathObject = (path) => {
let currentLevel = data;
let obj = null;
for(let keyFragment of path.split('.')) {
obj = currentLevel.find(v => v.key == keyFragment);
if(!obj) {
obj = {key: keyFragment, children: []};
currentLevel.push(obj);
}
currentLevel = obj.children;
}
return obj;
}
balanceData.forEach((d) => {
let {key, ...data} = d;
Object.assign(getOrBuildPathObject(key), data);
})
should be something like that
I would just iterate through the array and check each key.
Split the key at the dots myArray.split('.') returns an array.
Now iterate through that array and create an Object for each element.
Like
bs.ass.fixAss
Check if a root element bs exists.
If no, create an (empty) bs Element.
Check if an ass element is a child of bs
If no, create an (empty) ass Element
Check if an (empty) fixAss Element exists.
If no, create the fixAss Element with values and add it as child to the ass Element
If yes, fill the values
If its guaranteed that the data will always be in the right order (that means bs.ass.fixAss will always be AFTER bs.ass) then you may skip the checks.
I would use a HashMap for the children (not an array), because that makes it much easier to walk through the tree
myTrees[bs].children[ass].children[fixAss]
The whole thing could be created with plain TypesScript. I do not know any library that would solve this specific problem out of the box.
I have basically this structure for my data (this.terms):
{
name: 'First Category',
posts: [
{
name: 'Jim James',
tags: [
'nice', 'friendly'
]
},
{
name: 'Bob Ross',
tags: [
'nice', 'talkative'
]
}
]
},
{
name: 'Second Category',
posts: [
{
name: 'Snake Pliskin',
tags: [
'mean', 'hungry'
]
},
{
name: 'Hugo Weaving',
tags: [
'mean', 'angry'
]
}
]
}
I then output computed results so people can filter this.terms by tags.
computed: {
filteredTerms: function() {
let self = this;
let terms = this.terms; // copy original data to new var
if(this.search.tags) {
return terms.filter((term) => {
let updated_term = {}; // copy term to new empty object: This doesn't actually help or fix the problem, but I left it here to show what I've tried.
updated_term = term;
let updated_posts = term.posts.filter((post) => {
if (post.tags.includes(self.search.tags)) {
return post;
}
});
if (updated_posts.length) {
updated_term.posts = updated_posts; // now this.terms is changed even though I'm filtering a copy of it
return updated_term;
}
});
} else {
return this.terms; // should return the original, unmanipulated data
}
}
},
filteredTerms() returns categories with only the matching posts inside it. So a search for "angry" returns just "Second Category" with just "Hugo Weaving" listed.
The problem is, running the computed function changes Second Category in this.terms instead of just in the copy of it (terms) in that function. It no longer contains Snake Pliskin. I've narrowed it down to updated_term.posts = updated_posts. That line seems to also change this.terms. The only thing that I can do is reset the entire data object and start over. This is less than ideal, because it would be loading stuff all the time. I need this.terms to load initially, and remain untouched so I can revert to it after someone clears their search criterea.
I've tried using lodash versions of filter and includes (though I didn't really expect that to make a difference). I've tried using a more complicated way with for loops and .push() instead of filters.
What am I missing? Thanks for taking the time to look at this.
Try to clone the object not to reference it, you should do something like :
let terms = [];
Object.assign(terms,this.terms);
let terms = this.terms;
This does not copy an array, it just holds a reference to this.terms. The reason is because JS objects and arrays are reference types. This is a helpful video: https://www.youtube.com/watch?v=9ooYYRLdg_g
Anyways, copy the array using this.terms.slice(). If it's an object, you can use {...this.terms}.
I updated my compute function with this:
let terms = [];
for (let i = 0; i < this.terms.length; i++) {
const term = this.copyObj(this.terms[i]);
terms.push(term);
}
and made a method (this.copyObj()) so I can use it elsewhere. It looks like this:
copyObj: function (src) {
return Object.assign({}, src);
}
I'm writing a program that takes a store inventory and searches for specific items in that inventory, pushing those items into an array. The inventory is all one object, and each object within this object is an item of the inventory. The items themselves have no keys- they're simply object literals. Therefore I'm stuck looping through them using fast enumeration (for item in products). Each item looks like this:
{ id: 2759167427,
title: 'Practical Silk Bag',
handle: 'practical-silk-bag',
vendor: 'Legros, Willms and Von',
product_type: 'Bag'
}
What I'm trying to do is push the item object to an array if and only if that item is either a keyboard or a computer. To that end I tried employing something like this :
var kbComps = [];
//loop through the inventory, looking for everything that is a keyboard or a computer
for (var key in products) {
var item = products[key];
for (var property in item) {
if (item[property].includes("Computer") || item[property].includes("Keyboard")) {
kbComps.push(item);
}
}
}
However I'm getting an error that tells me includes isn't a defined method, meaning the program isn't recognizing item[title] as a string, so now I'm stuck. How would I circumvent this? Any help is appreciated.
Cheers all
UPDATED
I changed the implementation to loop over an object and not an array. I think this is what you are looking for.
Here is a working jsBin
May be this is a little simpler and I'm sure it would work for you
// Products base data
var productsData = {
a: {
id: 2759167427,
title: 'Practical Silk Bag',
handle: 'practical-silk-bag',
vendor: 'Legros, Willms and Von',
product_type: 'Bag',
},
b: {
id: 2759167417,
title: 'Practical Silk Bag 2',
handle: 'practical-silk-bag-2',
vendor: 'Legros, Willms and Von 2',
product_type: 'Bag 2',
},
c: {
id: 2759167417,
title: 'Practical Silk Bag 3',
handle: 'practical-silk-bag-3',
vendor: 'Legros, Willms and Von 3',
product_type: 'Computer', // This product must be returned
},
d: {
id: 2759167417,
title: 'Practical Silk Bag 4',
handle: 'practical-silk-bag-4',
vendor: 'Legros, Willms and Von 4',
product_type: 'Keyboard', // This product must be returned
}
};
/**
* Function to find products by any condition
*/
function searchItemsByCondition(products, evaluateCondition) {
var ans = [];
for (var item in productsData) {
// Making sure the object has the property product_type
if (products[item].hasOwnProperty('product_type')) {
if (evaluateCondition(products[item])) {
ans.push(products[item]);
}
}
}
return ans;
}
function searchByKeyboardOrComputer(product) {
return (product.product_type === 'Computer') || (product.product_type === 'Keyboard');
}
// Call the function passing the evaluation function to satisfy.
// It should log only the items with 'Keyboard' or 'Computer' product_type
console.log(searchItemsByCondition(productsData, searchByKeyboardOrComputer));
Hope this works for you!
In the first iteration of your loop, you're checking if id contains a string but id is a number therefore .includes fails.
I'm not sure what you're intention is but you might want to only check .includes if the item is a string.
if (typeof item[property] === 'string' && (item[property].includes("Computer") || item[property].includes("Keyboard"))) {
If you throw some console logs in you can see what's going on. https://jsfiddle.net/qexssczd/1/
I have a very deep nested category structure and I am given a category object that can exist at any depth. I need to be able to iterate through all category nodes until I find the requested category, plus be able to capture its parent categories all the way through.
Data Structure
[
{
CategoryName: 'Antiques'
},
{
CategoryName: 'Art',
children: [
{
CategoryName: 'Digital',
children: [
{
CategoryName: 'Nesting..'
}
]
},
{
CategoryName: 'Print'
}
]
},
{
CategoryName: 'Baby',
children: [
{
CategoryName: 'Toys'
},
{
CategoryName: 'Safety',
children: [
{
CategoryName: 'Gates'
}
]
}
]
},
{
CategoryName: 'Books'
}
]
Code currently in place
function findCategoryParent (categories, category, result) {
// Iterate through our categories...initially passes in the root categories
for (var i = 0; i < categories.length; i++) {
// Check if our current category is the one we are looking for
if(categories[i] != category){
if(!categories[i].children)
continue;
// We want to store each ancestor in this result array
var result = result || [];
result.push(categories[i]);
// Since we want to return data, we need to return our recursion
return findCategoryParent(categories[i].children, category, result);
}else{
// In case user clicks a parent category and it doesnt hit above logic
if(categories[i].CategoryLevel == 1)
result = [];
// Woohoo...we found it
result.push(categories[i]);
return result;
}
}
}
Problem
If I return my recursive function it will work fine for 'Art' and all of its children..but since it returns, the category Baby never gets hit and therefor would never find 'Gates' which lives Baby/Safety/Gates
If I do not return my recursive function it can only return root level nodes
Would appreciate any recommendations or suggestions.
Alright, I believe I found a solution that appears to work for my and not sure why my brain took so long to figure it out...but the solution was of course closure.
Essentially I use closure to keep a scoped recursion and maintain my each iteration that it has traveled through
var someobj = {
find: function (category, tree, path, callback) {
var self = this;
for (var i = tree.length - 1; i >= 0; i--) {
// Closure will allow us to scope our path variable and only what we have traversed
// in our initial and subsequent closure functions
(function(){
// copy but not reference
var currentPath = path.slice();
if(tree[i] == category){
currentPath.push({name: tree[i].name, id: tree[i].id});
var obj = {
index: i,
category: category,
parent: tree,
path: currentPath
};
callback(obj);
}else{
if(tree[i].children){
currentPath.push({name: tree[i].name, id: tree[i].id});
self.find(category, tree[i].children, currentPath, callback);
}
}
})(tree[i]);
}
},
/**
* gets called when user clicks a category to remove
* #param {[type]} category [description]
* #return {[type]} [description]
*/
removeCategory: function (category) {
// starts the quest for our category and its ancestors
// category is one we want to look for
// this.list is our root list of categoires,
// pass in an intial empty array, each closure will add to its own instance
// callback to finish things off
this.find(category, this.list, [], function(data){
console.log(data);
});
}
}
Hope this helps others that need a way to traverse javascript objects and maintain parent ancestors.
I have the following object that I'm receiving from an API:
{
'2012-12-12': [
{ 'id': 1234,
'type': 'A' },
{ 'id': 1235,
'type': 'A' },
{ 'id': 1236,
'type': 'B' },
],
'2012-12-13': [
{ 'id': 1237,
'type': 'A' },
{ 'id': 1238,
'type': 'C' },
{ 'id': 1239,
'type': 'B' },
]
}
Then I want to have another variable named types of type Array that will hold every possible value of the type attribute of each one of the objects. In this case it would be:
types = ['A', 'B', 'C']
I'm trying to have it done in a functional way (I'm using underscore.js) but I'm unable to figure out a way of doing it. Right now I'm using
types = [];
_.each(response, function(arr1, key1) {
_.each(arr1, function(arr2, key2) {
types.push(arr2.type);
});
});
types = _.uniq(types);
But that's very ugly. Can you help me in figuring out a better way of writing this code?
Thanks!
This should work:
types = _.chain(input) // enable chaining
.values() // object to array
.flatten() // 2D array to 1D array
.pluck("type") // pick one property from each element
.uniq() // only the unique values
.value() // get an unwrapped array
Fiddle: http://jsfiddle.net/NFSfs/
Of course, you can remove all whitespace if you want to:
types = _.chain(input).values().flatten().pluck("type").uniq().value()
or without chaining:
types = _.uniq(_.pluck(_.flatten(_.values(input)),"type"));
flatten seems to work on objects, even though the documentation clearly states it shouldn't. If you wish to code against implementation, you can leave out the call to values, but I don't recommend that. The implementation could change one day, leaving your code mysteriously broken.
If you just want shorter code, you could flatten the objects into a single Array, then map that Array.
var types = _.unique(_.map(_.flatten(_.toArray(response)), function(arr) {
return arr.type;
}));
Here's another version. Mostly just for curiosity's sake.
var types = _.unique(_.pluck(_.reduce(response, _.bind(Function.apply, [].concat), []), "type"));
Here's another one.
var types = _.unique(_.reduce(response, function(acc, arr) {
return acc.concat(_.pluck(arr,"type"));
}, []));
And another.
var types = _.unique(_.pluck([].concat.apply([], _.toArray(response)), "type"))