How can I make recursive templates in AngularJS when using nested objects?
RecursionAngularjsRecursion 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.