Thiago Melo
Thiago Melo

Reputation: 1177

How to do recursion in a directive?

We have this directive which receives a dictionary.

We were thinking that a directivy would be able of calling itself recursively, but it is not working.

Our current data structure is:

var cubicApp = angular.module('CubicApp', ['autocomplete']).controller('PreviewForecastCtrl', function ($scope, $http) {
    $scope.name = 'Thiago';

    $scope.performance = {
        headers: [{ name: '', colspan: 1 }, { name: 'Week result', colspan: 6 }, { name: '2017', colspan: 13 }, { name: 'General', colspan: 4 }],
        subheaders: [
            { name: 'Product', colspan: 1 },
            { name: '29-May', colspan: 1 },
            { name: '30-May', colspan: 1 },
            { name: '31-May', colspan: 1 },
            { name: '01-Jun', colspan: 1 },
            { name: '02-Jun', colspan: 1 },
            { name: 'Total', colspan: 1 },
            { name: 'Jan', colspan: 1 },
            { name: 'Feb', colspan: 1 },
            { name: 'Mar', colspan: 1 },
            { name: 'Apr', colspan: 1 },
            { name: 'May', colspan: 1 },
            { name: 'Jun', colspan: 1 },
            { name: 'Jul', colspan: 1 },
            { name: 'Aug', colspan: 1 },
            { name: 'Sep', colspan: 1 },
            { name: 'Oct', colspan: 1 },
            { name: 'Nov', colspan: 1 },
            { name: 'Dec', colspan: 1 },
            { name: 'Year', colspan: 1 },
            { name: '1M', colspan: 1 },
            { name: '6M', colspan: 1 },
            { name: '12M', colspan: 1 },
            { name: 'Accum.', colspan: 1 }
        ],
        books: [
            {
                name: 'Renda Variável',
                css: 'primary',
                totals: Array.from({length: 23}, () => Math.random()),
                books: [
                    {
                        name: 'Ibovespa Ativo',
                        css: 'active',
                        totals: Array.from({ length: 23 }, () => Math.random()),
                        books: [
                            {
                                name: 'BOVA11 (Equity)',
                                css: '',
                                totals: Array.from({ length: 23 }, () => Math.random()),
                                books: []
                            },
                            {
                                name: 'Cash BRL (Cash)',
                                css: '',
                                totals: Array.from({ length: 23 }, () => Math.random()),
                                books: []
                            }
                        ]
                    }
                ]
            }
        ]
    };

    $scope.loadInfo = function () {

    };
});

The directives are:

cubicApp.directive('drillDownTable', function ($document) {
    return {
        restrict: 'E',
        transclude: true,
        scope: {
            data: '=',
        },
        templateUrl: '../../Scripts/app/templates/drill-down-table.html'
    };
}).directive('inner', function ($document, $compile) {
    return {
        restrict: 'A',
        transclude: true,
        scope: {
            inner: '=',
        },
        templateUrl: '../../Scripts/app/templates/drill-down-inner-table.html'
    };
});;

drill-down-table.html:

<table class="table table-hover table-bordered jambo_table">
    <thead>
        <tr>
            <th ng-repeat="h in data.headers" colspan="{{h.colspan}}" style="text-align:center !important">{{h.name}}</th>
        </tr>
        <tr>
            <th ng-repeat="h in data.subheaders" colspan="{{h.colspan}}" style="text-align:center !important">{{h.name}}</th>
        </tr>
    </thead>
    <tbody>
        <tr ng-repeat="book in data.books" inner="book"></tr>
    </tbody>
</table>

drill-down-inner-table.html:

<tr>
    <td>{{inner.name}}</td>
    <td ng-repeat="t in inner.totals">{{t | number : 2}}</td>
</tr>
<tr ng-repeat="book in inner.books" inner="book"></tr>

Upvotes: 1

Views: 118

Answers (1)

Jesus is Lord
Jesus is Lord

Reputation: 15399

I think restricting the directive type to A and putting it on the same element as the ng-repeat is causing the issue. When I made the directive type E, and changed the templates it worked (see Plunker below).

Of course moving the directive to an element inside the ng-repeat causes problems with keeping everything in the table aligned because you're trying to recursively traverse an object and output <tr />'s.

My recommendation would be to write a recursive function that converts the recursive data.books structure into an array that will nicely play with a single ng-repeat. You might be able to get your original approach to work, but I suspect converting it to an array will take less time than creating a recursive directive that works correctly. Main downside is keeping the recursive structure up-to-date if data.books changes. To address this, my recommendation would be to figure out ahead of time what all can cause data.books to change (so you don't have to $watch it) and call the recursive function every time it's changed.

Another alternative might be to use some CSS to style what I have below into something that visually is what you want (give proper widths to all the columns, worry about borders, etc.).

http://plnkr.co/edit/pxCWxyfGtHxfAZG5FiVJ?p=preview

HTML:

<!DOCTYPE html>
<html ng-app="CubicApp">

  <head>
    <script data-require="[email protected]" data-semver="1.6.2" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body ng-controller="PreviewForecastCtrl">
    <drill-down-table data="performance"></drill-down-table>
  </body>

</html>

Main:

angular.module('autocomplete', []);

var cubicApp = angular.module('CubicApp', ['autocomplete']).controller('PreviewForecastCtrl', function ($scope, $http) {
    $scope.name = 'Thiago';

    $scope.performance = {
        headers: [{ name: '', colspan: 1 }, { name: 'Week result', colspan: 6 }, { name: '2017', colspan: 13 }, { name: 'General', colspan: 4 }],
        subheaders: [
            { name: 'Product', colspan: 1 },
            { name: '29-May', colspan: 1 },
            { name: '30-May', colspan: 1 },
            { name: '31-May', colspan: 1 },
            { name: '01-Jun', colspan: 1 },
            { name: '02-Jun', colspan: 1 },
            { name: 'Total', colspan: 1 },
            { name: 'Jan', colspan: 1 },
            { name: 'Feb', colspan: 1 },
            { name: 'Mar', colspan: 1 },
            { name: 'Apr', colspan: 1 },
            { name: 'May', colspan: 1 },
            { name: 'Jun', colspan: 1 },
            { name: 'Jul', colspan: 1 },
            { name: 'Aug', colspan: 1 },
            { name: 'Sep', colspan: 1 },
            { name: 'Oct', colspan: 1 },
            { name: 'Nov', colspan: 1 },
            { name: 'Dec', colspan: 1 },
            { name: 'Year', colspan: 1 },
            { name: '1M', colspan: 1 },
            { name: '6M', colspan: 1 },
            { name: '12M', colspan: 1 },
            { name: 'Accum.', colspan: 1 }
        ],
        books: [
            {
                name: 'Renda Variável',
                css: 'primary',
                totals: Array.from({length: 23}, () => Math.random()),
                books: [
                    {
                        name: 'Ibovespa Ativo',
                        css: 'active',
                        totals: Array.from({ length: 23 }, () => Math.random()),
                        books: [
                            {
                                name: 'BOVA11 (Equity)',
                                css: '',
                                totals: Array.from({ length: 23 }, () => Math.random()),
                                books: []
                            },
                            {
                                name: 'Cash BRL (Cash)',
                                css: '',
                                totals: Array.from({ length: 23 }, () => Math.random()),
                                books: []
                            }
                        ]
                    }
                ]
            }
        ]
    };

    $scope.loadInfo = function () {

    };
});

Directives:

cubicApp.directive('drillDownTable', function ($document) {
    return {
        restrict: 'E',
        transclude: true,
        scope: {
            data: '=',
        },
        templateUrl: 'drill-down-table.html'
    };
}).directive('inner', function ($document, $compile) {
    return {
        restrict: 'E',
        transclude: true,
        scope: {
            inner: '=',
        },
        templateUrl: 'drill-down-inner-table.html'
    };
});;

drill-down-table.html:

<table class="table table-hover table-bordered jambo_table">
    <thead>
        <tr>
            <th ng-repeat="h in data.headers" colspan="{{h.colspan}}" style="text-align:center !important">{{h.name}}</th>
        </tr>
        <tr>
            <th ng-repeat="h in data.subheaders" colspan="{{h.colspan}}" style="text-align:center !important">{{h.name}}</th>
        </tr>
    </thead>
    <tbody>
        <tr ng-repeat="book in data.books">
            <td colspan="{{data.subheaders.length}}">
                <inner inner="book"></inner>
            </td>
        </tr>
    </tbody>
</table>

drill-down-inner-table.html:

<table>
    <tbody>
        <tr>
            <td colspan="{{ inner.totals.length }}">{{inner.name}}</td>
        </tr>
        <tr>
            <td ng-repeat="t in inner.totals">{{t | number : 2}}</td>
        </tr>
        <tr ng-repeat="book in inner.books" >
            <td colspan="{{ inner.totals.length }}">
                <inner inner="book" ng-if="book"></inner>
            </td>
        </tr>
    </tbody>
</table>

Note ng-if="book" probably isn't necessary; I added that when I troubleshooting something.

Upvotes: 1

Related Questions