generate elements based on key structure of object - javascript

I have an multidimensional object. Now I wanna generate DOM-elements based on the key structure of this object.
As a default view all root keys should be shown as div elements. With a click on one of this elements the divs should be replaced with the direct children of the clicked key.
My current version looks like this
object:
let object = {
"1.0": {
"1.0.1": {},
"1.0.2": {},
},
"1.1": {
"1.1.1": {
"1.1.1.1": {},
},
"1.1.2": {},
},
};
this is my recursive function to generate DOM elements for each key:
function categoryTree(obj) {
for (var key in obj) {
categoryContainer.innerHTML += "<div>" + key + "</div>";
categoryTree(obj[key]);
}
}
Now, I don't know how to make this interactive and show the child keys only when the parent was clicked.

You could build nested html structure with createElement and for...in loop. And then you can also add event listener on div that will toggle its children display property.
let object = {
"1.0": {
"1.0.1": {},
"1.0.2": {}
},
"1.1": {
"1.1.1": {
"1.1.1.1": {}
},
"1.1.2": {}
}
}
let categoryContainer = document.querySelector(".categoryContainer")
function categoryTree(obj, parent, start = true) {
for (var key in obj) {
let div = document.createElement("div");
div.textContent = key;
if (parent.children) parent.className += " bold";
if (!start) div.className = "normal hide"
div.addEventListener('click', function(e) {
e.stopPropagation()
Array.from(div.children).forEach(child => {
child.classList.toggle('hide')
})
})
categoryTree(obj[key], div, false)
parent.appendChild(div)
}
}
categoryTree(object, categoryContainer)
.hide {display: none;}
.normal {font-weight: normal;}
.bold {font-weight: bold;}
<div class="categoryContainer"></div>

Just use the DOM methods:
const n = (type, settings = {}) => Object.assign(document.createElement(type), settings);
function treeNode(name, children) {
const text = n("p", { textContent: name });
const container = n("div");
container.style.display = "none";
for(const [childName, child] of Object.entries(children))
container.appendChild(treeNode(childName, child));
const node = n("div");
node.appendChild(text);
node.appendChild(container);
node.onclick = () => container.style.display = "block";
return node;
}
categoryContainer.appendChild(treeNode("root", object));

Related

Create Html Tree view with native javascript / HTML

I need to create an HTML/CSS tree view as in the example from already created object using native javascript.
Please suggest,
BR
You could first build nested structure and then use recursive approach to also create html from that data where if the current element has children property you call the function again with that children array as a data parameter.
var data = [{"name":"container-1","type":"container","description":"container description"},{"name":"category-1","type":"category","parent":"container-1"},{"name":"grid-1","type":"grid","parent":"category-1"},{"name":"chart-1","type":"chart","parent":"category-1"},{"name":"container-2","type":"container"},{"name":"category-2","type":"category","parent":"container-2"},{"name":"category-3","type":"category","parent":"container-2"},{"name":"grid-2","type":"grid","parent":"category-2"},{"name":"chart-2","type":"chart","parent":"category-2"},{"name":"grid-3","type":"grid","parent":"category-3"}]
function toTree(data, pid = undefined) {
return data.reduce((r, e) => {
if (pid == e.parent) {
const obj = { ...e }
const children = toTree(data, e.name)
if (children.length) obj.children = children;
r.push(obj)
}
return r
}, [])
}
function toHtml(data, isRoot = true) {
const ul = document.createElement('ul')
if (!isRoot) {
ul.classList.add('hide')
}
data.forEach(e => {
let isVisible = isRoot;
const li = document.createElement('li')
const text = document.createElement('span')
const button = document.createElement('button')
if (e.children) {
button.textContent = '+'
li.appendChild(button)
}
text.textContent = e.name
li.appendChild(text)
if (e.children) {
const children = toHtml(e.children, false)
li.appendChild(children)
button.addEventListener('click', function() {
if (isRoot) {
isVisible = !isVisible
}
button.textContent = isVisible ? '+' : '-'
children.classList.toggle('hide')
if (!isRoot) {
isVisible = !isVisible
}
})
}
ul.appendChild(li)
})
return ul;
}
const tree = toTree(data)
const html = toHtml(tree)
document.body.appendChild(html)
.hide {
display: none;
}
button {
margin-right: 10px;
}

Recursion with DOM in JavaScript

I am learning JavaScript and I was trying to solve a question given at https://javascript.info/modifying-document#create-a-tree-from-the-object.
The question was to create a nested ul/li from the nested object given.
The following is my code:
let data =
{
"Fish": {
"trout": {},
"salmon": {}
},
"Tree": {
"Huge": {
"sequoia": {},
"oak": {}
},
"Flowering": {
"redbud": {},
"magnolia": {}
}
}
};
function createTree(data,key=null,parent_element=document.body){
if(Object.keys(data).length){
if(key){
li = document.createElement('li');
li.textContent = key;
li = parent_element.append(li);
}
ul = document.createElement('ul');
parent_element.append(ul);
for(let key in data){
createTree(data[key],key,ul);
}
return
}
li = document.createElement('li');
li.textContent = key;
parent_element.append(li);
return;
}
createTree(data);
This produces the following output
while the expected output is the following
What is wrong with my code? I can't find anything wrong with my logic.
There is nothing wrong with your logic. The problem is, you forgot to put a var declaration before your ul variable in your createTree function. Add var before it and your code works. (You should ALWAYS declare variables with var, let, or const or things can get weird.)
let data = {
"Fish": {
"trout": {},
"salmon": {}
},
"Tree": {
"Huge": {
"sequoia": {},
"oak": {}
},
"Flowering": {
"redbud": {},
"magnolia": {}
}
}
};
function createTree(data, key = null, parent_element = document.body) {
var li;
if (Object.keys(data).length) {
if (key) {
li = document.createElement('li');
li.textContent = key;
li = parent_element.append(li);
}
var ul = document.createElement('ul');
parent_element.append(ul);
for(let key in data){
createTree(data[key], key, ul);
}
return;
}
li = document.createElement('li');
li.textContent = key;
parent_element.append(li);
return;
}
createTree(data);
Here's a quick breakdown of the different ways to declare variables in javascript and what each one means:
// Creates a global variable.
myVar1 = 1;
// Creates a variable within the scope you're currently in. It's "hoisted"
// to the top of the scope you're currently in, so if you declare a var in
// the middle of a function, it gets pulled to the very top when your code
// is executed.
var myVar2 = 2;
// Declares a variable that is not hoisted.
let myVar3 = 3;
// Declares a constant that cannot be reassigned.
const myVar4 = 4;
The reason your implementation failed is because ul became a global variable which caused your function to not return a desirable result.
let data = {
"Fish": {
"trout": {},
"salmon": {}
},
"Tree": {
"Huge": {
"sequoia": {},
"oak": {}
},
"Flowering": {
"redbud": {},
"magnolia": {}
}
}
};
function createTree(data, key = null, parent_element = document.body) {
if (Object.keys(data).length) {
let ul = document.createElement('ul');
if (key) {
li = document.createElement('li');
li.textContent = key;
li = parent_element.append(li);
}
ul = document.createElement('ul');
parent_element.append(ul);
for (let key in data) {
createTree(data[key], key, ul);
}
return
}
li = document.createElement('li');
li.textContent = key;
parent_element.append(li);
return;
}
createTree(data);
You need to create let ul = document.createElement('ul'); then need to append <li> was creating issue...

Unable to trigger click function in own custom plugin

I am trying to write own custom plain Javascript plugin.
Here is my sample plugin code:
(function() {
var pMethods = {
append: function(text) {
var node = this.node;
node.innerHTML += text;
},
click: function(fn) {
if (this.node instanceof Array) {
this.node.forEach(function(e) {
e.addEventListener('click', function() {
fn();
});
}, this);
} else {
this.node.addEventListener('click', function(e) {
fn(e);
});
}
}
};
myPlugin = function(selector) {
this.node = document.querySelectorAll(selector);
if (this.node.length === 1) {
this.node = this.node[0];
}
return this.node;
};
myPlugin.prototype = pMethods;
this.r = function(selector) {
return new myPlugin(selector);
};
}());
which has just two function append and click.
Here is my HTML:
<div class="close"></div>
Now I am trying to add click event on close div as follow:
r('.close').click(function() {
alert('Hi')
});
but it is not working as expected and I don't know what I'm missing here.
Your code did not work because you were explicitly checking if your element collection is an Array. Any element collection returned will be a NodeList which is an array like object, but not an array.
if (this.node instanceof Array)
should be
if (this.node instanceof NodeList)
Or you could use Array.prototype.slice to convert the NodeList to an Array
this.node = Array.prototype.slice.call(
document.querySelectorAll(selector)
)
Here are a couple of optimisations.
(function() {
var pMethods = {
append: function(text) {
// iterate over the collection
this.nodes.forEach(function(node) {
node.innerHTML += text;
})
// return this for chaining
return this
},
click: function(fn) {
// iterate over the collection
this.nodes.forEach(function(e) {
e.addEventListener('click', fn);
});
// return this for chaining
return this
},
find: function(selector) {
return new myPlugin(
// flat map over each of the nodes in the collection
this.nodes.reduce(function(nodes, node) {
return [].concat.apply(nodes, node.querySelectorAll(selector))
}, [])
)
}
};
myPlugin = function(nodes) {
// changed constructor to recievea array of elemnets only
// it's private so won't affect anything else
this.nodes = nodes
};
myPlugin.prototype = pMethods;
this.r = function(selector) {
var nodes = null
// handle creating the object with normal elements
if (selector instanceof HTMLElement) {
nodes = [selector]
}
else {
nodes = [].slice.call(
document.querySelectorAll(selector)
);
}
return new myPlugin(nodes);
};
}());
r('.close')
.click(function(e) {
console.log('alerts suck! ' + e.target.textContent)
r(e.target).find('.child').append(' appended child!')
})
.append(' append works!')
<div class="close">
close
<div class="child">this is the child</div>
</div>
your constructor function (e.g. myPlugin = function(selector) {) should return this instead of this.node

Create a hierarchical object from HTML outline

I'm ultimately wanting to create a jQuery plugin that loops through the top level of the DOM and adds elements to an object until it gets to a heading, at which point it pushes an object with the text of that heading.
The following sibling elements would be added to this new object until a new heading is encountered — if the heading is at the same level, a new object is created as a sibling of the parent object as before, and DOM siblings are added to that instead of the first object; if the heading is at a lower level, that's added as a child of the first object, and sibling DOM elements are added as children to that heading object; if it's a higher level headline, a new object is added one level above the last heading object and the cycle continues.
Example:
<p>wooo</p>
<h1>stuff</h1>
<p>stuff</p>
<p>more stuff</p>
<h2>yet more stuff</h2>
<p>still more stuff</p>
<h3>even still more stuff</h3>
<p>yep — stuff!</p>
<h1>still yet more stuff</h1>
<p>stuff stuff stuff</p>
<p>stuff stuff stuffarino</p>
Becomes...
{
'p_wooo': HTMLElementObject,
'h1_stuff': {
'p_stuff': HTMLElementObject,
'p_more_stuff': HTMLElementObject,
'h2_yet_more_stuff': {
'p_still_more_stuff': HTMLElementObject,
'h3_even_still_more_stuff': {
'p_yep_stuff': HTMLElementObject,
}
},
},
'h1_still_yet_more_stuff': {
'p_stuff_stuff_stuff': HTMLElementObject,
'p_stuff_stuff_stuffarino': HTMLElementObject
{
}
Here's what I have so far:
var root = $(res)
.filter('#contents')
.children()
.not('style'); // Don't need no stylesheets hurr!
var sections = root.filter('h1');
var outline = {};
for (var i = 0; i < sections.length; i++) {
var children;
if (i+1 <= sections.length) {
children = root.filter(sections[i]).after().nextUntil(sections[i+1]).filter(function(){return $(this).text().trim() !== '';});
}
var slug = getSlug($(sections[i]).text(), {separator: '_'});
outline[slug] = children;
}
console.dir(outline);
Alas, it only works for H1s. How would I turn this into a recursive function that adds H2-H6s?
I'll start you with an example that traverses the nodes and adds them all into the same tree object. It should be fairly easy to figure out the rest from here:
JSBin: http://jsbin.com/bixekutuhe/1/edit?html,js,output
// Helpers
function isNode(el) { return el && el.nodeType === 1; }
function tag(el) { return el.tagName.toLowerCase(); }
var tree = {}, key;
var node = document.body.firstElementChild;
while (isNode(node) && tag(node) !== 'script') { // add blacklists or whitelists that you might need
key = node.textContent;
tree[node.tagName.toLowerCase() + '_' +key.split(' ').join('_')] = node;
node = node.nextElementSibling; // move to next element
}
console.log(tree);
Update
Try the following example instead:
var tree = {};
var currentTree = tree, tagName, key;
var node = document.body.firstElementChild;
function isNode(el) { return el && el.nodeType === 1; }
function tag(el) { return el.tagName.toLowerCase(); }
while (isNode(node)) {
tagName = tag(node);
key = tagName + '_' + node.textContent.trim().split(' ').join('_');
switch(tagName) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
if (tagName === 'h1') {
currentTree = tree[key] = {};
} else {
currentTree = currentTree[key] = {};
}
break;
default:
currentTree[key] = node;
break;
}
// Move to the next element
node = node.nextElementSibling;
}
console.log(tree);

Setting multiple attributes for an element at once with JavaScript

How can I set multiple attributes at once with JavaScript? Unfortunately, I'm not able to use a framework like jQuery on this project. Here is what I have now:
var elem = document.createElement("img");
elem.setAttribute("src", "http://example.com/something.jpeg");
elem.setAttribute("height", "100%");
elem.setAttribute("width", "100%");
You could make a helper function:
function setAttributes(el, attrs) {
for(var key in attrs) {
el.setAttribute(key, attrs[key]);
}
}
Call it like this:
setAttributes(elem, {"src": "http://example.com/something.jpeg", "height": "100%", ...});
You might be able to use Object.assign(...) to apply your properties to the created element. Although some "properties (elem.height etc.) are read-only, i.e. accessors with only a getter (undefined setter)."
Keep in mind that height and width attributes are defined in pixels, not percents. You'll have to use CSS to make it fluid.
var elem = document.createElement('img')
Object.assign(elem, {
className: 'my-image-class',
src: 'https://dummyimage.com/320x240/ccc/fff.jpg',
height: 120, // pixels
width: 160, // pixels
onclick: function () {
alert('Clicked!')
}
})
document.body.appendChild(elem)
// One-liner:
// document.body.appendChild(Object.assign(document.createElement(...), {...}))
.my-image-class {
height: 100%;
width: 100%;
border: solid 5px transparent;
box-sizing: border-box
}
.my-image-class:hover {
cursor: pointer;
border-color: red
}
body { margin:0 }
You could code an ES5.1 helper function:
function setAttributes(el, attrs) {
Object.keys(attrs).forEach(key => el.setAttribute(key, attrs[key]));
}
Call it like this:
setAttributes(elem, { src: 'http://example.com/something.jpeg', height: '100%' });
2023 Update
Don't use this as an extension to Element.prototype. In 2012, it was debatable practice. In 2023, the debate is settled: it's not the way to go about things. Manipulating the prototype of library-external classes has risks that are difficult or impossible to mitigate; this is an ugly tool. I tried to note that, but was apparently not emphatic enough.
However, you can read the internal approach of the method and write it as a function, it would work the same. I might use something like this:
const setAttributes = (el, attrs) =>
Object.keys(attrs)
.filter(key => el[key] !== undefined)
.forEach(key =>
typeof attrs[key] === 'object'
? Object.keys(attrs[key])
.forEach(innerKey => el[key][innerKey] = attrs[key][innerKey])
: el[key] = attrs[key]
);
http://jsfiddle.net/uL8tm603/46/
Original 2012 answer follows
If you wanted a framework-esq syntax (Note: IE 8+ support only), you could extend the Element prototype and add your own setAttributes function:
Element.prototype.setAttributes = function (attrs) {
for (var idx in attrs) {
if ((idx === 'styles' || idx === 'style') && typeof attrs[idx] === 'object') {
for (var prop in attrs[idx]){this.style[prop] = attrs[idx][prop];}
} else if (idx === 'html') {
this.innerHTML = attrs[idx];
} else {
this.setAttribute(idx, attrs[idx]);
}
}
};
This lets you use syntax like this:
var d = document.createElement('div');
d.setAttributes({
'id':'my_div',
'class':'my_class',
'styles':{
'backgroundColor':'blue',
'color':'red'
},
'html':'lol'
});
Try it: http://jsfiddle.net/ywrXX/1/
If you don't like extending a host object (some are opposed) or need to support IE7-, just use it as a function
Note that setAttribute will not work for style in IE, or event handlers (you shouldn't anyway). The code above handles style, but not events.
Documentation
Object prototypes on MDN - https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/prototype
setAttribute on MDN - https://developer.mozilla.org/en-US/docs/DOM/element.setAttribute
You can create a function that takes a variable number of arguments:
function setAttributes(elem /* attribute, value pairs go here */) {
for (var i = 1; i < arguments.length; i+=2) {
elem.setAttribute(arguments[i], arguments[i+1]);
}
}
setAttributes(elem,
"src", "http://example.com/something.jpeg",
"height", "100%",
"width", "100%");
Or, you pass the attribute/value pairs in on an object:
function setAttributes(elem, obj) {
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
elem[prop] = obj[prop];
}
}
}
setAttributes(elem, {
src: "http://example.com/something.jpeg",
height: "100%",
width: "100%"
});
You could also make your own chainable object wrapper/method:
function $$(elem) {
return(new $$.init(elem));
}
$$.init = function(elem) {
if (typeof elem === "string") {
elem = document.getElementById(elem);
}
this.elem = elem;
}
$$.init.prototype = {
set: function(prop, value) {
this.elem[prop] = value;
return(this);
}
};
$$(elem).set("src", "http://example.com/something.jpeg").set("height", "100%").set("width", "100%");
Working example: http://jsfiddle.net/jfriend00/qncEz/
const setAttributes = (el, attrs) =>
Object.entries(attrs)
.forEach(args =>
el.setAttribute(...args))
Or create a function that creates an element including attributes from parameters
function elemCreate(elType){
var element = document.createElement(elType);
if (arguments.length>1){
var props = [].slice.call(arguments,1), key = props.shift();
while (key){
element.setAttribute(key,props.shift());
key = props.shift();
}
}
return element;
}
// usage
var img = elemCreate('img',
'width','100',
'height','100',
'src','http://example.com/something.jpeg');
FYI: height/width='100%' would not work using attributes. For a height/width of 100% you need the elements style.height/style.width
Try this
function setAttribs(elm, ob) {
//var r = [];
//var i = 0;
for (var z in ob) {
if (ob.hasOwnProperty(z)) {
try {
elm[z] = ob[z];
}
catch (er) {
elm.setAttribute(z, ob[z]);
}
}
}
return elm;
}
DEMO: HERE
you can simply add a method (setAttributes, with "s" at the end) to "Element" prototype like:
Element.prototype.setAttributes = function(obj){
for(var prop in obj) {
this.setAttribute(prop, obj[prop])
}
}
you can define it in one line:
Element.prototype.setAttributes = function(obj){ for(var prop in obj) this.setAttribute(prop, obj[prop]) }
and you can call it normally as you call the other methods. The attributes are given as an object:
elem.setAttributes({"src": "http://example.com/something.jpeg", "height": "100%", "width": "100%"})
you can add an if statement to throw an error if the given argument is not an object.
No function example:
let checkbox = document.createElement('input');
for (const [key, value] of Object.entries({
type: 'checkbox',
id: 'sys-surname',
class: 'switcher23',
value: 1,
name: 'surname'
})) {
checkbox.setAttribute(key, value);
}
let elem = document.createElement("img");
Object.entries({"src": "http://example.com/something.jpeg"),
"height": "100%",
"width": "100%"}).forEach(kv => elem.setAttribute(kv[0], kv[1]));
use this function to create and set attributes at the same time
function createNode(node, attributes){
const el = document.createElement(node);
for(let key in attributes){
el.setAttribute(key, attributes[key]);
}
return el;
}
use it like so
const input = createNode('input', {
name: 'test',
type: 'text',
placeholder: 'Test'
});
document.body.appendChild(input);
I guess it's best way to set attributes at once for any element in this class.
function SetAtt(elements, attributes) {
for (var element = 0; element < elements.length; element++) {
for (var attribute = 0; attribute < attributes.length; attribute += 2) {
elements[element].setAttribute(attributes[attribute], attributes[attribute + 1]);
}
}
}
var Class = document.getElementsByClassName("ClassName"); // class array list
var Data = ['att1', 'val1', 'att2', 'val2', 'att3', 'val3']; //attributes array list
SetAtt(Class, Data);
That's an easy way
let div = document.getElementsByTagName("div")[0];
let attr = ["class", "id", "title"];
let attrVlu = ["ahmed", "mohamed", "ashraf"];
for(let i = 0; i < 3; i++) {
div.setAttribute(attr[i], attrVlu[i]);
}
var elem = document.createElement("img");
function setAttributes(attributes) {
for (let key in attributes) {
elem.setAttribute(key, attributes[key]);
}
}
setAttributes({src: "http://example.com/something.jpeg",height: "100%",width: "100%",});
The most simple way is:
Create an array with objects into him, like:
array = [{obj1}, {obj2}, etc...]
next, you iterate on array to set the key's object like the
attribute and the value's object like the value of attribute:
example:
let arr = [{'id': 'myId'}, {'class': 'myClassname'}]
/iterate on array/
function setAt(array){
for (attr of array){
myElement.setAttribute(attr, array[attr])
}
}
later, you call the function passing your array like args:
setAt(arr)
THE EASIEST:
`
var elem = document.createElement("img");
var attrs = {
src: "http://example.com/something.jpeg",
height: "100%",
width: "100%"
}
Object.assign(elem, attrs);
`

Categories