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:
xxxxxxxxxx
1
2
<html>
3
<head>
4
<style>
5
6
div.item {
7
margin: 5px 0 0 0;
8
border: 1px solid #e8e8e8;
9
}
10
11
div.item div.item {
12
margin: 0;
13
border-style: solid none none none;
14
}
15
16
div.expander {
17
display: flex;
18
cursor: pointer;
19
}
20
21
div.button {
22
flex: 0;
23
width: 0;
24
height: 0;
25
}
26
27
div.button.expanded {
28
margin: 6px 4.5px 0 1px;
29
border-left: 5px solid transparent;
30
border-right: 5px solid transparent;
31
border-top: 8px solid #3685d6;
32
}
33
34
div.button.collapsed {
35
margin: 4.5px 3.5px;
36
border-top: 5px solid transparent;
37
border-bottom: 5px solid transparent;
38
border-left: 8px solid #3685d6;
39
}
40
41
div.type {
42
padding: 0 0 2px 0;
43
flex: 1;
44
color: #39add0;
45
}
46
47
div.tree {
48
margin: 0 0 0 16px;
49
flex: 1 100%;
50
display: table;
51
}
52
53
div.line {
54
display: table-row;
55
}
56
57
div.key {
58
padding: 0 16px 0 0;
59
width: 0%;
60
min-height: 19px;
61
display: table-cell;
62
vertical-align: top;
63
color: #881280;
64
}
65
66
div.object {
67
padding: 0;
68
display: flex;
69
flex-direction: column;
70
}
71
72
div.value {
73
padding: 0 0 0 1px;
74
min-height: 19px;
75
}
76
77
div.value.nil {
78
color: #221199
79
}
80
81
div.value.boolean {
82
color: #221199
83
}
84
85
div.value.number {
86
color: #2771bb;
87
}
88
89
div.value.string {
90
color: #d03131;
91
}
92
93
div.value.symbol {
94
color: #c80042;
95
}
96
97
div.value.function {
98
color: #770088;
99
}
100
101
div.value.bigint {
102
color: #2771bb;
103
}
104
105
div.value.regexp {
106
color: #ff5500;
107
}
108
109
</style>
110
</head>
111
<body>
112
<script>
113
114
const TO_STRING = Object.prototype.toString;
115
116
const getType = (entry) => {
117
const text = TO_STRING.call(entry);
118
return text.slice(8, -1);
119
};
120
121
const iterateEntries = (entry, callback) => {
122
for (const key in entry) {
123
callback(key, entry[key]);
124
}
125
};
126
127
const createButton = (expanded) => {
128
const hButton = document.createElement('div');
129
hButton.className = 'button ' + (expanded > 0 ? 'expanded' : 'collapsed');
130
return hButton;
131
};
132
133
const createType = (type) => {
134
const hType = document.createElement('div');
135
hType.className = 'type';
136
hType.innerText = type;
137
return hType;
138
};
139
140
const createExpander = (type, expanded, onClick) => {
141
const hExpander = document.createElement('div');
142
hExpander.addEventListener('click', () => {
143
const classes = hButton.classList;
144
if (expanded > 0) {
145
classes.remove('expanded');
146
classes.add('collapsed');
147
expanded = 0;
148
} else {
149
classes.remove('collapsed');
150
classes.add('expanded');
151
expanded = 1;
152
}
153
onClick(expanded);
154
});
155
hExpander.className = 'expander';
156
const hButton = createButton(expanded);
157
const hType = createType(type);
158
hExpander.appendChild(hButton);
159
hExpander.appendChild(hType);
160
return hExpander;
161
};
162
163
const createKey = (key) => {
164
const hKey = document.createElement('div');
165
hKey.className = 'key';
166
hKey.innerText = key + ':';
167
return hKey;
168
};
169
170
const createLine = (key, value, expanded) => {
171
const hLine = document.createElement('div');
172
hLine.className = 'line';
173
const hKey = createKey(key);
174
const hEntry = renderEntry(value, expanded);
175
hLine.appendChild(hKey);
176
hLine.appendChild(hEntry);
177
return hLine;
178
};
179
180
const createTree = (object, expanded) => {
181
const hTree = document.createElement('div');
182
hTree.className = 'tree';
183
iterateEntries(object, (key, value) => {
184
const hLine = createLine(key, value, expanded - 1);
185
hTree.appendChild(hLine);
186
});
187
return hTree;
188
};
189
190
const renderValue = (value, clazz) => {
191
const hItem = document.createElement('div');
192
hItem.className = 'item value ' + clazz;
193
hItem.innerText = value;
194
return hItem;
195
};
196
197
const renderNull = () => {
198
return renderValue('null', 'nil');
199
};
200
201
const renderUndefined = () => {
202
return renderValue('undefined', 'nil');
203
};
204
205
const renderBoolean = (value) => {
206
const text = value.toString();
207
return renderValue(text, 'boolean');
208
};
209
210
const renderNumber = (value) => {
211
const text = value.toString();
212
return renderValue(text, 'number');
213
};
214
215
const renderString = (value) => {
216
const text = `"${value}"`;
217
return renderValue(text, 'string');
218
};
219
220
const renderError = (value) => {
221
const stack = value.stack;
222
if (stack) {
223
return renderValue(stack, 'text');
224
} else {
225
const text = (value.name || 'Error') + ': ' + (value.message || '<unknown>');
226
return renderValue(text, 'text');
227
}
228
};
229
230
const renderSymbol = (value) => {
231
const text = value.toString();
232
return renderValue(text, 'symbol');
233
};
234
235
const renderFunction = (value) => {
236
const text = 'function ' + (value.name || 'anonymous') + '() { /* ... */ }';
237
return renderValue(text, 'function');
238
};
239
240
const renderBigint = (value) => {
241
const text = value + 'n';
242
return renderValue(text, 'bigint');
243
};
244
245
const renderRegexp = (value) => {
246
const text = value.toString();
247
return renderValue(text, 'regexp');
248
};
249
250
const renderObject = (object, expanded, type) => {
251
let hTree = null;
252
const onClick = (expanded) => {
253
if (expanded) {
254
if (hTree == null) {
255
hTree = createTree(object, 0);
256
}
257
hObject.appendChild(hTree);
258
} else {
259
hObject.removeChild(hTree);
260
}
261
};
262
const hObject = document.createElement('div');
263
hObject.className = 'item object';
264
const hExpander = createExpander(type, expanded, onClick);
265
hObject.appendChild(hExpander);
266
if (expanded > 0) {
267
hTree = createTree(object, expanded);
268
hObject.appendChild(hTree);
269
}
270
return hObject;
271
};
272
273
const renderEntry = (entry, expanded = 10) => {
274
const type = getType(entry);
275
switch (type) {
276
case 'Null': return renderNull();
277
case 'Undefined': return renderUndefined();
278
case 'Boolean': return renderBoolean(entry);
279
case 'Number': return renderNumber(entry);
280
case 'String': return renderString(entry);
281
case 'Error': return renderError(entry);
282
case 'Symbol': return renderSymbol(entry);
283
case 'Function': return renderFunction(entry);
284
case 'BigInt': return renderBigint(entry);
285
case 'RegExp': return renderRegexp(entry);
286
case 'Array': return renderObject(entry, expanded, `Array(${entry.length})`);
287
default: return renderObject(entry, expanded, type);
288
}
289
};
290
291
292
// Usage example:
293
294
const object = {
295
parent: null,
296
priority: 5,
297
completed: true,
298
tasks: [
299
{id: 1n, type: 'lecture', toString: () => { }},
300
{id: 2n, type: 'class', toString: () => { }},
301
{id: 3n, type: 'work', toString: () => { }},
302
],
303
filters: [
304
/^lecture-/i,
305
/^class-/i,
306
/^work-/i
307
]
308
};
309
310
document.body.appendChild(renderEntry(object, 0)); // expansion disabled
311
// document.body.appendChild(renderEntry(object, 1)); // 1 level expanded
312
// document.body.appendChild(renderEntry(object, 2)); // 2 levels expanded
313
314
</script>
315
</body>
316
</html>