How show javascript object as html tree? - javascript

I would like to generate a tree from the following object:
configuration:
{
name: "General",
icon: "general",
children: [
{
name: "Line 1",
icon: "line",
children: [
{
name: "Area 1",
icon: "area",
children: [
{ name: "Y", icon: "x" },
{ name: "Z", icon: "z" },
],
},
{ name: "Area 2", icon: "line" },
],
},
{
name: "Line 2,
icon: "line",
children: [{ name: "Area 3", icon: "area" }],
},
],
},
In html I have my own custom element:
<my-details>
<div slot="summary"><my-icon name="${icon}"></my-icon> ${name}</div>
${this.children} // ???
</my-details>`;
so I created the function:
createHierarchy() {
if (configuration?.length > 0) {
const details = _configuration.map(({ name, icon }) => {
return ` <my-details>
<div slot="summary"><my-icon name="${icon}"></my-icon> ${name}</div>
${this.children}
</my-details>`;
});
hierarchyContainer.innerHTML = details?.join("");
}
}
but I don't know how to convert this structure in a loop or map to a tree, each element of the hierarchy should be a my-details element and have a name as a slot and in the middle it should have children
the structure should look like this:
hierarchy tree

What you are looking for is a recursive function. Which is a function that calls itself.
It should end up looking something like this:
function parseNode(node) {
let html = `<my-details>
<div slot="summary">
<my-icon name="${node.icon}"></my-icon>
${node.name}.
</div>`;
if(node.children) {
node.children.forEach(childNode => {
html += parseNode(childNode);
})
;}
html += '</my-details>';
return html;
}
parseNode(configuration);
This example turns your entire configuration into an html string (or it should it is untested). You can output to your document/component.
Note that it relies on all nodes having a name and an icon and a possible "children" has to be an array.

Related

Accessing properties from an object directly?

In Javascript, if we have a scenario in which there is a large object and we need to access its properties then which approach is better?
Suppose we have this object:
let largeObj = {
items: {
item: [
{
id: "0001",
type: "donut",
name: "Cake",
ppu: 0.55,
batters: {
batter: [
{
id: "1001",
type: "Regular"
},
{
id: "1002",
type: "Chocolate"
},
{
id: "1003",
type: "Blueberry"
},
{
id: "1004",
type: "Devil's Food"
}
]
},
topping: [
{
id: "5001",
type: "None"
},
{
id: "5002",
type: "Glazed"
},
{
id: "5005",
type: "Sugar"
},
{
id: "5007",
type: "Powdered Sugar"
},
{
id: "5006",
type: "Chocolate with Sprinkles"
},
{
id: "5003",
type: "Chocolate"
},
{
id: "5004",
type: "Maple"
}
]
}
]
}
}
and we want to create firstBatterAndTopping object which has first batter and first topping data. Now, if we access properties of this object in below three ways, which approach is better in terms of performance. Is there any difference in these or these will perform the same. Or is there any other scenario like this in which this will impact performance?
let firstBatterAndTopping = {
batterId: largeObj.items.item[0].batters.batter[0].id,
batterType: largeObj.items.item[0].batters.batter[0].type,
toppingId: largeObj.items.item[0].topping[0].id,
toppingType: largeObj.items.item[0].topping[0].type
}
or
let firstItem = largeObj.items.item[0];
let firstBatterAndTopping = {
batterId: firstItem.batters.batter[0].id,
batterType: firstItem.batters.batter[0].type,
toppingId: firstItem.topping[0].id,
toppingType: firstItem.topping[0].type
}
or
let firstItem = largeObj.items.item[0];
let firstBatter = firstItem.batters.batter[0];
let firstTopping = firstItem.topping[0]
let firstBatterAndTopping = {
batterId: firstBatter.id,
batterType: firstBatter.type,
toppingId: firstTopping.id,
toppingType: firstTopping.type
}
Try immer out! Is a javascript library very useful for managing complex objects with a lot of nests.
Following a simple scenario:
import produce from "immer"
const baseState = [
{
title: "Learn TypeScript",
done: true
},
{
title: "Try Immer",
done: false
}
]
const nextState = produce(baseState, draftState => {
draftState.push({title: "Tweet about it"})
draftState[1].done = true
})
// the new item is only added to the next state,
// base state is unmodified
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)
// same for the changed 'done' prop
expect(baseState[1].done).toBe(false)
expect(nextState[1].done).toBe(true)
// unchanged data is structurally shared
expect(nextState[0]).toBe(baseState[0])
// ...but changed data isn't.
expect(nextState[1]).not.toBe(baseState[1])

Dynamically create multidimensional array from split input

I have an array of ojects which all have a path and a name property.
Like
[
{
"id": "1",
"path": "1",
"name": "root"
},
{
"id": "857",
"path": "1/857",
"name": "Animals"
},
{
"id": "1194",
"path": "1/857/1194",
"name": "Dinasours"
},
...and so on
]
Here are some path examples
1/1279/1282
1/1279/1281
1/1279/1280
1/857
1/857/1194
1/857/1194/1277
1/857/1194/1277/1278
I want to turn this into a multidimensional array like:
const data = {
id: "1",
name: "Root",
children: [
{
id: "1279",
name: "Toys",
},
{
id: "857",
name: "Animals",
children: [
{
id: "1194",
name: "Dinasours",
children: [
{
id: "1277",
name: "T-Rex",
children: [
{
id: "1278",
name: "Superbig T-Rex",
},
],
},
],
},
],
},
],
};
As you can understand the amount of data is much larger.
Is there a neat way to transform this data?
I wonder if this would be sufficient for your needs?
I'll refer to the objects as nodes (just because I'm a graph theory person, and that's how I roll).
Build an index mapping each id to the object itself using a Map. (Purely for efficiency. You could technically find each node from scratch by id each time you need it.)
Split the path to obtain the second last path fragment which should be the id of the direct parent of the node. (Assuming there's only one and that there is guaranteed to be a node corresponding to that id?)
Add the child to the parent's list of children. We'll be careful not to add it multiple times.
This will result in nodes that have no children literally having no children property (as opposed to having a children property that is just []). I also did not remove/delete the path property from the objects.
As a note of caution, if there are path fragments that do not have corresponding objects, this will not work.
const nodes = [
{ id: '1', path: '1', name: 'root' },
{ id: '857', path: '1/857', name: 'Animals' },
{ id: '1194', path: '1/857/1194', name: 'Dinasours' }
//...and so on
];
const index = new Map();
for (let node of nodes) {
index.set(node.id, node)
}
for (let node of nodes) {
const fragments = node.path.split('/');
const parentId = fragments[fragments.length - 2];
const parent = index.get(parentId);
if (parent !== undefined) {
parent.children = parent.children || [];
if (!parent.children.includes(node)) {
parent.children.push(node);
}
}
}
// TODO: Decide which node is the root.
// Here's one way to get the first (possibly only) root.
const root = index.get(nodes[0].path.split('/')[0]);
console.dir(root, { depth: null });
Assuming that the root is always the same I came up with this code, it took me some time but it was fun to think about it.
var data = {};
list.forEach(item => {
var path = item.path.split("/");
let parent = data;
path.forEach((id) => {
if (!parent.id) {
parent.id = id;
parent.children = [];
if (id != item.id) {
let next = {}
parent.children.push(next);
parent = next;
}
} else if (parent.id != id) {
let next = parent.children.find(child => child.id == id);
if (!next) {
next = { id: id, children: [] }
parent.children.push(next);
}
parent = next;
}
});
parent.id = item.id;
parent.name = item.name
});
output:
{
"id": "1",
"children": [
{
"id": "857",
"children": [
{
"id": "1194",
"children": [
{
"id": "1277",
"children": [
{ "id": "1278", "children": [], "name": "Superbig T-Rex" }
],
"name": "T-Rex"
}
],
"name": "Dinasours"
}
],
"name": "Animals"
},
{ "id": "1279", "children": [], "name": "Toys" }
],
"name": "Root"
}
I think that having more roots here may need some fixing. Although I think the problem would be different if we were talking about multiple roots since your data variable is an object
Also, if you think in a recursive way it can be more understandable, but no comments on performance.

Filter an array of objects based on another [TS/JS]

I have two dropdowns - where each dropdown should filter an objects key. The dropdowns should not exclude each other, or both values from dropdown should work indenpentedly from each other (ie both dropdown values does not need to be true for filtering).
When I select an item from the dropdown, I get one array with two objects, for each dropdown:
[
{
"name": "Type",
"value": [
"calibration"
],
"selected": [
{
"text": "calibration"
}
]
},
{
"name": "Function group",
"value": [
"1 - Test",
"2 - Programming"
],
"selected": [
{
"text": "1 - Test"
}
]
}
]
Above shows two objects, for the two different dropdowns - one with name "type" and one with "Function group".
The "value" in the object above is all of the dropdown items.
"selected" holds the selected item from the dropdown and the filtering should be based on that.In this case we have selected "calibration" and "Test".
The "type" dropdown should filter on the data "category" field while the "function group" should filter on "groupDescription" field. The data that needs to be filtered based on the mentioned keyes and selected values looks like this:
const mockData = [
{
operationDetails: {
id: '5119-03-03-05',
number: '41126-3',
description: 'Clutch wear, check. VCADS Pro operation',
category: 'calibration', //type dropdown
languageCode: 'en',
countryCode: 'GB'
},
functionDetails: {
groupId: 411,
groupDescription: 'Test', //function group dropdown
languageCode: '',
countryCode: ''
},
lastPerformed: '2021-02-22',
time: 20,
isFavorite: false
}
,
{
operationDetails: {
id: '5229-03-03-05',
number: '41126-3',
description: 'Defective brake pad',
category: 'calibration', ///type dropdown
languageCode: 'en',
countryCode: 'GB'
},
functionDetails: {
groupId: 411,
groupDescription: 'Programming', //function group dropdown
languageCode: '',
countryCode: ''
},
lastPerformed: '2020-01-22',
time: 20,
isFavorite: false
}
]
Playground with mock data and response example from dropdown here.
How to filter the data based on the values from the dropdown objects, for each key its responsible for?
It's not the prettiest code, but it does work. The one thing that you'd want to watch out for is the regex. It would be better to not have to parse and do a straight match like category, but if your cases are static then you should be able to figure out if this will work every time. It would also be nice to have a field key in filterDetails so you know which field to try to match in the actual data and you could program that in.
const filterDetails = [
{
name: "Type",
value: ["calibration"],
selected: [
{
text: "calibration",
},
],
},
{
name: "Function group",
value: ["1 - Test", "2 - Programming"],
selected: [
{
text: "Test",
},
],
},
];
const mockData = [
{
operationDetails: {
id: "5119-03-03-05",
number: "41126-3",
description: "Clutch wear, check. VCADS Pro operation",
category: "calibration", //type
languageCode: "en",
countryCode: "GB",
},
functionDetails: {
groupId: 411,
groupDescription: "Test", //function group
languageCode: "",
countryCode: "",
},
lastPerformed: "2021-02-22",
time: 20,
isFavorite: false,
},
{
operationDetails: {
id: "5229-03-03-05",
number: "41126-3",
description: "Defective brake pad",
category: "calibration", ///type
languageCode: "en",
countryCode: "GB",
},
functionDetails: {
groupId: 411,
groupDescription: "Programming", //function group
languageCode: "",
countryCode: "",
},
lastPerformed: "2020-01-22",
time: 20,
isFavorite: false,
},
];
console.log(
"filtered mockData: ",
mockData.filter(({ operationDetails, functionDetails }) => {
let groupDescriptionMatch = false;
let categoryMatch = false;
for (const details of filterDetails) {
if (
details.name === "Type" &&
details.selected[0].text === operationDetails.category
)
categoryMatch = true;
if (details.name === "Function group") {
let parsedGroup = details.selected[0].text.match(/[a-zA-Z]+/g);
if (parsedGroup[0] === functionDetails.groupDescription) {
groupDescriptionMatch = true;
}
}
}
return groupDescriptionMatch && categoryMatch;
})
);

Creating a tree from a flat list using lodash

I am trying to create a category tree using the array of json objects below.
I want to set a category as a child of another category if its parent equals the id of the other, and I want the posts also to be a children of that category instead of having a separate field for posts, I'll add a flag field that if it is a category or not isParent.
It looks like its working alright, but as you may see, if a category has both category and post as child, it'll only show the categories. Another problem with that is if the post has a null value on its array, it will still push them as children.
What are the mistakes in my code, or is there a simpler or better solution to this?
var tree = unflatten(getData());
var pre = document.createElement('pre');
console.log(tree);
pre.innerText = JSON.stringify(tree, null, 4);
document.body.appendChild(pre);
function unflatten(array, parent, tree) {
tree = typeof tree !== 'undefined' ? tree : [];
parent = typeof parent !== 'undefined' ? parent : {
id: 0
};
_.map(array, function(arr) {
_.set(arr, 'isParent', true);
});
var children = _.filter(array, function(child) {
return child.parent == parent.id;
});
if (!_.isEmpty(children)) {
if (parent.id == 0) {
tree = children;
} else {
parent['children'] = children;
}
_.each(children, function(child) {
var posts = _.map(child.posts, function(post) {
return _.set(post, 'isParent', false);
});
child['children'] = posts;
delete child.posts;
unflatten(array, child);
});
}
return tree;
}
function getData() {
return [{
"id": "c1",
"parent": "",
"name": "foo",
"posts": [{
"id": "p1"
}]
}, {
"id": "c2",
"parent": "1",
"name": "bar",
"posts": [{
"id": "p2"
}]
}, {
"id": "c3",
"parent": "",
"name": "bazz",
"posts": [
null
]
}, {
"id": "c4",
"parent": "3",
"name": "sna",
"posts": [{
"id": "p3"
}]
}, {
"id": "c5",
"parent": "3",
"name": "ney",
"posts": [{
"id": "p4"
}]
}, {
"id": "c6",
"parent": "5",
"name": "tol",
"posts": [{
"id": "p5"
}, {
"id": "p6"
}]
}, {
"id": "c7",
"parent": "5",
"name": "zap",
"posts": [{
"id": "p7"
}, {
"id": "p8"
}, {
"id": "p9"
}]
}, {
"id": "c8",
"parent": "",
"name": "quz",
"posts": [
null
]
}, {
"id": "c9",
"parent": "8",
"name": "meh",
"posts": [{
"id": "p10"
}, {
"id": "p11"
}]
}, {
"id": "c10",
"parent": "8",
"name": "ror",
"posts": [{
"id": "p12"
}, {
"id": "p13"
}]
}, {
"id": "c11",
"parent": "",
"name": "gig",
"posts": [{
"id": "p14"
}]
}, {
"id": "c12",
"name": "xylo",
"parent": "",
"posts": [{
"id": "p15"
}]
}, {
"id": "c13",
"parent": "",
"name": "grr",
"posts": [{
"id": "p16"
}, {
"id": "p17"
}, {
"id": "p14"
}, {
"id": "p18"
}, {
"id": "p19"
}, {
"id": "p20"
}]
}]
}
<script src="//cdn.jsdelivr.net/lodash/3.10.1/lodash.min.js"></script>
Expected Output
So the expected output will be more like:
[
{
id: 'c1',
isParent: true,
children: [
{
id: 'c2',
isParent: true,
children: []
},
{
id: 'p1'
isParent: false
}
]
}
]
And so on..
Your code is very imperative. Try focusing on the "big picture" of data flow instead of writing code by trial-and-error. It's harder, but you get better results (and, in fact, usually it's faster) :)
My idea is to first group the categories by their parents. This is the first line of my solution and it actually becomes much easier after that.
_.groupBy and _.keyBy help a lot here:
function makeCatTree(data) {
var groupedByParents = _.groupBy(data, 'parent');
var catsById = _.keyBy(data, 'id');
_.each(_.omit(groupedByParents, ''), function(children, parentId) {
catsById['c' + parentId].children = children;
});
_.each(catsById, function(cat) {
// isParent will be true when there are subcategories (this is not really a good name, btw.)
cat.isParent = !_.isEmpty(cat.children);
// _.compact below is just for removing null posts
cat.children = _.compact(_.union(cat.children, cat.posts));
// optionally, you can also delete cat.posts here.
});
return groupedByParents[''];
}
I recommend trying each part in the developer console, then it becomes easy to understand.
I have made a small fidde that I think that is what you want.
http://jsfiddle.net/tx3uwhke/
var tree = buildTree(getData());
var pre = document.getElementById('a');
var jsonString = JSON.stringify(tree, null, 4);
console.log(jsonString);
pre.innerHTML = jsonString;
document.body.appendChild(pre);
function buildTree(data, parent){
var result = [];
parent = typeof parent !== 'undefined' ? parent : {id:""};
children = _.filter(data, function(value){
return value.parent === parent.id;
});
if(!_.isEmpty(children)){
_.each(children, function(child){
if (child != null){
result.push(child);
if(!_.isEmpty(child.posts)){
var posts = _.filter(child.posts, function(post){
return post !== null && typeof post !== 'undefined';
});
if(!_.isEmpty(posts)){
_.forEach(posts, function(post){
post.isParent = false;
});
}
result = _.union(result, posts);
delete child.posts;
}
ownChildren = buildTree(data, child);
if(!_.isEmpty(ownChildren)){
child.isParent = true;
child.children = ownChildren;
}else{
child.isParent = false;
}
}
});
}
return result;
}
EDIT: made a new fiddle to contain the isParent part you can find it here
While this problem looks simple, I can remember to have struggled achieving it in a simple way. I therefore created a generic util to do so
You only have to write maximum 3 custom callbacks methods.
Here is an example:
import { flattenTreeItemDeep, treeItemFromList } from './tree.util';
import { sortBy } from 'lodash';
const listItems: Array<ListItem> = [
// ordered list arrival
{ id: 1, isFolder: true, parent: null },
{ id: 2, isFolder: true, parent: 1 },
{ id: 3, isFolder: false, parent: 2 },
// unordered arrival
{ id: 4, isFolder: false, parent: 5 },
{ id: 5, isFolder: true, parent: 1 },
// empty main level folder
{ id: 6, isFolder: true, parent: null },
// orphan main level file
{ id: 7, isFolder: false, parent: null },
];
const trees = treeItemFromList(
listItems,
(listItem) => listItem.isFolder, // return true if the listItem contains items
(parent, leafChildren) => parent.id === leafChildren.parent, // return true if the leaf children is contained in the parent
(parent, folderChildren) => parent.id === folderChildren.parent // return true if the children is contained in the parent
);
console.log(trees);
/*
[
{
children: [
{
children: [{ data: { id: 3, isFolder: false, parent: 2 }, isLeaf: true }],
data: { id: 2, isFolder: true, parent: 1 },
isLeaf: false,
},
{
children: [{ data: { id: 4, isFolder: false, parent: 5 }, isLeaf: true }],
data: { id: 5, isFolder: true, parent: 1 },
isLeaf: false,
},
],
data: { id: 1, isFolder: true, parent: null },
isLeaf: false,
},
{ children: [], data: { id: 6, isFolder: true, parent: null }, isLeaf: false },
{
data: {
id: 7,
isFolder: false,
parent: null,
},
isLeaf: true,
},
]
*/
I did not check with your example as all cases are different, you however need to implement only 3 methods to let the algorithm build the tree for you:
If the item is a folder or a leaf (in your case just check if the children contain any non falsy item) i.e. listItem.posts.some((value)=>!!value)
if a parent contains the leaf child, (parent, child) => !!parent.posts.filter((val)=>!!val).find(({id})=>child.id === id)
if a parent contains the folder: optional if this is the same logic as for a leaf child.

How to screen quotes for Cytoscape Web

I'm trying to use quoted text in one of node's attributes:
var network_json = {
dataSchema: {
nodes: [ { name: "label", type: "string" },
{ name: "foo", type: "string" }
]
},
data: {
nodes: [ { id: "1", label: "1", foo: "Text without quotes" },
{ id: "2", label: "2", foo: "Some \"quoted\" text" }
]
}
};
vis.draw({network: network_json});
And then making listeners for each node:
vis.addListener("click", "nodes", function(event) {
alert(event.target);
})
But I've got "Unexpected token ILLEGAL" error while clicking on a node with quoted text.
How should I screen quotes there?
For people who have same problem, it's a bug.
They promise they would fix it in next stable release.
https://groups.google.com/d/topic/cytoscapeweb-discuss/GWU0deOaaRs/discussion
They send fixed swf to email by request.

Categories

Resources