Breaking the rules: Rendering JSON, with only a recursive AngularJS template
Being the "business guy" at my startup means that when I code, I do it for fun. And being a former academic, means that when I want to have some fun coding, it turns into research.
One of the things I love about AngularJS is how little code it takes to do cool things if you're doing it right, and how it extends the declarative nature of HTML for the webapp era. So, when I do fun coding research, I try to see how far I can go with it. One of my favourite recent creations, basically contains no code at all. But "basically" is a bit different than "no code at all", so there's still room to improve.
I've always thought that JSON should be super-easy to visualise, so I've given it a go with Angular. But not just any Angular approach would do. I decided to base it on some previous work I'd done with recursive templates. In the process of getting that to work and producing a very elegant result (if I may say so myself), I had to break or bend several "best practices" of AngularJS. My view is that these breaks are justified, the kind of rule-breaking that is permitted if you can clearly argue your thought process. Let's start with the code, and we'll get to the arguing afterwards. 80 lines of Angularized HTML and JavaScript that will display any JSON file you point them to.
Mouse over the example below for a cool little effect. You can see the code by clicking on the "Code" button on the top right of the window below.
So how is this breaking the rules? Let me count the ways:
1. Disabling the digest limit
Notice the little line that writes
$rootScopeProvider.digestTtl(Infinity);
That line basically allows us to render a JSON file as deep as we like. The catch is that we need to suspend a limit that was put there for a reason. AngularJS has a digest limit because infinite recursion happens really easily when you do things wrong. But we know that this code doesn't infinitely recurse. It just recurses deeper than the 10-digest limit, with the example JSON file I'm using. And it could go deeper if given a deeper nested file.
Isn't there a way to work around this? Yes there is. The way is to create a specialised directive that does the job of the template. But that would be equivalent of respecting the rules for their own sake. And when we're doing fun coding for research purposes, respecting the rules is the one thing we don't do, especially for their own sake. Nevermind the elegance tax that would entail.
2. Value in value. What?!
If you look at lines 68 and 75, you'll see a somewhat odd formulation:
ng-repeat="(key, value) in value"
This is outrageous! Any programming language would choke at this obvious circular reference. Or is it?
Angular has an interesting way of parsing the 'in' operator, which we exploit to make template recursion work. The left side of the operator, in our case (key, value)
gets evaluated in the context of the child scope that is created for each iteration. On the other hand, the right side, in our case value
, is understood to be in the parent scope. As such, not only do the two instances not clash, they actually enable the recursion, since one scope's child is another child's parent.
3. ng-init
The sinful lines 68 and 75 also read:
ng-init="groupValue = $parent.value"
ng-init is not really supposed to be used, except to alias things in combination with an ng-repeat. Since these two lines do indeed also have an ng-repeat, this isn't such a terrible violation. But it is cheeky. What I'm doing there is to pass the child an easily findable reference to its parent. You might ask why I'm doing this, since I could just use $parent? Well, we actually create 3 layers of inheritance for every level of nesting in a JSON file. This means that to reach the actual parent from where I need it, I have to do something like $parent.$parent.$parent
. And this isn't just ugly. It's fragile, as it depends on the levels of inheritance being exactly three. By creating the groupValue reference, I'm able to reach the greatgrandparent with a simple name, even if I'm unsure how many generations actualy intervene.
4. Writing on the parent
But why do I want to reach the greatgrandparent you ask? Isn't that in itself a sign of trouble? Well, I'm glad you asked! As you may know, Angular uses vanilla JavaScript objects and protptypal inheritance for its scope hierarchy. This is super cool and super easy to set up, until you try to write on a property your current scope inherited from a parent scope. When you do, the property becomes detached and receives no further updates should it change in the parent. Further, the changes you make in the child don't get reflected up the chain. The normal remedy (hack-around) is to not store direct values in scope properties, but to store object references and put the values there. But this doesn't work with our value in value hack, so here we are. Writing to the parent means there is no more breaking of the inheritance hierarchy,
What this in turn means is that not only do we render our JSON file, we can actually edit its values, and those values will be reflected in the property that holds the deserialised JSON file we used to begin with. We can then send that back wherever we got it from, and it will contain all the edits. Not bad for an 80-line snippet, eh?
5. Gratuitous rule-breaking: window.s
Since we've reached proper first-world anarchy levels of rule-breaking, nothing's stopping us now. I've included one of my all-time favourite hacks:
window.sc = $scope
Putting this in the root controller gives us easy access to the scope through the console. Nothing better when you quickly want to see what's on the scope for debugging purposes. Yeah, you could access it using a much longer manoeuver, but why bother? In our case, you could type sc.value
to quickly verify that indeed the two-way binding is preserved despite the multiple levels of inheritance.
If nothing else, I hope the above piece of code will help you understand more about the inner workings of AngularJS and how/why it works like it works. Do I advise you use these tricks in production? Some yes, most no. If anything, it will confuse everyone else and you'll need to have a long conversation about it.
Hope you had fun, hit me with your comments!