Creating An Interactive JSON Explorer Using CSS Grid In Angular 9.0.0-next.14
At work, I spend a lot of time combing through Logs. Which means, I spend a lot of time looking at JSON (JavaScript Object Notation) payloads. Some of these payloads are rather large, which is why I do things like create Loggly bookmarklets for better Log rendering. Partly as a fun Code Kata and partly because I think I'll actually use this, I created JSON Explorer using CSS Grid and Angular 9.0.0-next.14. This JavaScript application parses JSON into an interactive data structure that, hopefully, makes it easier for me to examine the JSON content in my logs.
Run this demo in my JSON Explorer project on GitHub.
View this code in my JSON Explorer project on GitHub.
The implementation of this Angular app is mostly straightforward. It takes a JSON string (provided by the user), parses said JSON string into a native JavaScript data structure, and then renders that data structure in the browser. It's the rendering of the data structure that's interesting. I'm using a recursive Angular component, <json-node>
, that outputs the data as a series of nested CSS Grid layouts.
This JsonNodeComponent
has a single input, [value]
, that renders the passed-in data-structure. For simple values - strings, numbers, booleans, and null - it renders the value and then completes. But, for complex values - Objects and Arrays - it renders the value using a recursive call that renders <json-node>
for each one of its entries (ie, object keys and array indices).
To see this in action, let's look at the HTML template for the JsonNodeComponent
class. As you look at this code, keep in mind that the rendering of parts of the data tree can be toggled on-and-off, which is why there are many [ngIf]
directives:
<div
class="payload"
[ngSwitch]="valueType">
<ng-template ngSwitchCase="Null">
<div
(click)="toggle()"
class="label is-null"
[class.is-collapsed]="isCollapsed">
Null
</div>
<ng-template [ngIf]="( ! isCollapsed )">
<div class="value is-null">
null
</div>
</ng-template>
</ng-template>
<ng-template ngSwitchCase="String">
<div
(click)="toggle()"
class="label is-string"
[class.is-collapsed]="isCollapsed">
String
</div>
<ng-template [ngIf]="( ! isCollapsed )">
<div class="value is-string">
<a (click)="parseString( $event )">
{{ value }}
</a>
</div>
</ng-template>
</ng-template>
<ng-template ngSwitchCase="Number">
<div
(click)="toggle()"
class="label is-number"
[class.is-collapsed]="isCollapsed">
Number
</div>
<ng-template [ngIf]="( ! isCollapsed )">
<div class="value is-number">
{{ value }}
</div>
</ng-template>
</ng-template>
<ng-template ngSwitchCase="Boolean">
<div
(click)="toggle()"
class="label is-boolean"
[class.is-collapsed]="isCollapsed">
Boolean
</div>
<ng-template [ngIf]="( ! isCollapsed )">
<div class="value is-boolean">
{{ value }}
</div>
</ng-template>
</ng-template>
<ng-template ngSwitchCase="Array">
<div
(click)="toggle()"
class="header is-array"
[class.is-collapsed]="isCollapsed">
<div class="type">
Array
</div>
<div class="entry-count">
Entries: {{ entryCount }}
</div>
</div>
<ng-template [ngIf]="( ! isCollapsed )">
<ng-template ngFor let-subvalue let-index="index" [ngForOf]="value">
<div
(click)="toggle( index )"
class="label is-array"
[class.is-collapsed]="collapsedEntries[ index ]">
{{ index }}
</div>
<ng-template [ngIf]="( ! collapsedEntries[ index ] )">
<div class="value is-array">
<json-node [value]="subvalue"></json-node>
</div>
</ng-template>
</ng-template>
</ng-template>
</ng-template>
<ng-template ngSwitchCase="Object">
<div
(click)="toggle()"
class="header is-object"
[class.is-collapsed]="isCollapsed">
<div class="type">
Object
</div>
<div class="entry-count">
Entries: {{ entryCount }}
</div>
</div>
<ng-template [ngIf]="( ! isCollapsed )">
<ng-template ngFor let-subvalue [ngForOf]="value | keyvalue">
<div
(click)="toggle( subvalue.key )"
class="label is-object"
[class.is-collapsed]="collapsedEntries[ subvalue.key ]">
{{ subvalue.key }}
</div>
<ng-template [ngIf]="( ! collapsedEntries[ subvalue.key ] )">
<div class="value is-object">
<json-node [value]="subvalue.value"></json-node>
</div>
</ng-template>
</ng-template>
</ng-template>
</ng-template>
</div>
Notice that the last two cases in the ngSwitch
directive, for Array
and Object
, each iterate over the values entries, making a recursive call to the JsonNodeComponent
. This allows the Angular app to render a data structure of arbitrary depth and complexity.
NOTE: This rendering cannot handle circular references. However, since the data being piped into this component is generated from a
JSON.parse()
call, we know that no circular references will ever exist.
As you can see in the HTML template, each data-point is rendered with a Label and a Value. These two elements are implemented as 2 columns in a CSS Grid layout. CSS Grid makes this super easy, especially considering the fact that I can collapse the Value element based on user interactions. Since CSS Grid doesn't allow elements to float up into different rows (the way CSS Flexbox may in this particular context), I can collapse entire parts of the rendered data structure without altering the layout.
Another thing that I wanted to try and build into this JSON Explorer app was the ability to share a JSON payload with your teammate. To do this, I am attempting to Base64-encode the JSON payload and persist it to the URL. I am not sure how long this URL can get before the browser starts throwing errors. But, at least for smaller JSON payloads, this approach seems to work well.
Altogether, when I run this Angular application using the demo data, I get the following interactive data structure:
This started as a fun little Angular Code Kata; but, I think this JSON Explorer actually became a tool that I will be using. One thing in particular that I like about this is the ability to parse nested JSON payloads on-the-fly (as you can see in the GIF above) - this will be really helpful when looking at certain Log entries.
Want to use code from this post? Check out the license.
Reader Comments
I thought this was a:
When I saw the image on Twitter!
But, seriously, this is a great exploration of recursion.
I used to use something called:
To create collapsible tree like structures. It is part of Google Material's API:
https://material.angular.io/components/tree/overview
I used it, primarily, to make nested lists of checkboxes, but you could easily adapt it to make an object explorer or file system structure.
@Charles,
Oh, rest assured that this was heavily influenced by the majesty of
cfdump
/dump()
:D That's still one of the best debugging tools of all time.I finally tried the Angular Material CDK! I used the drag-n-drop for my Breadboarding Proof-of-Concept. It seemed pretty cool. I really need to try the CDK a bit more.
Material & CDK are super awesome.
I used to think their UI was too clinical, but now I just love its productivity benefits.
You can create super professional interfaces in minutes. And these things just work out of the box. Mind you, with Google's engineering team behind it, it's no wonder this stuff is so robust. And the best thing, is that it is pretty much integrated into Angular.
Yes. The Drag & Drop component is superb, as is the Mat-tree, CDK Stepper and Material Modals!
I even use Material Lite, now, which is Angular Material's Vanilla JS sibling.