I wanted to reproduce the type of navigation system found in the console of Firefox and Chrome: you can explore an object's properties by unfolding boxes:
So I'm searching to:
display all of the object's properties (even if they're nested)
being able to fold/unfold the properties if themselves are objects
Element handling
I have written two functions createElement
and appendElement
, I will need them later:
/**
* Creates an HTML element without rendering in the DOM
* @params {String} htmlString is the HTML string that is created
* @return {HTMLElement} is the actual object of type HTMLElement
*/
const createElement = htmlString => {
const div = document.createElement('div');
div.innerHTML = htmlString;
return div.firstChild;
}
/**
* Appends the given element to another element already rendered in the DOM
* @params {String or HTMLElement} parent is either a CSS String selector or a DOM element
* {String or HTMLElement} element is either a HTML String or an HTMLElement
* @return {HTMLElement} the appended child element
*/
const appendElement = (parent, element) => {
element = element instanceof HTMLElement ? element : createElement(element);
return (parent instanceof HTMLElement ? parent : document.querySelector(parent))
.appendChild(element);
}
Version 1
My initial attempt was to use a recursive approach. Essentially each level would call the function for each of its children which would, in turn, call the function for their own children.
In the end, it would result in having the complete tree displayed on the page.
const showContent = (object, parent) => {
Object.keys(object).forEach(e => {
if(object[e] && object[e].constructor == Object) {
showContent(object[e], appendElement(parent, `<div class="level fold"><div class="parent"><span>-</span> ${e}<div></div>`));
} else {
appendElement(parent, `<div class="level">${e}: <span>${object[e]}</span></div>`)
}
});
}
// display the object's property
showContent(obj, 'body');
// toggle function to fold/unfold the properties
const toggle = () => event.target.parentElement.classList.toggle('fold');
// listen to click event on each element
document.querySelectorAll('.parent span').forEach(e => e.parentElement.addEventListener('click', toggle));
const showContent = (object, parent) => {
Object.keys(object).forEach(e => {
if(object[e] && object[e].constructor == Object) {
showContent(object[e], appendElement(parent, `<div class="level fold"><div class="parent"><span>-</span> ${e}<div></div>`));
} else {
appendElement(parent, `<div class="level">${e}: <span>${object[e]}</span></div>`)
}
});
}
// display the object's property
showContent(obj, 'body');
// toggle function to fold/unfold the properties
const toggle = () => event.target.parentElement.classList.toggle('fold');
// listen to click event on each element
document.querySelectorAll('.parent span').forEach(e => e.parentElement.addEventListener('click', toggle));
body>.level{position:relative;left:25%;width:50%}.level{background:lightgrey;border:2px solid brown;padding:4px;margin:4px;overflow:hidden}.level>span{color:#0366d5}.level>.parent{color:green;display:inline-block;width:100%}.fold{height:18px}
<script>const createElement=htmlString=>{const div=document.createElement('div');div.innerHTML=htmlString;return div.firstChild};const appendElement=(parent,element)=>{element=element instanceof HTMLElement?element:createElement(element);return(parent instanceof HTMLElement?parent:document.querySelector(parent)).appendChild(element)};const obj = {innerObj:{other:'yes',note:12,innerObj:{other:'no',note:1}},name:'one',int:1225,bool:true};</script>
Here are a few things I didn't like about this code:
there's an event on each property element, also the event listener needs to be set after the element has been added to the DOM. So calling
showContent
before the event handlers doesn't feel natural.this version doesn't support circular structures. For example:
let obj = {}; obj['obj'] = obj; showContent(obj);
will fail...
So this won't work for me. I need something able to handle cycles without too much trouble and that would not require to add an event listener each time a new property is unfolded.
Version 2
I came up with a better version that solved all these problems:
/**
* Shows all the object's properties with a depth of 1
* @params {Object} object, its first level properties are shown
* {String or HTMLElement} parent the element in which are displayed the properties
* @return {undefined}
*/
const showObject = (object, parent='body') => {
Object.entries(object).forEach(([key, value]) => {
if(value && value.constructor == Object) {
const element = appendElement(parent, `<div class="level fold"><div class="parent"><span>-</span> ${key}<div></div>`);
element.addEventListener('click', () => {showObject(value, element)}, {once: true});
} else {
appendElement(parent, `<div class="level">${key}: <span>${value}</span></div>`);
}
});
};
/**
* Toggles the CSS class .fold on the provided element
* @params {HTMLElement} element on which to toggle the class
* @return {undefined}
*/
const fold = element => element.classList.toggle('fold');
/**
* Document click listener
*/
document.addEventListener('click', function() {
const target = event.target;
const isFoldable = target.classList.contains('parent');
if(isFoldable) {
fold(target.parentElement);
}
});
/**
* Shows all the object's properties with a depth of 1
* @params {Object} object, its first level properties are shown
* {String or HTMLElement} parent the element in which are displayed the properties
* @return {undefined}
*/
const showObject = (object, parent='body') => {
Object.entries(object).forEach(([key, value]) => {
if(value && value.constructor == Object) {
const element = appendElement(parent, `<div class="level fold"><div class="parent"><span>-</span> ${key}<div></div>`);
element.addEventListener('click', () => {showObject(value, element)}, {once: true});
} else {
appendElement(parent, `<div class="level">${key}: <span>${value}</span></div>`);
}
});
};
/**
* Toggles the CSS class .fold on the provided element
* @params {HTMLElement} element on which to toggle the class
* @return {undefined}
*/
const fold = element => element.classList.toggle('fold');
/**
* Document click listener
*/
document.addEventListener('click', function() {
const target = event.target;
const isFoldable = target.classList.contains('parent');
if(isFoldable) {
fold(target.parentElement);
}
});
showObject(obj);
body>.level{position:relative;left:25%;width:50%}.level{background:lightgrey;border:2px solid brown;padding:4px;margin:4px;overflow:hidden}.level>span{color:#0366d5}.level>.parent{color:green;display:inline-block;width:100%}.fold{height:18px}
<script>const createElement=htmlString=>{const div=document.createElement('div');div.innerHTML=htmlString;return div.firstChild};const appendElement=(parent,element)=>{element=element instanceof HTMLElement?element:createElement(element);return(parent instanceof HTMLElement?parent:document.querySelector(parent)).appendChild(element)};const obj = {innerObj:{other:'yes',note:12,innerObj:{other:'no',note:1}},name:'one',int:1225,bool:true};</script>
Here it is seen working with a cycle:
let obj = {};
obj['obj'] = obj;
showObject(obj);
body>.level{position:relative;left:25%;width:50%}.level{background:lightgrey;border:2px solid brown;padding:4px;margin:4px;overflow:hidden}.level>span{color:#0366d5}.level>.parent{color:green;display:inline-block;width:100%}.fold{height:18px}
<script>const createElement=htmlString=>{const div=document.createElement('div');div.innerHTML=htmlString;return div.firstChild};const appendElement=(parent,element)=>{element=element instanceof HTMLElement?element:createElement(element);return(parent instanceof HTMLElement?parent:document.querySelector(parent)).appendChild(element)};const showObject=(object,parent='body')=>{Object.entries(object).forEach(([key,value])=>{if(value&&value.constructor==Object){const element=appendElement(parent,`<div class="level fold"><div class="parent"><span>-</span> ${key}<div></div>`);element.addEventListener('click',()=>{showObject(value,element)},{once:!0})}else{appendElement(parent,`<div class="level">${key}: <span>${value}</span></div>`)}})};const fold=element=>element.classList.toggle('fold');document.addEventListener('click',function(){const target=event.target;const isFoldable=target.classList.contains('parent');if(isFoldable){fold(target.parentElement)}})</script>
Questions
- What do you think?
- What could be improved (structure, naming, comments, programming style)?
- Any advice?