Recursive Function to Create DOM Tree String from DOM Object - javascript

I'm currently struggling with trying to build out a function that should take an input object from parsing HTML5 (node-html5-parser) and I need to process all tags and children tags, with their content to output a string of xml. The issue I'm running into is how to get a recursive function (or any function) to properly maintain the HTML5 tag order and outputting the content.
Example:
<div><span>My Content</span></div>
With the node-html5-parser, I get the following when it parses that:
rootElem {
childNodes: [
{
tagName: 'div',
childNodes: [
{
tagName: 'span',
childNodes: [
{
rawText: 'My Content'
}
],
}
]
}
]
}
I thought a simple DFS recursive algorithm could be used to build up a string, but I can't get it to work correctly.
const DOMRootObj = parse(this.htmlStr);
const processedData = this.processContent(DOMRootObj);
processContent(root: any): string {
if (root && !root.childNodes.length) {
return root.rawText;
} else {
for (const childNode of root.childNodes) {
const str = this.processContent(childNode);
const { tagName } = childNode;
if (tagName) {
this.outputStr += `<${tagName}>${str}</${tagName}>`;
}
}
}
}
from this HTML that's parsed by the parse() function: (object is as above)
<div><span>My Content</span></div>
This ends up outputting:
<span>undefined</span><div>undefined</div>
but It should output:
<div><span>My Content</span></div>

Not sure which XML format you expect, but the recursive call is not that hard to implement:
function toString(node, name="root") {
let tag = typeof name === "string" ? name : node.tagName;
return tag ? `<${tag}>${(node.childNodes||[]).map(toString).join``}</${tag}>`
: (node.rawText || "");
}
// Sample:
let rootElem = {
childNodes: [{
tagName: 'div',
childNodes: [{
tagName: 'span',
childNodes: [{
rawText: 'My Content'
}],
}]
}]
};
console.log(toString(rootElem, "root"));

Ive got a few days some same kind of problem inside a browsere. I want to walk over a dom tree and execute a function with given parameters on the nodes. In my case i want to reassign the element id if any. Thats why the function is named nodeinit. Anyhow
function nodeinit(node,dataset){console.log(node);}
function shadowWalker(node, nodeinit, dataset) {
nodeinit(node, dataset);
if (node) {
console.log(node.id);
if (node.childNodes.length > 0) {
node = node.firstChild; //needed to init while loop
while (node) {
shadowWalker(node, nodeinit, dataset);
node = node.nextSibling;
}
}
}
}
So its called with the node to start of. Nodeinit is a function and dataset is the parameter object for the nodeinit function (some kind of presets). Some similar answers you can find here by looking for transverse dom tree. Just as a idea or starting point.

Unpolished code, but you can get the ideia. You can add more attributes like onclick and tabindex simply by repeating the pattern. Although I don't think putting everything on the generator a good idea (you can lose track of what's being generated).
fillHeaderCurrenciesList = () => {
// Generated structure example
// <div
// id='currency-1'
// class='currency currency--hoverable'
// onclick='selectMainCurrency(this.id)'
// >
// <div class='currency__name'>Bitcoin</div>
// <div class='currency__icon'>
// <img src='assets/img/icon-coin.svg' alt='LG Bank' />
// <img src='assets/img/icon-btc.svg' alt='LG Bank' />
// </div>
// <div class='currency__balance'>
// <span class='currency__title'>Balance</span>
// <div class='currency__value'>
// <span>Exemplo1 | R$ 234.342.367,90</span>
// </div>
// </div>
// </div>;
let currenciesList = document.getElementById('currencies-list');
for (let i = 0; i < currencyData.length; i++) {
const currency = currencyData[i];
let currencyTree = {
tagName: 'div',
id: `currency-${i}`,
class: ['currency', 'currency--hoverable'],
onClick: function () {
selectMainCurrency(this, 'currencies-main-currency');
},
onKeyDown: function () {
keyDownEnter(
selectMainCurrency(this, 'currencies-main-currency')
);
},
tabIndex: '0',
children: [
{
tagName: 'div',
class: ['currency__name'],
text: currency.name,
},
{
tagName: 'div',
class: ['currency__icon'],
children: [
{
tagName: 'img',
src: currency.icon.src,
alt: currency.icon.alt,
},
{
tagName: 'img',
src: currency.iconName.src,
alt: currency.iconName.alt,
},
],
},
{
tagName: 'div',
class: ['currency__balance'],
children: [
{
tagName: 'span',
class: ['currency__title'],
text: 'Balance',
},
{
tagName: 'div',
class: ['currency__value'],
children: [
{
tagName: 'span',
text: currency.balance,
},
],
},
],
},
],
};
currenciesList.appendChild(DOMListItemGenerator(currencyTree));
}
};
const currencyData = [
{
name: 'bitcoin',
icon: {
src: 'assets/img/icon-coin.svg',
alt: 'LG Bank',
},
iconName: {
src: 'assets/img/icon-btc.svg',
alt: 'LG Bank',
},
quotation: '',
balance: 'Exemplo1bitcoin | R$ 234.342.367,90',
},
{
name: 'ethereum',
icon: {
src: 'assets/img/icon-coin.svg',
alt: 'LG Bank',
},
iconName: {
src: 'assets/img/icon-btc.svg',
alt: 'LG Bank',
},
quotation: '',
balance: 'Exemplo2ethereum | R$ 234.342.367,90',
},
{
name: 'ethereum',
icon: {
src: 'assets/img/icon-coin.svg',
alt: 'LG Bank',
},
iconName: {
src: 'assets/img/icon-btc.svg',
alt: 'LG Bank',
},
quotation: '',
balance: 'Exemplo2ethereum | R$ 234.342.367,90',
},
{
name: 'ethereum',
icon: {
src: 'assets/img/icon-coin.svg',
alt: 'LG Bank',
},
iconName: {
src: 'assets/img/icon-btc.svg',
alt: 'LG Bank',
},
quotation: '',
balance: 'Exemplo2ethereum | R$ 234.342.367,90',
},
{
name: 'teste',
icon: {
src: 'assets/img/icon-coin.svg',
alt: 'LG Bank',
},
iconName: {
src: 'assets/img/icon-btc.svg',
alt: 'LG Bank',
},
quotation: '',
balance: 'Exemplo3teste | R$ 234.342.367,90',
},
];
fillHeaderCurrenciesList();
DOMListItemGenerator = (inputTree) => {
let tree = Object.entries(inputTree);
let item;
let output;
for (let i = 0; i < tree.length; i++) {
const branch = tree[i];
if (branch[0] === 'tagName') {
output = document.createElement(branch[1]);
}
if (branch[0] === 'id') {
output.setAttribute('id', branch[1]);
}
if (branch[0] === 'tabIndex') {
output.setAttribute('tabindex', branch[1]);
}
if (branch[0] === 'class') {
for (const classItem of branch[1]) {
output.classList.add(classItem);
}
}
if (branch[0] === 'src') {
output.src = branch[1];
}
if (branch[0] === 'alt') {
output.alt = branch[1];
}
if (branch[0] === 'text') {
output.textContent = branch[1];
}
if (branch[0] === 'onClick') {
output.onclick = branch[1];
}
if (branch[0] === 'onKeyDown') {
output.onkeydown = branch[1];
}
if (branch[0] === 'children' && branch[1].length > 0) {
for (let j = 0; j < branch[1].length; j++) {
const children = branch[1][j];
item = DOMListItemGenerator(children);
output.appendChild(item);
}
}
}
return output;
};

Related

How to loop over nested JSON array and filter out search Query in Angular?

I have a complex nested JSON Array and I want to filter it(name property) through based on what user enters in input tag and show it as an autocomplete. A basic of it I have created here on stackblitz click here for the code. I have two entries of name "Tom" in two different objects so when user types Tom it should appear twice in the autocomplete as of now it shows only once. So if I press letter "T" it should show me all the names starting with "T". Here in this case "Tom" twice if I press "To" and if I press just "T" then Tiffany and Tom 2 times. Could you please help me here in the code ?
Any help is appreciated. Thanks much!
You can check the below code also, I have updated code you check in stackbliz https://stackblitz.com/edit/angular-ivy-k14se7
matches = [];
ngOnInit() {}
searchKeyup(ev) {
var term: any = ev.target as HTMLElement;
console.log("Value", term.value);
this.matches = [];
let content = [
{
name: 'Robert',
children: [],
},
{
name: 'Doug',
children: [
{
name: 'James',
children: [
{
name: 'John',
children: [
{
name: 'Tom',
children: [],
},
],
},
],
},
],
},
{
name: 'Susan',
children: [
{
name: 'Tiffany',
children: [
{
name: 'Merry',
children: [
{
name: 'Sasha',
children: [],
},
{
name: 'Tommy',
children: [],
},
],
},
],
},
],
},
];
if(term.value.length > 0){
this.filter(content, term.value);
} else {
document.getElementById('list').innerHTML = '';
}
if (this.matches.length > 0) {
document.getElementById('list').innerHTML = this.matches.map(match => match.name).join(",");
} else{
document.getElementById('list').innerHTML = "";
}
}
filter(arr, term) {
arr.forEach((i) => {
if (i.name.includes(term)) {
this.matches.push(i);
}
if (i.children.length > 0) {
this.filter(i.children, term);
}
});
console.log(this.matches);
}
You were on a good path. The only missing thing in this recursive walk is keeping state of matches like this:
filter(arr, term, matches) {
if (term.length == 0) {
matches = [];
document.getElementById('list').innerHTML = '';
}
arr.forEach((i) => {
if (i.name.includes(term)) {
matches.push(i);
}
if (i.children.length > 0) {
this.filter(i.children, term, matches);
}
});
console.log(matches);
if (matches.length > 0) {
document.getElementById('list').innerHTML = matches[0].name;
}
}

Search Inside Dropdown with data in tree structure React JS

I have developed a custom component which renders dropdown with a tree like structure inside it and allows the user to search for values inside the dropdown. Somehow the search works only after two levels of the tree structure.
We would be able to search only on the inside of NextJS label. The previous levels do not render results.
My function looks like this:
const searchFunction = (menu: treeData[], searchText: string) => {
debugger; //eslint-disable-line no-debugger
for (let i = 0; i < menu.length; i++) {
if (menu[i].name.includes(searchText)) {
setFound(true);
return menu[i].name;
} else if (!menu[i].name.includes(searchText)) {
if (menu[i].children !== undefined) {
return searchFunction(menu[i].children, searchText);
}
} else {
return 'Not Found';
}
}
};
And My data is like this:
import { treeData } from './DdrTreeDropdown.types';
export const menu: treeData[] = [
{
name: 'Web Project',
children: [
{
name: 'NextJS',
children: [
{
name: 'MongoDB',
},
{
name: 'Backend',
children: [
{
name: 'NodeJS',
},
],
},
],
},
{
name: 'ReactJS',
children: [
{
name: 'Express',
},
{
name: 'mysql',
children: [
{
name: 'jwt',
},
],
},
],
},
],
},
{
name: 'lorem project',
children: [
{
name: 'Vue Js',
children: [
{
name: 'Oracle Db',
},
{
name: 'JDBC',
children: [
{
name: 'Java',
},
],
},
],
},
{
name: 'ReactJS',
children: [
{
name: 'Express',
},
{
name: 'mysql',
children: [
{
name: 'jwt',
},
],
},
],
},
],
},
];
The sandbox link of the component is here:
https://codesandbox.io/s/upbeat-feynman-89ozi?file=/src/styles.ts
I haven't looked at the context that this is used in, so apologies if I'm missing something about how this is supposed to work. I've assumed that you can call setFound after running this function based on whether it finds anything or not and that it only needs to return one value. But hopefully this helps.
const menu = [{"name":"Web Project","children":[{"name":"NextJS","children":[{"name":"MongoDB"},{"name":"Backend","children":[{"name":"NodeJS"}]}]},{"name":"ReactJS","children":[{"name":"Express"},{"name":"mysql","children":[{"name":"jwt"}]}]}]},{"name":"lorem project","children":[{"name":"Vue Js","children":[{"name":"Oracle Db"},{"name":"JDBC","children":[{"name":"Java"}]}]},{"name":"ReactJS","children":[{"name":"Express"},{"name":"mysql","children":[{"name":"jwt"}]}]}]}];
const searchFunction = (menu, searchText) => {
let result;
for(let i = 0; i < menu.length; i++) {
if(menu[i].name.includes(searchText)) {
return menu[i].name;
} else if(menu[i].children !== undefined) {
result = searchFunction(menu[i].children, searchText);
if(result) return result;
}
}
return null;
};
console.log(searchFunction(menu, 'NextJS'));
console.log(searchFunction(menu, 'jwt'));
console.log(searchFunction(menu, 'foo'));
Looking at why the current version doesn't work, I think it goes something like this:
Let's take 'jwt' as the searchText.
We start in the 'Web Project' object, the name does not match, so we go to the else if block (BTW, we can never reach the else block as the else if condition is the opposite of the if condition).
The 'Web Project' object does have children so we will return from the new call to searchFunction; notice that 'lorem project' can never be reached as we will (regardless of the result) return the value of searchFunction and skip the rest of the loop.
Inside of our new and subsequent calls to searchFunction the same is going to happen until we find either a matching item or an item without children.
If we get to an item without children the the loop will successfully carry on to the siblings of the item.
If it doesn't find a match or an item with children it will exit the for loop and return undefined up the chain to the caller of the initial searchFunction.

Javascript - transforming an object of array list to new formated one?

I'm trying to transform an object contain array to another one with javascript. Below is an example of the object field and what the formatted one should look like.
let Fields = {
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
};
I need The new Fields to looks like this
let newFields = {
name: 'GAME',
tags:[
{ name: 'playPES', value: "{{PES}}" },
{ name: 'playFIFA', value: "{{FIFA}}" }
]},
One contributor suggested me a method like this but i think something need to modify in it but couldn't figure it out.
export const transform = (fields) => ({
tags: Object .entries (fields) .map (([name, innerFields]) => ({
name,
tags: innerFields.map(({code, title: title: {en})=>({name: en, value: code}))
}))
});
// newFields= transform(Fields)
I'm new working with javascript so any help is greatly appreciated, Thanks.
const transform = (o) => {
return Object.entries(o).map((e)=>({
name: e[0],
tags: e[1].map((k)=>({name: (k.title)?k.title.en:undefined, value: k.code}))
}))[0]
}
console.log(transform({
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
}))
Using the entries method you posted:
let Fields = {
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
};
// 1. Obtain keys and values from first object
Fields = Object.entries(oldFields);
// 2. Create new object
const newFields = {};
// 3. Create the name key value pair from new Fields array
newFields.name = Fields[0][0];
// 4. Create the tags key value pair by mapping the subarray in the new Fields array
newFields.tags = Fields[0][1].map(entry => ({ name: entry.title.en, value: entry.code }));
Object.entries(Fields) will return this:
[
"GAME",
[TagsArray]
]
And Object.entries(Fields).map will be mapping this values.
The first map, will receive only GAME, and not an array.
Change the code to something like this:
export const transform = (Fields) => {
const [name, tags] = Object.entries(Fields);
return {
name,
tags: tags.map(({ code, title }) => ({
name: title.en,
value: code
}))
}
}
Hope it help :)
let Fields = {
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
};
let newFields = {
name: 'GAME',
tags:[
{ name: 'playPES', value: "{{PES}}" },
{ name: 'playFIFA', value: "{{FIFA}}" }
]
}
let answer = {
name: "Game",
tags: [
]
}
Fields.GAME.map(i => {
var JSON = {
"name": i.title.en,
"value": i.code
}
answer.tags.push(JSON);
});
console.log(answer);
I think that this is more readable, but not easier... If you want the result as object you need to use reduce, because when you do this
Object.keys(Fields)
Your object transform to array, but reduce can change array to object back.
let Fields = {
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
};
const result = Object.keys(Fields).reduce((acc, rec) => {
return {
name: rec,
tags: Fields[rec].map(el => {
return {
name: el.title.en,
value: el.code
}
})
}
}, {})
console.log(result)
let Fields = {
GAME: [
{ code: '{{PES}}', title: { en: "playPES"} },
{ code: '{{FIFA}}', title: { en: "playFIFA " } },
]
};
const transform = (fields) => ({
tags: Object .entries (fields) .map (([name, innerFields]) => ({
name,
tags: innerFields.map(({code, title: title,en})=>({name: title.en, value: code}))
}))
});
//check required output in console
console.log(transform(Fields));

Create a key map for all paths in a recursive/nested object array

I have an n levels deep nested array of tag objects with title and ID. What I'm trying to create is a an object with IDs as keys and values being an array describing the title-path to that ID.
I'm no master at recursion so my attempt below doesn't exactly provide the result I need.
Here's the original nested tag array:
const tags = [
{
title: 'Wood',
id: 'dkgkeixn',
tags: [
{
title: 'Material',
id: 'ewyherer'
},
{
title: 'Construction',
id: 'cchtfyjf'
}
]
},
{
title: 'Steel',
id: 'drftgycs',
tags: [
{
title: 'Surface',
id: 'sfkstewc',
tags: [
{
title: 'Polished',
id: 'vbraurff'
},
{
title: 'Coated',
id: 'sdusfgsf'
}
]
},
{
title: 'Quality',
id: 'zsasyewe'
}
]
}
]
The output I'm trying to get is this:
{
'dkgkeixn': ['Wood'],
'ewyherer': ['Wood', 'Material'],
'cchtfyjf': ['Wood', 'Construction'],
'drftgycs': ['Steel'],
'sfkstewc': ['Steel', 'Surface'],
'vbraurff': ['Steel', 'Surface', 'Polished'],
'sdusfgsf': ['Steel', 'Surface', 'Coated'],
'zsasyewe': ['Steel', 'Quality']
}
So I'm building this recursive function which is almost doing it's job, but I keep getting the wrong paths in my flat/key map:
function flatMap(tag, acc, pathBefore) {
if (!acc[tag.id]) acc[tag.id] = [...pathBefore];
acc[tag.id].push(tag.title);
if (tag.tags) {
pathBefore.push(tag.title)
tag.tags.forEach(el => flatMap(el, acc, pathBefore))
}
return acc
}
const keyMap = flatMap({ title: 'Root', id: 'root', tags}, {}, []);
console.log("keyMap", keyMap)
I'm trying to get the path until a tag with no tags and then set that path as value for the ID and then push the items 'own' title. But somehow the paths get messed up.
Check this, makePaths arguments are tags, result object and prefixed titles.
const makePaths = (tags, res = {}, prefix = []) => {
tags.forEach(tag => {
const values = [...prefix, tag.title];
Object.assign(res, { [tag.id]: values });
if (tag.tags) {
makePaths(tag.tags, res, values);
}
});
return res;
};
const tags = [
{
title: "Wood",
id: "dkgkeixn",
tags: [
{
title: "Material",
id: "ewyherer"
},
{
title: "Construction",
id: "cchtfyjf"
}
]
},
{
title: "Steel",
id: "drftgycs",
tags: [
{
title: "Surface",
id: "sfkstewc",
tags: [
{
title: "Polished",
id: "vbraurff"
},
{
title: "Coated",
id: "sdusfgsf"
}
]
},
{
title: "Quality",
id: "zsasyewe"
}
]
}
];
console.log(makePaths(tags));

Javascript unlimited category depth tree

I am trying to generate a tree of flat array , placing sub categories next to the parent category and having hard doing same.
var categories = [
{
name: 'Javascript'
},
{
name: 'jQuery',
parent: 'Javascript'
},
{
name: 'AngularUi',
parent: 'Angular'
},
{
name: 'Angular',
parent: 'Javascript'
},
{
name: 'D3',
parent: 'Javascript'
}
];
var tree = [];
function getChilds(array,identifier){
return _.filter(array,function(val){
return val.parent == identifier
});
}
function createTree(array){
for(var x=0;x<_.size(array);x++){
tree.push(array[x].name);
var childs = getChilds(array,array[x].name);
if(_.size(childs) > 0){
createTree(childs);
}else{
$('div').append(JSON.stringify(tree));
}
}
}
createTree(categories);
<script src="http://underscorejs.org/underscore-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div></div>
Expected Output
['Javascript','Jquery','Angular','AngularUi','D3']
This is what i have tried so far, and using underscore for little help. Any help will be appreciated .
I don't know why you would want to do this (the result you have described is not a tree at all), but the following will provide the result you are describing:
function addChildren(source, identifier, dest) {
source.filter(function(val) {
return val.parent == identifier;
}).forEach(function(val) {
dest.push(val.name);
addChildren(source, val.name, dest);
});
}
function buildTree(source) {
var dest = [];
addChildren(source, undefined, dest);
return dest;
}
var categories = [{
name: 'Javascript'
}, {
name: 'jQuery',
parent: 'Javascript'
}, {
name: 'AngularUi',
parent: 'Angular'
}, {
name: 'Angular',
parent: 'Javascript'
}, {
name: 'D3',
parent: 'Javascript'
}];
var tree = buildTree(categories);
Note that the approach above using filter is an O(N2) operation (it's very inefficient).
You can change it into an O(N) operation by indexing the source arrray first using _.groupBy:
function addChildren(index, identifier, dest) {
(index[identifier] || []).forEach(function (val) {
dest.push(val.name);
addChildren(index, val.name, dest);
});
}
function buildTree(source) {
var dest = [];
addChildren(_.groupBy(source, 'parent'), undefined, dest);
return dest;
}
var categories = [{
name: 'Javascript'
}, {
name: 'jQuery',
parent: 'Javascript'
}, {
name: 'AngularUi',
parent: 'Angular'
}, {
name: 'Angular',
parent: 'Javascript'
}, {
name: 'D3',
parent: 'Javascript'
}];
var tree = buildTree(categories);
console.log(tree);
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>

Categories

Resources