inserting item into a nested javascript object - javascript

how does one go about inserting an item into a nested javascript array of objects (with and without using a library)? running to a problem where once you insert the item after traversing, how would you reassign it back to the original object without manually accessing the object like data.content[0].content[0].content[0] etc..? already tried Iterate through Nested JavaScript Objects but could not get the reassignment to work
const data = {
"content": [
{
"name": "a",
"content": [
{
"name": "b",
"content": [
{
"name": "c",
"content": []
}
]
}
]
}
]
}
inserting {"name": "d", "content": []} into the contents of c
const data = {
"content": [
{
"name": "a",
"content": [
{
"name": "b",
"content": [
{
"name": "c",
"content": [{"name": "d", "content": []}]
}
]
}
]
}
]
}

const data = {
"content": [{
"name": "a",
"content": [{
"name": "b",
"content": [{
"name": "c",
"content": []
}]
}]
}]
}
const insert = createInsert(data)
insert({
"name": "d",
"content": []
}, 'c')
console.log(data)
// create a new function that will be able to insert items to the object
function createInsert(object) {
return function insert(obj, to) {
// create a queue with root data object
const queue = [object]
// while there are elements in the queue
while (queue.length) {
// remove first element from the queue
const current = queue.shift()
// if name of the element is the searched one
if (current.name === to) {
// push the object into the current element and break the loop
current.content.push(obj)
break
}
// add child elements to the queue
for (const item of current.content) {
queue.push(item)
}
}
}
}

It looks like we should assume that the name property uniquely identifies an object in the data structure. With that assumption you could create a mapping object for it, so to map a given name to the corresponding object in the nested structure. Also keep track which is the parent of a given object.
All this meta data can be wrapped in a decorator function, so that the data object gets some capabilities to get, add and remove certain names from it, no matter where it is in the hierarchy:
function mappable(data) {
const map = { "__root__": { content: [] } };
const parent = {};
const dfs = (parentName, obj) => {
parent[obj.name] = parentName;
map[obj.name] = obj;
obj.content?.forEach?.(child => dfs(obj.name, child));
}
Object.defineProperties(data, {
get: { value(name) {
return map[name];
}},
add: { value(parentName, obj) {
this.get(parentName).content.push(obj);
dfs(parentName, obj);
}},
remove: { value(name) {
map[parent[name]].content = map[parent[name]].content.filter(obj =>
obj.name != name
);
delete map[name];
delete parent[name];
}}
});
data.add("__root__", data);
}
// Demo
const data = {"content": [{"name": "a","content": [{"name": "b","content": [{"name": "c","content": []}]}]}]};
mappable(data);
data.add("c", { name: "d", content: [] });
console.log(data);
console.log(data.get("d")); // { name: "d", content: [] }
data.remove("d");
console.log(data.get("d")); // undefined
console.log(data); // original object structure

Related

Convert array of objects with paths strings into nested array [duplicate]

I'm looking for the best way to convert multiple string paths to a nested object with javascript. I'm using lodash if that could help in any way.
I got the following paths:
/root/library/Folder 1
/root/library/Folder 2
/root/library/Folder 1/Document.docx
/root/library/Folder 1/Document 2.docx
/root/library/Folder 2/Document 3.docx
/root/library/Document 4.docx
and I would like to create the following array of object:
var objectArray =
[
{
"name": "root", "children": [
{
"name": "library", "children": [
{
"name": "Folder 1", "children": [
{ "name": "Document.docx", "children": [] },
{ "name": "Document 2.docx", "children": [] }
]
},
{
"name": "Folder 2", "children": [
{ "name": "Document 3.docx", "children": [] }
]
},
{
"name": "Document 4.docx", "children": []
}
]
}
]
}
];
I suggest implementing a tree insertion function whose arguments are an array of children and a path. It traverses the children according to the given path and inserts new children as necessary, avoiding duplicates:
// Insert path into directory tree structure:
function insert(children = [], [head, ...tail]) {
let child = children.find(child => child.name === head);
if (!child) children.push(child = {name: head, children: []});
if (tail.length > 0) insert(child.children, tail);
return children;
}
// Example:
let paths = [
'/root/library/Folder 1',
'/root/library/Folder 2',
'/root/library/Folder 1/Document.docx',
'/root/library/Folder 1/Document 2.docx',
'/root/library/Folder 2/Document 3.docx',
'/root/library/Document 4.docx'
];
let objectArray = paths
.map(path => path.split('/').slice(1))
.reduce((children, path) => insert(children, path), []);
console.log(objectArray);
Iterate over each string and resolve it to an object:
var glob={name:undefined,children:[]};
["/root/library/Folder 1","/root/library/Folder 2","/root/library/Folder 1/Document.docx","/root/library/Folder 1/Document 2.docx","/root/library/Folder 2/Document 3.docx","/root/library/Document 4.docx"]
.forEach(function(path){
path.split("/").slice(1).reduce(function(dir,sub){
var children;
if(children=dir.children.find(el=>el.name===sub)){
return children;
}
children={name:sub,children:[]};
dir.children.push(children);
return children;
},glob);
});
console.log(glob);
http://jsbin.com/yusopiguci/edit?console
Improved version:
var glob={name:undefined,children:[]};
var symbol="/" /* or Symbol("lookup") in modern browsers */ ;
var lookup={[symbol]:glob};
["/root/library/Folder 1","/root/library/Folder 2","/root/library/Folder 1/Document.docx","/root/library/Folder 1/Document 2.docx","/root/library/Folder 2/Document 3.docx","/root/library/Document 4.docx"]
.forEach(function(path){
path.split("/").slice(1).reduce(function(dir,sub){
if(!dir[sub]){
let subObj={name:sub,children:[]};
dir[symbol].children.push(subObj);
return dir[sub]={[symbol]:subObj};
}
return dir[sub];
},lookup);
});
console.log(glob);
It creates the same result but it is may much faster ( up to O(n) vs. O(n+n!))
http://jsbin.com/xumazinesa/edit?console

KnockoutJS - Comparing observableArray with another observableArray

I have a json data formatted like this
{
"ITEMS": [
{
"option": "one",
"values": ["A","B","C","D"]
},
{
"option": "two",
"values": ["E","F","G"]]
}
]
}
and script code like this
function Item(data) {
this.option = ko.observable(data.option);
this.values = ko.observableArray(data.values);
}
var ProductIndexVM = (function() {
function ProductIndexVM() {
items = ko.observable();
filtered_items = ko.observableArray([]);
selectedItems = ko.computed(function() {
return ko.utils.arrayFilter(items(), function(item) {
// return something using compare
});
});
// after ajax success
var mappedItems = $.map(result.ITEMS, function(item) {
return new Item(item)
});
items(mappedItems);
}
return ProductIndexVM;
})();
After successful ajax request data has been mapped into items observable. The problem is I need to compare filtered_items observableArray with item functions' values observableArray so that if items inside filtered_items match with any one of the item functions' values it will return that or not. Simplifying whole thing I want to sort out items with only those contains in filtered_items.
As an example if filtered_items have content same as below
["A", "C", "E"]
the output will be
{
"ITEMS": [
{
"option": "one",
"values": ["A","C"]
},
{
"option": "two",
"values": ["E"]]
}
]
}
How can I compare that inside selectedItems?
This really has nothing much to do with knockout, its a simple algorithmic question. Given an array of object and an array of "selected values" how can I filter the former using the latter:
var input = {
"ITEMS": [
{
"option": "one",
"values": ["A","B","C","D"]
},
{
"option": "two",
"values": ["E","F","G"]
}
]
}
var filters = ["A","C","E"];
var result = input.ITEMS.map(item => {
return {
"option": item.option,
"values": item.values.filter(value => filters.indexOf(value)>-1)
}
});
console.log(result);
Now applied to your knockout, just read your observable arrays as you normally would but use the same algorithm as demonstrated above.

Create JSON Array dynamically from an object

I have an object A as shown below.
var A = {
"1": [ "1_1", "1_2", "1_3" ],
"2": [ "2_1", "2_2" ]
};
Need to build a new array dynamically using js. Suppose
object A key should map to attribute text of Array AA and value should be to children as given below.
var AA = [
{
"text": "1",
"state": "open",
"children": [
{ "text": "1_1" },
{ "text": "1_2" },
{ "text": "1_3" }
]
},
{
"text": "2",
"state": "open",
"children": [
{ "text": "2_1" },
{ "text": "2_2" }
]
}
];
This is my function but its not working as expected. Could someone pls help?
function constructJSONArr() {
var A = {
"1": [ "1_1", "1_2", "1_3" ],
"2": [ "2_1", "2_2" ]
};
for (var key in A) {
var tempArr = [];
tempArr.push(key);
for (var i = 0; i < key.length; i++) {
return {
'text': key,
'state': 'closed',
'children': A[key].map(function(child) {
return {
'text': child
};
})
}
}
}
}
When you return inside a function, the function ends and returns immediately. In your case, the return inside the for loop causes the function to return the 1st key object. To solve this, you need to create the objects and push them into an arr. You can return freely inside Array.map() because each iteration invokes a function.
Fixed solution:
Iterate with for...in. Get the key. Push a new object into arr. Use the key as the text property, the state, and children. To create the children get the array from the original object by the key, and use Array.map() to generate the child objects. Return arr.
var A = {
"1": ["1_1", "1_2", "1_3"],
"2": ["2_1", "2_2"]
};
function constructJSONArr(A) {
var arr = [];
for (var key in A) {
arr.push({
text: key,
state: 'closed',
children: A[key].map(function(t) {
return {
text: t
};
})
});
}
return arr;
}
var result = constructJSONArr(A);
console.log(result);
ESNext solution
Use Object.entries() to get keys and respective values from the object A. Iterate the entries with two nested Array.map() calls. The 1st to create the outer object, and the 2nd to create the children.
const A = {
"1": ["1_1", "1_2", "1_3"],
"2": ["2_1", "2_2"]
};
const constructJSONArr = (obj) =>
Object.entries(obj).map(([text, children]) => ({
text,
state: 'closed',
children: children.map((text) => ({
text
}))
}));
var result = constructJSONArr(A);
console.log(result);
You can use Object.keys() to iterate through the object and Array.map to create the new array.
var A = {
"1": ["1_1", "1_2", "1_3"],
"2": ["2_1", "2_2"]
};
var transformed = Object.keys(A).map(key => {
return {
text: key,
state: "open",
children: A[key].map(value => {
return {
text: value
};
})
};
});
console.log(transformed);

Create a directory tree from dropbox api delta using the paths

I found the question How to convert a file path into treeview?, but I'm not sure how to get the desired result in JavaScript:
I'm trying to turn an array of paths into a JSON tree:
var paths = [
"/org/openbmc/UserManager/Group",
"/org/stackExchange/StackOverflow",
"/org/stackExchange/StackOverflow/Meta",
"/org/stackExchange/Programmers",
"/org/stackExchange/Philosophy",
"/org/stackExchange/Religion/Christianity",
"/org/openbmc/records/events",
"/org/stackExchange/Religion/Hinduism",
"/org/openbmc/HostServices",
"/org/openbmc/UserManager/Users",
"/org/openbmc/records/transactions",
"/org/stackExchange/Religion/Islam",
"/org/openbmc/UserManager/Groups",
"/org/openbmc/NetworkManager/Interface"
];
I want to have json structure like below using the folder paths.
var xyz = [{
"path": "photos",
"name": "photos",
"children": [
{
"path": "photos/summer",
"name": "summer",
"children": [
{
"path": "photos/summer/june",
"name": "june",
"children": [
{
"path": "photos/summer/june/windsurf",
"name": "windsurf",
}
]
}
]
},
{
"path": "photos/winter",
"name": "winter",
"children": [
{
"path": "photos/winter/january",
"name": "january",
"children": [
{
"path": "photos/winter/january/ski",
"name": "ski",
},
{
"path": "photos/winter/january/snowboard",
"name": "snowboard",
}
]
}
]
}
]
}];
I have used below function but it's not working
var parsePathArray = function(paths) {
var parsed = [];
for (var i = 0; i < paths.length; i++) {
var position = parsed;
var split = paths[i].split('/');
for (var j = 0; j < split.length; j++) {
if (split[j] !== "") {
if (typeof position[split[j]] === 'undefined')
position[split[j]] = {};
position.children = [position[split[j]]];
position.name = split[j];
position = position[split[j]];
}
}
}
return parsed;
}
Disclaimer: I wrote this answer because it's a fun exercise. I'm still disappointed in you for not trying and not taking the time to explain what it is you don't understand...
I didn't follow your exact format so you'll have to try to understand how it's done instead of being able to copy the code and leave :)
I'll touch upon each step briefly to not risk explaining what you already know.
Step 1:
Go from a list of strings to a list of arrays:
["a/1", "a/2", "b/1"] -> [["a", "1"], ["a", "2"], ["b", "1"]]
We use String.prototype.slice to remove the prepended "/" and String.prototype.split with your folder delimiter to convert to an array: path.split("/")
Step 2
Loop over each folder and add the folder to an object.
[["a", "1"], ["a", "2"], ["b", "1"]] -> { a: { 1: {}, 2: {} }, b: { 1: {} } }
We use a reducer that accesses an object using bracket notation obj[key] instantiating new folder objects and returning the deepest location along the way.
Step 3
Recursively loop over the keys of your object and convert to a specified format:
{ a: { 1: { } } -> { name: "a", path: [], children: [ /* ... */ ] }
We take a list of keys, which are folder names, using Object.keys. Recursively call for each nested object.
Please, update your answer with the specific step you have trouble with which allows others to help as well, and me to describe the step in more detail.
const pathStrings = ["/org/openbmc/UserManager/Group", "/org/stackExchange/StackOverflow", "/org/stackExchange/StackOverflow/Meta", "/org/stackExchange/Programmers", "/org/stackExchange/Philosophy", "/org/stackExchange/Religion/Christianity", "/org/openbmc/records/events", "/org/stackExchange/Religion/Hinduism", "/org/openbmc/HostServices", "/org/openbmc/UserManager/Users", "/org/openbmc/records/transactions", "/org/stackExchange/Religion/Islam", "/org/openbmc/UserManager/Groups", "/org/openbmc/NetworkManager/Interface"];
const paths = pathStrings
.map(str => str.slice(1)) // remove first "/"
.map(str => str.split("/"));
// Mutates map!
const mergePathInToMap = (map, path) => {
path.reduce(
(loc, folder) => (loc[folder] = loc[folder] || {}, loc[folder]),
map
);
return map;
};
// Folder structure as { folderName: folderContents }
const folderMap = paths.reduce(mergePathInToMap, {});
// Go from
// { folderName: folderContents }
// to a desired format like
// { name: folderName, children: [contents] }
const formatStructure = (folder, path) => {
return Object
.keys(folder)
.map(k => ({
name: k,
path: path,
children: formatStructure(folder[k], path.concat(k))
}))
}
console.log(
JSON.stringify(
formatStructure(folderMap, []),
null,
2
)
)
.as-console-wrapper { min-height: 100% }

javascript filter nested object based on key value

I wish to filter a nested javascript object by the value of the "step" key:
var data = {
"name": "Root",
"step": 1,
"id": "0.0",
"children": [
{
"name": "first level child 1",
"id": "0.1",
"step":2,
"children": [
{
"name": "second level child 1",
"id": "0.1.1",
"step": 3,
"children": [
{
"name": "third level child 1",
"id": "0.1.1.1",
"step": 4,
"children": []},
{
"name": "third level child 2",
"id": "0.1.1.2",
"step": 5,
"children": []}
]},
]}
]
};
var subdata = data.children.filter(function (d) {
return (d.step <= 2)});
This just returns the unmodified nested object, even if I put value of filter to 1.
does .filter work on nested objects or do I need to roll my own function here, advise and correct code appreciated.
cjm
Recursive filter functions are fairly easy to create. This is an example, which strips a JS object of all items defined ["depth","x","x0","y","y0","parent","size"]:
function filter(data) {
for(var i in data){
if(["depth","x","x0","y","y0","parent","size"].indexOf(i) != -1){
delete data[i];
} else if (i === "children") {
for (var j in data.children) {
data.children[j] = filter(data.children[j])
}
}
}
return data;
}
If you would like to filter by something else, just updated the 2nd line with your filter function of choice.
Here's the function to filter nested arrays:
const filter = arr => condition => {
const res = [];
for (const item of arr) {
if (condition(item)) {
if (!item.children) {
res.push({ ...item });
} else {
const children = filter(item.children)(condition);
res.push({ ...item, children })
}
}
}
return res;
}
The only thing you have to do is to wrap your root object into an array to reach self-similarity. In common, your input array should look like this:
data = [
{ <...>, children: [
{ <...>, children: [...] },
...
] },
...
]
where <...> stands for some properties (in your case those are "name", "step" and "id"), and "children" is an optional service property.
Now you can pass your wrapped object into the filter function alongside a condition callback:
filter(data)(item => item.step <= 2)
and you'll get your structure filtered.
Here are a few more functions to deal with such structures I've just coded for fun:
const map = arr => f => {
const res = [];
for (const item of arr) {
if (!item.children) {
res.push({ ...f({ ...item }) });
} else {
res.push({ ...f({ ...item }), children: map(item.children)(f) });
}
}
return res;
}
const reduce = arr => g => init => {
if (!arr) return undefined;
let res = init;
for (const item of arr) {
if (!item.children) {
res = g(res)({ ...item });
} else {
res = g(res)({ ...item });
res = reduce(item.children)(g)(res);
}
}
return res;
}
Usage examples:
map(data)(item => ({ step: item.step }))
reduce(data)($ => item => $ + item.step)(0)
Likely, the code samples aren't ideal but probably could push someone to the right direction.
Yes, filter works on one array (list), like the children of one node. You have got a tree, if you want to search the whole tree you will need to use a tree traversal algorithm or you first put all nodes into an array which you can filter. I'm sure you can write the code yourself.

Categories