How can I make recursive templates in AngularJS when using nested objects?

RecursionAngularjs

Recursion Problem Overview


I'm trying to build a form dynamically from a JSON object, which contains nested groups of form elements:

  $scope.formData = [  {label:'First Name', type:'text', required:'true'},  {label:'Last Name', type:'text', required:'true'},  {label:'Coffee Preference', type:'dropdown', options: ["HiTest", "Dunkin", "Decaf"]},
  {label: 'Address', type:'group', "Fields":[
      {label:'Street1', type:'text', required:'true'},
      {label:'Street2', type:'text', required:'true'},
      {label:'State', type:'dropdown',  options: ["California", "New York", "Florida"]}
    ]},
  ];

I've been using ng-switch blocks, but it becomes untenable with nested items, like in the Address object above.

Here's the fiddle: http://jsfiddle.net/hairgamiMaster/dZ4Rg/

Any ideas on how to best approach this nested problem? Many thanks!

Recursion Solutions


Solution 1 - Recursion

I think that this could help you. It is from an answer I found on a Google Group about recursive elements in a tree.

The suggestion is from Brendan Owen: http://jsfiddle.net/brendanowen/uXbn6/8/

<script type="text/ng-template" id="field_renderer.html">
    {{data.label}}
    <ul>
        <li ng-repeat="field in data.fields" ng-include="'field_renderer.html'"></li>
    </ul>
</script>

<ul ng-controller="NestedFormCtrl">
    <li ng-repeat="field in formData" ng-include="'field_renderer.html'"></li>
</ul>

The proposed solution is about using a template that uses the ng-include directive to call itself if the current element has children.

In your case, I would try to create a template with the ng-switch directive (one case per type of label like you did) and add the ng-include at the end if there are any child labels.

Solution 2 - Recursion

Combining what @jpmorin and @Ketan suggested (slight change on @jpmorin's answer since it doesn't actually work as is)...there's an ng-if to prevent "leaf children" from generating unnecessary ng-repeat directives:

<script type="text/ng-template" id="field_renderer.html">
  {{field.label}}
  <ul ng-if="field.Fields">
      <li ng-repeat="field in field.Fields" 
         ng-include="'field_renderer.html'">
      </li>
  </ul>
</script>
<ul>
  <li ng-repeat="field in formData" ng-include="'field_renderer.html'"></li>
</ul>

here's the working version in Plunker

Solution 3 - Recursion

Might consider using ng-switch to check availability of Fields property. If so, then use a different template for that condition. This template would have an ng-repeat on the Fields array.

Solution 4 - Recursion

I know this is an old question, but for others who might come by here though a search, I though I would leave a solution that to me is somewhat more neat.

It builds on the same idea, but rather than having to store a template inside the template cache etc. I wished for a more "clean" solution, so I ended up creating https://github.com/dotJEM/angular-tree

It's fairly simple to use:

<ul dx-start-with="rootNode">
  <li ng-repeat="node in $dxPrior.nodes">
    {{ node.name }}
    <ul dx-connect="node"/>
  </li>
</ul>

Since the directive uses transclusion instead of compile (as of the latest version), this should perform better than the ng-include example.

Example based on the Data here:

angular
  .module('demo', ['dotjem.angular.tree'])
  .controller('AppController', function($window) {

    this.formData = [
      { label: 'First Name', type: 'text', required: 'true' },
      { label: 'Last Name',  type: 'text', required: 'true' }, 
      { label: 'Coffee Preference', type: 'dropdown', options: ["HiTest", "Dunkin", "Decaf"] }, 
      { label: 'Address', type: 'group',
      "Fields": [{
        label: 'Street1', type: 'text', required: 'true' }, {
        label: 'Street2', type: 'text', required: 'true' }, {
        label: 'State',   type: 'dropdown', options: ["California", "New York", "Florida"]
      }]
    }, ];

    this.addNode = function(parent) {
      var name = $window.prompt("Node name: ", "node name here");
      parent.children = parent.children || [];
      parent.children.push({
        name: name
      });
    }

    this.removeNode = function(parent, child) {
      var index = parent.children.indexOf(child);
      if (index > -1) {
        parent.children.splice(index, 1);
      }
    }

  });

<div ng-app="demo" ng-controller="AppController as app">

   <form>
        <ul class="unstyled" dx-start-with="app.formData" >
            <li ng-repeat="field in $dxPrior" data-ng-switch on="field.type">
                <div data-ng-switch-when="text">
                    <label>{{field.label}}</label>
                    <input type="text"/>
                </div>
                <div data-ng-switch-when="dropdown">
                    <label>{{field.label}}</label>
                    <select>
                        <option ng-repeat="option in field.options" value="{{option}}">{{option}}</option>
                    </select>
                </div>
                <div data-ng-switch-when="group" class="well">
                    <h2>{{field.label}}</h2>
                    <ul class="unstyled" dx-connect="field.Fields" />
                </div>   
            </li>
        </ul>
            <input class="btn-primary" type="submit" value="Submit"/>
    </form>
  
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
  <script src="https://rawgit.com/dotJEM/angular-tree-bower/master/dotjem-angular-tree.min.js"></script>

</div>

Solution 5 - Recursion

I just want to extend jpmorin post in case of a property based structure.

JSON:

{
  "id": 203,
  "question_text_id": 1,
  "yes": {
    "question_text_id": 25,
    "yes": {
      "question_text_id": 26
    }
  },
  "no": {
    "question_text_id": 4
  }
}

As you can see json object here doesn't contain array structure.

HTML

<div>
    <script type="text/ng-template" id="tree_item_renderer.html">
        <span>{{key}} {{value}}</span>
        <ul>
            <li ng-repeat="(key,value) in value.yes" ng-include="'tree_item_renderer.html'"></li>
            <li ng-repeat="(key,value) in value.no" ng-include="'tree_item_renderer.html'"></li>
        </ul>
    </script>

    <ul>
        <li ng-repeat="(key,value) in symptomeItems" ng-include="'tree_item_renderer.html'"></li>
    </ul>
</div>

With this case, you can iterate over it.

Angular documentation for ng-repeat over properties here,

and some row implementation can be found here.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionHairgami_MasterView Question on Stackoverflow
Solution 1 - RecursionjpmorinView Answer on Stackoverflow
Solution 2 - RecursionDexter LegaspiView Answer on Stackoverflow
Solution 3 - RecursionKetanView Answer on Stackoverflow
Solution 4 - RecursionJensView Answer on Stackoverflow
Solution 5 - RecursionArsen KhachaturyanView Answer on Stackoverflow