EN
JavaScript - display object as expandable tree
7
points
In this article, we would liek to show how in simple way display object as expandable tree using JavaScript.
Presented solution creates tree using HTML elements. The tree can be created in expanded state by setting expanded
argument in renderEntry()
functuion that indicates how many levels in depth should be expanded. It means by using 0
we create collapsed tree.
Practical example:
// ONLINE-RUNNER:browser;
<!doctype html>
<html>
<head>
<style>
div.item {
margin: 5px 0 0 0;
border: 1px solid #e8e8e8;
}
div.item div.item {
margin: 0;
border-style: solid none none none;
}
div.expander {
display: flex;
cursor: pointer;
}
div.button {
flex: 0;
width: 0;
height: 0;
}
div.button.expanded {
margin: 6px 4.5px 0 1px;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 8px solid #3685d6;
}
div.button.collapsed {
margin: 4.5px 3.5px;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 8px solid #3685d6;
}
div.type {
padding: 0 0 2px 0;
flex: 1;
color: #39add0;
}
div.tree {
margin: 0 0 0 16px;
flex: 1 100%;
display: table;
}
div.line {
display: table-row;
}
div.key {
padding: 0 16px 0 0;
width: 0%;
min-height: 19px;
display: table-cell;
vertical-align: top;
color: #881280;
}
div.object {
padding: 0;
display: flex;
flex-direction: column;
}
div.value {
padding: 0 0 0 1px;
min-height: 19px;
}
div.value.nil {
color: #221199
}
div.value.boolean {
color: #221199
}
div.value.number {
color: #2771bb;
}
div.value.string {
color: #d03131;
}
div.value.symbol {
color: #c80042;
}
div.value.function {
color: #770088;
}
div.value.bigint {
color: #2771bb;
}
div.value.regexp {
color: #ff5500;
}
</style>
</head>
<body>
<script>
const TO_STRING = Object.prototype.toString;
const getType = (entry) => {
const text = TO_STRING.call(entry);
return text.slice(8, -1);
};
const iterateEntries = (entry, callback) => {
for (const key in entry) {
callback(key, entry[key]);
}
};
const createButton = (expanded) => {
const hButton = document.createElement('div');
hButton.className = 'button ' + (expanded > 0 ? 'expanded' : 'collapsed');
return hButton;
};
const createType = (type) => {
const hType = document.createElement('div');
hType.className = 'type';
hType.innerText = type;
return hType;
};
const createExpander = (type, expanded, onClick) => {
const hExpander = document.createElement('div');
hExpander.addEventListener('click', () => {
const classes = hButton.classList;
if (expanded > 0) {
classes.remove('expanded');
classes.add('collapsed');
expanded = 0;
} else {
classes.remove('collapsed');
classes.add('expanded');
expanded = 1;
}
onClick(expanded);
});
hExpander.className = 'expander';
const hButton = createButton(expanded);
const hType = createType(type);
hExpander.appendChild(hButton);
hExpander.appendChild(hType);
return hExpander;
};
const createKey = (key) => {
const hKey = document.createElement('div');
hKey.className = 'key';
hKey.innerText = key + ':';
return hKey;
};
const createLine = (key, value, expanded) => {
const hLine = document.createElement('div');
hLine.className = 'line';
const hKey = createKey(key);
const hEntry = renderEntry(value, expanded);
hLine.appendChild(hKey);
hLine.appendChild(hEntry);
return hLine;
};
const createTree = (object, expanded) => {
const hTree = document.createElement('div');
hTree.className = 'tree';
iterateEntries(object, (key, value) => {
const hLine = createLine(key, value, expanded - 1);
hTree.appendChild(hLine);
});
return hTree;
};
const renderValue = (value, clazz) => {
const hItem = document.createElement('div');
hItem.className = 'item value ' + clazz;
hItem.innerText = value;
return hItem;
};
const renderNull = () => {
return renderValue('null', 'nil');
};
const renderUndefined = () => {
return renderValue('undefined', 'nil');
};
const renderBoolean = (value) => {
const text = value.toString();
return renderValue(text, 'boolean');
};
const renderNumber = (value) => {
const text = value.toString();
return renderValue(text, 'number');
};
const renderString = (value) => {
const text = `"${value}"`;
return renderValue(text, 'string');
};
const renderError = (value) => {
const stack = value.stack;
if (stack) {
return renderValue(stack, 'text');
} else {
const text = (value.name || 'Error') + ': ' + (value.message || '<unknown>');
return renderValue(text, 'text');
}
};
const renderSymbol = (value) => {
const text = value.toString();
return renderValue(text, 'symbol');
};
const renderFunction = (value) => {
const text = 'function ' + (value.name || 'anonymous') + '() { /* ... */ }';
return renderValue(text, 'function');
};
const renderBigint = (value) => {
const text = value + 'n';
return renderValue(text, 'bigint');
};
const renderRegexp = (value) => {
const text = value.toString();
return renderValue(text, 'regexp');
};
const renderObject = (object, expanded, type) => {
let hTree = null;
const onClick = (expanded) => {
if (expanded) {
if (hTree == null) {
hTree = createTree(object, 0);
}
hObject.appendChild(hTree);
} else {
hObject.removeChild(hTree);
}
};
const hObject = document.createElement('div');
hObject.className = 'item object';
const hExpander = createExpander(type, expanded, onClick);
hObject.appendChild(hExpander);
if (expanded > 0) {
hTree = createTree(object, expanded);
hObject.appendChild(hTree);
}
return hObject;
};
const renderEntry = (entry, expanded = 10) => {
const type = getType(entry);
switch (type) {
case 'Null': return renderNull();
case 'Undefined': return renderUndefined();
case 'Boolean': return renderBoolean(entry);
case 'Number': return renderNumber(entry);
case 'String': return renderString(entry);
case 'Error': return renderError(entry);
case 'Symbol': return renderSymbol(entry);
case 'Function': return renderFunction(entry);
case 'BigInt': return renderBigint(entry);
case 'RegExp': return renderRegexp(entry);
case 'Array': return renderObject(entry, expanded, `Array(${entry.length})`);
default: return renderObject(entry, expanded, type);
}
};
// Usage example:
const object = {
parent: null,
priority: 5,
completed: true,
tasks: [
{id: 1n, type: 'lecture', toString: () => { }},
{id: 2n, type: 'class', toString: () => { }},
{id: 3n, type: 'work', toString: () => { }},
],
filters: [
/^lecture-/i,
/^class-/i,
/^work-/i
]
};
document.body.appendChild(renderEntry(object, 0)); // expansion disabled
// document.body.appendChild(renderEntry(object, 1)); // 1 level expanded
// document.body.appendChild(renderEntry(object, 2)); // 2 levels expanded
</script>
</body>
</html>