70

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!

Hairgami_Master
  • 5,429
  • 10
  • 45
  • 66
  • I just answered this in a pretty generic way over on a different question: http://stackoverflow.com/questions/14430655/recursion-in-angular-directives/29736154#29736154 – tilgovi Apr 19 '15 at 22:02

5 Answers5

122

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.

jpmorin
  • 6,008
  • 2
  • 28
  • 39
  • 2
    Many thanks- I think this is the the technique to use. Cheers! – Hairgami_Master Mar 30 '13 at 17:29
  • what if your data structure doesn't repeat so you can't use a repeater but you have an indeterminite number of nested values? See: http://stackoverflow.com/questions/25044253/how-can-you-navigate-a-tree-in-reverse-order-with-angularjs – Randyaa Jul 30 '14 at 20:59
  • @jpmorin: How I'd treat [this case](http://jsbin.com/sixigokuno/2/edit?html,js,output)? You can reply on [code review site](http://codereview.stackexchange.com/questions/75357/parse-recursively-a-json-object-with-angularjs) – el.severo Dec 31 '14 at 15:42
  • This solution is great but keep the performance in mind. We had serious performanceproblem with this solution. Read Ben Nadels article for more information: http://www.bennadel.com/blog/2738-using-ngrepeat-with-nginclude-hurts-performance-in-angularjs.htm – Samuel Kupferschmid Dec 17 '15 at 07:13
  • Can I use `track by` in this solution? I need to add the keyboard navigation and searching each element by its ID does not seem as a good solution. – owczarek Sep 07 '16 at 14:27
  • I second Samuel's recommendation. We had a 5 level nested loop with this that was taking 4+ secs to load with this approach. When switched to a repeated static HTML is was ~1 sec. – Kevinleary.net Apr 20 '17 at 14:43
  • Don't forget the single quotes around your ng-include template, I did. Took me awhile to solve, Thanks to: https://stackoverflow.com/questions/21149653/ng-include-not-working-with-script-type-text-ng-template – olahell Jun 21 '17 at 08:20
  • There is always a performance trade off to flexibility . This approach resolves for n recursion cases. Great approach. – ozkary Jun 24 '17 at 05:16
  • Is there a way to identify the depth of the recursion within the template? – Ted Scheckler Aug 24 '17 at 13:36
  • @FishBulbX you could try to pass down the current depth of your node http://jsfiddle.net/uXbn6/4540/ – jpmorin Aug 28 '17 at 21:40
  • Note that the ` – David Sep 14 '17 at 15:21
  • Could you resolve my problem guys..https://stackoverflow.com/questions/52039222/how-to-implement-hierarchical-multilevel-datatable-in-javascript?noredirect=1#comment91031196_52039222 – Varun Sharma Aug 28 '18 at 11:11
10

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

Dexter Legaspi
  • 3,192
  • 1
  • 35
  • 26
1

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.

Ketan
  • 5,861
  • 3
  • 32
  • 39
1

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>
Jens
  • 3,353
  • 1
  • 23
  • 27
0

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.

Arsen Khachaturyan
  • 7,904
  • 4
  • 42
  • 42