Recursively Creating a Treeview in Angular
Recursion in the Template
My first thought was to have the template of the directive to reference itself. However, this on its own will create an infinite loop. I needed to get my hands dirty and dig into how Angular compiles and renders directives. A directive in angular has largely four methods that gets fired; compile, controller, pre-link and post link. There are excellent posts on this topic [jvandemo.com, undefinednull.com], and the gist of it is that:
The compile function runs first, and it has access to the raw template HTML (i.e., angular expressions not evaluated yet) and has no scope attached to it. This is where we can edit our template if needed. The controller function runs next with its own $scope attached, but nested child directives are yet connected. Then the pre-link function runs while linking the child directives with its parents, and finally the post-link function runs bottom-up. These link function have access to the scope from the controller and also the instance HTML that will be compiled and injected.
In our code below, we remove the HTML content from our template element in the compile function, thereby breaking the recursion loop. Then we manually compile the content in our post-link function.
You can view the solution in action here.
The solution above is quite composable, especially if you abstract the recursion into a service. However, it has serious shortcomings when it comes to performance. It creates too many two-way data bindings in too many scopes. Since we need to give each child node its own set of data, our directive needs an isolated scope, thus creating a separate scope for every node. Each node also has ng-repeat, which also creates separate scopes for each loop. With a dataset with a few hundred nodes, it took at least a full second or two for the entire tree to render.
Recursively Create Template
In order to optimize for performance, I went back to the basics. In our code below, we build a HTML string recursively then compile it and attach to the DOM. (htmlString is a string inside an array so that manipulation of it will persist removing the need to reassign every time)
The solution above is also not without shortcomings. By building the entire HTML string before compilation, we acquire performance but lose two-way binding with the data object. Re-rendering the component after a change in the data object requires a few hacks.
We attach ng-if to the directive and briefly remove the directive by setting the expression inside ng-if to false. We also need a $timeout between switching the expression from false to true again.
This doesn’t sit well with me as it feels quite hacky. However, if the task is to render a treeview with a couple hundred nodes, I would assume that the need for automatic re-rendering of the directive would be low.
Also, achieving data binding might be harder this way, but listening to each node is still a breeze. Since the HTML string we construct will go through Angular’s $compile method, we can simply add any other directives inside the string, including built-in directives such as ng-click.
You can view the solution (with event listeners) in action here.