Thursday, 10 January 2013

AngularJS and recursive template



I love JavaScript. It's like Lego, you can build anything really fast. And one of the reason I like it also a disadvantage: it's super flexible. The proliferation of new JS libraries are all using this behavior of the language. This time I wanted to try what AngularJS is about.


You can read the official website for a precise definition - but for me it's a super clever data binding and structuring tool. The binding engine can sync the data between UI and JS variables. The structuring layer can help you to build up your complex ui. I've got a question the other day: What would you use AngularJS for? Now I'm kinda sure about it: it's for webapps where you need mostly a data-reflective dynamic UI where reaction time matters.

There are great videos and tutorials online, I'm not gonna cover any of them. Instead I explain my problem I wanted to solve with the library.

I wanted to make a mindmap - where you can add nodes, edit them - and at the end you just got an exportable format (JSON or Freemind XML). For the map I needed recursive rendering to represent the hierarchy of the tree. So I've checked the reference. Among many useful (but less relevant features) I've found ngInclude. My only problem I didn't find any way to transfer variables. On the ui you can easily create iterations but couldn't crack my head through the ngController and other parts.

Eventually in a presentation they mentioned how scope is handled in AngluarJS. I was looking for some solutions on Google (idea no. 1 and idea no. 2) but to be honest I didn't want to write a new directive or module for that. It just smelled. Then I realized I could use a little trick with the scope :) Here you are the how-to of the damn-simple tree recursive template demo:

First we need the HTML template to hold the content:


(my sincere apology about image source code - but Blogspot is incapable of any kind of source code embedding - I'm seriosly pissed off)

You can see we load the minimized AngularJS library from Google CDN, our custom script and the stylesheet. The only strange thing is ng-app in the HTML tag. That tells AngularJS to process the tagged DOM part - here the whole document.
Let's add the container to the body that holds the generated output:


You can see 2 things here. ng-controller="MindMap" and ng-include. ng-controller defines the controller class for that particular DOM subtree. I'll talk about ng-include soon.
We define the ng-controller in our mindmap.js:


It looks like a very simple function. One argument we will is use the current scope of the call. This will be instantiated and attached to that DOM element. Now we can host the variable that will contain the tree structure - let's put it into the controller:


You can see it's added to the scope element - this way we can refer it through the current scope that lives in the DOM also. The data structure will be very simple, just an example:


I think you have the idea. Now let me explain the way we print the tree out. We would like to use a recursive rendering where I print the title and all children. Very simple. Children will print the same so there you have the recursion. To make it work we need a template that we can reuse: that is the referred node.html that we grab with ng-include. When we include the template first we need to print out the very first node's title, so it looks like this:


The double curly brackets are the expression executors / printers. $parent refers to the caller - which is our main HTML file with the scope where we defined our controller. Remember that there we added $scope.tree - which has the top level item. Okay - that will print out the very first title. Almost there :) Now comes the trick :) We need to iterate through all the children and make sure we can use the very same template to be delegated. And here comes the magic - we name the iterator to tree :)


ng-repeat will process the loop and puts the current item into tree - which is available through the $parent scope - which is awesome because we use that already. And we can then ng-include the node.html again. Now we have the tree listed just fine :)

But let's not stop here. Our tree is rather a leaf than a tree. How it will be a huge structure? Let's add dynamic data injection by adding a small add form for each leaf:


The form is fairly simple. We added the ng-submit handler that will receive the submit action. It has 2 parameters. The $parent.tree which contains always the $scope's subtree, and node_title. This one is interesting. Node title is a model element. Look at the input, it defines an ng-model. This maps the variable to node_title. We declare the submit handler in the main controller:


As with the variable - we also attach functions to the $scope. And simple as it is - we just add the new child Object to the subtree array. AngularJS will do all the binding actions and UI recreation on-demand :) It's freakin' awesome. And that's all :) I added a little CSS so it's not gonna be super ugly, just ugly:


It makes sure that the form is only visible when we're on the node.

You can find the complete source code on GitHub: https://github.com/itarato/AngularJSTree . Clone and play with it. Or you can visit the live demo: http://itarato.com/blog/AngularJSTree/

---

There are couple of things I've not yet managed to solve.

Deletion

I can create a delete button next to each element - but somehow it's tricky to find the right element to delete, even if I have the proper subtree object.

Form reset

I couldn't refer back to the input field and delete it's value. Mapping didn't worked that way or I missed something, I don't know yet.

If you could help me out with some tips I'd really appreciate it.

Peter

3 comments:

  1. Thank Peter for this helpful article.
    btw if you are yet interested to find a solution for the two last problems, you could take a look on the fork here: https://github.com/zerikv/AngularJSTree

    ReplyDelete
  2. cannot reach live demo

    ReplyDelete
    Replies
    1. Thanks for the check, my bad, updated the link ;)

      Delete