Building on this angular recursive extend topic.
I have slightly modified the version...
var extendDeep = function(dst) {
angular.forEach(arguments, function(obj) {
if (obj !== dst) {
angular.forEach(obj, function(value, key) {
if (dst[key] && angular.isObject(dst[key])) {
extendDeep(dst[key], value);
} else if(!angular.isFunction(dst[key])) {
dst[key] = value;
}
});
}
});
return dst;
};
but found that it does not account for arrays correctly. For example, if I have a object like:
var obj = { zoo: [ 'moose', 'panda' ] };
and then I call like:
deepExtend(obj, {
zoo: [ 'panda' ]
})
at first glance you would expect it to remove the object, however, it actually ends up like:
{ zoo: [ 'panda', 'panda' ] }
now. The expected output would look like:
{ zoo: [ 'panda' ] }
This is a very simple case of course.
I'm not really looking for a 'unique' or a 'merge' like most solutions talk about. I need to add items that are missing from the left, remove items that are on the left but not on the right, and then iterate recursively and extend those objects in place.
There are many different ways to go about this, looking for feedback on a good approach.
Related
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 want to filter some words contained in an array, contained in a string. For example:
Inputs
1. "disappear" definitions:
[ "Cease to be visible.",
"Cease to exist or be in use.",
"Be lost or go missing, become IMPOSSIBLE to find.",
"Abduct or arrest and kill or detain (a person) for political reasons,
without making their fate known." ]
2. banned words:
[ 'without', 'impossible' ]
Result
[ "Cease to be visible.", "Cease to exist or be in use." ]
I think, I'm very close to the answer:
function filterDefinition (defs, badWords) {
const definitionFilter = defs.filter(function(def) {
if (def.includes(badWords || badWords.toUpperCase()) {
return !defs
}
});
return definitionFilter;
}
Like this?
function filterDefinition (defs, badWords) {
return defs.filter(function(def) {
return !badWords.some(badWord => def.includes(badWord) || def.includes(badWord.toUpperCase()));
});
}
String.includes(string) does not work on arrays.
I'm using npm module traverse to filter data coming from mongodb / mongoose.
I might get this data:
[ { rating: 5,
title: { da: 'Web udvikling', en: 'Web Development' } },
{ rating: 5, title: { da: 'Node.js', en: 'Node.js' } } ]
'da' and 'en' indicates languages. I use traverse to filter mongoose data after current language like this:
var filtered = filterLanguage(lang, results);
// filter json obj by language
var filterLanguage = function(language, obj) {
return traverse(obj).map(function (item) {
if (this.key === language) {
this.parent.update(item);
}
});
};
I then show this in my template:
res.render('index', {
skills: filtered.skills
});
Finally I display it in the jade view:
ul.list-group
each skill, i in skills
if i < 5
li.list-group-item.sidebar-list-item= skill.title
Unfortunately it's displayed with quotes:
<ul>
<li>'Web Development'</li>
<li>'Node.js'</li>
</ul>
These quotes are not there in the unfiltered data (results.skill.title.da). So traverse is adding them. I used the module with 'plain' json and it's working perfectly.
The mongoose data seems plain and simple but of course there are a lot of properties on the prototype. Also traverse stalls if I don't omit '_id' (type bson/objectid) property from result set.
So traverse seems to have problems with mongoose data... Why is this? And how can I fix it?
-- EDIT --
I found a solution:
Before filtering I do this:
var json = JSON.parse(JSON.stringify(results));
var filtered = filterLanguage(lang, json);
This removes the quotes, but I'm not sure exactly what it does. Somehow converting the mongoose result to JSON? An explanation would be highly appreciated.
Fields in Mongoose documents are getters/setters, which seem to confuse either traverse or Jade/Pug.
The shortest method I found that seems to fix all of your issues is pretty ugly:
var filtered = filterLanguage(lang, results.map(r => JSON.parse(JSON.stringify(r))));
A more elaborate version:
var filtered = filterLanguage(lang, results.map(r => {
let j = r.toJSON()
j._id = j._id.toString()
return j;
}));
It would have been helpful to see what is the body of filterLanguage exactly or understand why it's called twice but as it stands, I don't think you need to use the traverse package at all.
A function such as below should do the trick and I even expanded it to work if the data is more tree-like and not as flat as represented in your example.
const reduceByLang = (data, lang) => {
// Look for a `lang` key in obj or
// if not found but still an object, recurse
const reduceByLangObj = (obj) => {
Object.keys(obj).forEach((key) => {
if (obj[key] === null) {
return;
}
if (obj[key][lang]) {
obj[key] = obj[key][lang]; // replace with desired lang
} else if (typeof obj[key] === 'object') {
reduceByLangObj(obj[key]); // recurse
}
});
return obj;
};
if (Array.isArray(data)) {
return data.map(reduceByLangObj);
} else {
return reduceByLangObj(data);
}
};
See example in JS Bin.
Also, if possible at all and if you do this type of selecting very often, I would look into saving the data in a different structure:
{ ratings: x, locales: { en: { title: 'Y' }, { da: { title: 'Z' } } } }
maybe, so that you can pick the selected language easily either in the query itself and/or in the controller.
EDIT: Checking for null.
I'm trying to combine and group an array with a bunch of flat arrays that contain only strings, no objects.
So my array looks something like this:
var array = [
["MotherNode", "Node1", "ChildNode1", "ChildOfChildNode1"],
["MotherNode", "Node1", "ChildNode2", "ChildOfChildNode2"],
["MotherNode", "Node2", "ChildNode3", "ChildOfChildNode3"],
["MotherNode", "Node2", "ChildNode3", "ChildOfChildNode4"],
["MotherNode", "Node3", "ChildNode4", "ChildOfChildNode5"],
["MotherNode", "Node3", "ChildNode4", "ChildOfChildNode5"]
]
Im doing this in javascript/angularjs and so far I've gathered that the best solution is probably to use underscore.js groupBy/combine methods. However most of the examples that i can find are dealing with arrays of objects where they can group them together by using a value's key. And I'm not good enough with algorithms yet to be able to figure this out on my own.
The array I'm dealing with can have hundreds of values and the result array could get 5-10 levels deep.
The result I'd like by parsing the above array would look something like this:
var result= {
"MotherNode": [{
"Node1":[{
"ChildNode1":"ChildOfChildNode1"
},{
"ChildNode2":"ChildOfChildNode2"
},{
"Node2":[{
"ChildNode3":["ChildOfChildNode3","ChildOfChildNode4"]
},{
"Node3":[{
"ChildNode4":"ChildOfChildNode5"
}
]
}
So does anyone have any clue how this can be done? I'm completely out of ideas.
I solved this using _.reduce grouping wasnt the way to go
var result = _.reduce(array,function(memo, val){
var tmp = memo;
_.each(val, function(fldr){
if(!_.has(tmp, fldr)){
tmp[fldr] = {}
}
tmp = tmp[fldr]
})
return memo
},{})
the end leaf wont be set as a value but it should be easy to change that behavior to whatever suits you use case
{ MotherNode:
{ Node1:
{ ChildNode1: { ChildOfChildNode1: {} },
ChildNode2: { ChildOfChildNode2: {} } },
Node2: { ChildNode3: { ChildOfChildNode3: {}, ChildOfChildNode4: {} } },
Node3: { ChildNode4: { ChildOfChildNode5: {} } } } }
If I can access an object from an object using list[value][index], how can I delete or unshift that object from list without using delete? (since that isn't possible in an object list)
My object looks like this:
var list = {
'test1': [
{
example1: 'hello1'
},
{
example2: 'world1'
}
]
'test2': [
{
example1: 'hello2'
},
{
example2: 'world2'
}
]
};
After deleting an object, I want it to look like this:
var list = {
'test1': [
{
example1: 'hello1'
}
]
'test2': [
{
example1: 'hello2'
},
{
example2: 'world2'
}
]
};
When I use delete, it looks like this:
var list = {
'test1': [
{
example1: 'hello1'
},
null
]
'test2': [
{
example1: 'hello2'
},
{
example2: 'world2'
}
]
};
You can remove the object from list by setting the value of list[key] to undefined. This won't remove the key, however - you'd need delete to do that:
list['test1'] = undefined; // list is now { test1: undefined, test2: [ ... ]}
delete list['test1']; // list is now { test2: [ ... ] }
Is there a particular reason you don't want to use delete? It won't make a difference if you're just checking list['test1'] for truthiness (e.g. if (list['test1']) ...), but if you want to iterate through list using for (var key in list) or something like that, delete is a better option.
EDIT: Ok, it looks like your actual question is "How can I remove a value from an array?", since that's what you're doing - the fact that your array is within an object, or contains objects rather than other values, is irrelevant. To do this, use the splice() method:
list.test1.splice(1,1); // list.test1 has been modified in-place
(Rewritten for corrected question.)
You can write:
list[value].splice(index, 1);
to delete list[value][index] and thereby shorten the array by one. (The above "replaces" 1 element, starting at position index, with no elements; see splice in MDN for general documentation on the method.)
Here is an example using splice:
http://jsfiddle.net/hellslam/SeW3d/