将多个模板作为HTML注入指令并编译到指令范围

时间:2015-03-05 21:39:09

标签: javascript angularjs templates angularjs-directive

我正在开发一个自定义角度指令,其主要目标是显示数据网格。我想允许我的指令用户将自己的html单元格模板注入其中,这将覆盖标准的单元格渲染机制。我当然可以让用户在json配置对象中定义他们的单元格模板,并使用属性将其传输到我的指令中,但我认为更好的方法是让用户直接将他们的模板指定为HTML。

预期用途

    <my-grid config="config" data="data">
      <cell field="active">
        <!-- User templates given here for specific columns -->
      </cell>
    </my-grid>

现在,我在我的指令中使用链接函数的transclude方法来收集指令控制器可用的<cell>模板。 但是transclude函数返回已编译的html,并且编译html的范围当然是使用页面<my-grid>的控制器。这意味着注入的单元模板不可能使用给定数据中的值进行转换到格子指令到位。

为了实现这一点,<cell>内的所有内容都需要注入未编译状态。但这似乎是不可能的。我甚至连接到编译方法,看看我是否可以直接从$ element输入参数获得给定的<cell>元素,但即使这样也为时已晚。此时,已经包含了来自其他包含指令的模板,覆盖了<cell>元素。

有没有人知道如何实现我想要做的事情?

我的指示

angular.module('myApp', [])
    .controller('myController', function ($scope) {
        'use strict';

        // Simplified metadata for the grid structure
        $scope.config = {
            columns: [{name: 'id'}, {name: 'name'}, {name: 'active'}, {name: 'comment'}]
        };

        // Provide data for the directive
        $scope.data = [
            {id: 1, name: 'test1', active: true, comment: 'Contains something'},
            {id: 2, name: 'test2', active: false, comment: 'Another comment'}
        ];
    })
    .directive('myGrid', ['$timeout', '$compile', function ($timeout, $compile) {
        'use strict';
        return {
            restrict        : 'E',
            templateUrl     : 'grid/templates/gridPanel.html',
            transclude      : true,
            controllerAs    : 'gridCtrl',
            bindToController: true,
            scope           : {
                config: '=',
                data  : '='
            },
            controller      : ['$scope', '$element', function ($scope, $element) {
                var ctrl = this;
                $timeout(function () {
                    var elToReplace = $('[tpl]', $element);
                    elToReplace.replaceWith(ctrl.getTemplate($compile(elToReplace.attr('tpl'))($scope)));
                });
                ctrl.getTemplate = function (column) {
                    return (ctrl.templates[column] ? ctrl.templates[column] : ctrl.templates['__ALL__']);
                }
            }],
            compile         : function compile ($element, $attrs, transclude) {
                var origEl = $element; // $element allready contains the directive template here. <cell> is unobtainable.
                return function postLink ($scope, $element, $attrs, ctrl, $transclude) {

                    // This provides a list of <cell> elements, but they are allready compiled in the scope they are provided in.
                    $transclude(function (overrides) {
                        ctrl.templates = _.chain(overrides)
                            .filter(function (content) {
                                return content.nodeName.toUpperCase() === 'CELL';
                            })
                            .indexBy(function (content) {
                                return $(content).attr('field');
                            })
                            .value();
                        ctrl.templates['__ALL__'] = '<span>{{ row[column.name] }}</span>';
                    });
                }
            }
        };
    }])
    .run(['$templateCache', function ($templateCache) {
        'use strict';
        $templateCache.put('grid/templates/gridPanel.html',
            '<div class="panel panel-default">' +
            '  <header class="panel-heading">' +
            '    TEST-GRID' +
            '  </header>' +
            '  <div class="panel-body">' +
            '    <table class="table table-condensed">' +
            '      <thead>' +
            '        <tr>' +
            '          <th ng-repeat="column in gridCtrl.config.columns track by column.name">' +
            '            {{ column.name }}' +
            '          </th>' +
            '        </tr>' +
            '      </thead>' +
            '      <tbody>' +
            '        <tr ng-repeat="row in gridCtrl.data track by $index">' +
            '          <td ng-repeat="column in gridCtrl.config.columns track by column.name">' +
            '            <span tpl="column"></span>' + // This is where the cell template should be rendered.
            '          </td>' +
            '        </tr>' +
            '      </tbody>' +
            '    </table>' +
            '  </div>' +
            '</div>');
    }]);
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet"/>

<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.3.1/lodash.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>

<div ng-app="myApp" ng-controller="myController">
  <div class="container-fluid">
    <h2>My grid setup</h2>
    <my-grid config="config" data="data">
      <cell field="active">
        <label class="checkbox">
          <input type="checkbox" ng-model="row.active" />
        </label>
      </cell>
    </my-grid>
  </div>
</div>

这不起作用。首先,它不会按原样转换模板。它只会转换我设置为“默认模板”的内容,并将其作为明文进行转换。没编译。如果我尝试在范围内编译它,则角度会产生一个不定的摘要循环。

1 个答案:

答案 0 :(得分:0)

修正了它。秘密是transclude函数接收范围。我只需要保存它,并在编译模板时使用它。 : - )

以下是我的指令版本,它按预期工作!

angular.module('myApp', [])
    .controller('myController', function ($scope) {
        'use strict';

        // Simplified metadata for the grid structure
        $scope.config = {
            columns: [{name: 'id'}, {name: 'name'}, {name: 'active'}, {name: 'comment'}]
        };

        // Provide data for the directive
        $scope.data = [
            {id: 1, name: 'test1', active: true, comment: 'Contains something'},
            {id: 2, name: 'test2', active: false, comment: 'Another comment'}
        ];
    })
    .directive('myGrid', ['$timeout', '$compile', function ($timeout, $compile) {
        'use strict';
        return {
            restrict        : 'E',
            templateUrl     : 'grid/templates/gridPanel.html',
            transclude      : true,
            controllerAs    : 'gridCtrl',
            bindToController: true,
            scope           : {
                config: '=',
                data  : '='
            },
            link            : function postLink ($scope, $element, $attrs, ctrl, $transclude) {
                // Set default cell template
                var defaultTemplates = [
                    $('<cell field="__ALL__"><span>{{ row[column.name] }}</span></cell>').get(0)
                ];
                // Collect cell templates given -----------------
                $transclude(function(overrides, futureScope) {
                    var transcluded = {
                        tpls: _.filter(overrides, function (content) {
                            return content.nodeName.toUpperCase() === 'CELL';
                        }),
                        scope: futureScope
                    };
                    transcluded.tpls = transcluded.tpls.concat(defaultTemplates); // Insert defaults
                  
                    // Expose cell templates to controller scope
                    ctrl.cellTemplates = _.chain(transcluded.tpls)
                        // Group by column name
                        .groupBy(function(tpl)   { return $(tpl).attr('field'); })
                    
                        // Create the template object
                        .mapValues(function(tpl) {
                            var template = $(tpl).html().trim();
                            return {
                                tpl: template,
                                isTranscluded: $(tpl).hasClass('ng-scope'),
                                origScope: ($(tpl).hasClass('ng-scope') ? transcluded.scope : null), // Save the original scope the template was given in
                                compiledFn: $compile(template) // Precompile templates
                            };
                        })
                        .value();
                    
                    // Create convenience method to retrieve template based on column name
                    ctrl.getTemplate = function (column) {
                        return _.has(ctrl.cellTemplates, column.name) ? ctrl.cellTemplates[column.name] : ctrl.cellTemplates.__ALL__;
                    }

                });

            },
            controller      : ['$scope', '$element', function ($scope, $element) {
                var ctrl = this;
            }]
        };
    }])
    .directive('myGridCell', function () {
        'use strict';
        return {
            restrict: 'A',
            require : '^^myGrid',
            scope   : {
                column  : '=myGridCell', // This is the configuration object for this column
                row     : '=',           // Contains the data for the entire row
                rowIndex: '='            // The row index in the grids dataset
            },
            link: function ($scope, $element, $attrs, ctrl) {
                var me = {
                    childScope: null,
                    renderTemplate: function () {
                        $element.empty();
                        // Retrieve template for the given column
                        var tpl = ctrl.getTemplate($scope.column);

                        // Cleanup old fun
                        if (me.childScope && me.childScope.$id !== $scope.$id) {
                            me.childScope.$destroy();
                        }
                        // Get or create the child scope
                        if (tpl.isTranscluded) {
                            // Create a new scope as a child of the one transcluded
                            me.childScope            = tpl.origScope.$new();
                            
                            // Attach important properties to newly created scope
                            me.childScope.row        = $scope.row;
                            me.childScope.column     = $scope.column;
                            me.childScope.rowIndex   = $scope.rowIndex;
                        } else {
                            me.childScope = $scope;
                        }
                        // Attach controller to childScope
                        me.childScope.gridCtrl = ctrl;

                        // Apply the template in the appropriate scope
                        tpl.compiledFn(me.childScope, function (clonedElement, scope) {
                            scope.$element = clonedElement;
                            $element.append(clonedElement);
                        });
                    }
                }
                me.renderTemplate();
            }
        }
    })
    .run(['$templateCache', function ($templateCache) {
        'use strict';
        $templateCache.put('grid/templates/gridPanel.html',
            '<div class="panel panel-default">' +
            '  <header class="panel-heading">' +
            '    TEST-GRID' +
            '  </header>' +
            '  <div class="panel-body">' +
            '    <table class="table table-condensed">' +
            '      <thead>' +
            '        <tr>' +
            '          <th ng-repeat="column in gridCtrl.config.columns track by column.name">' +
            '            {{ column.name }}' +
            '          </th>' +
            '        </tr>' +
            '      </thead>' +
            '      <tbody>' +
            '        <tr ng-repeat="(rowIndex, row) in gridCtrl.data track by $index">' +
            '          <td ng-repeat="column in gridCtrl.config.columns track by column.name" my-grid-cell="column" row="row" row-index="rowIndex">' +
            '          </td>' +
            '        </tr>' +
            '      </tbody>' +
            '    </table>' +
            '  </div>' +
            '</div>');
    }]);
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet"/>

<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.3.1/lodash.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>

<div ng-app="myApp" ng-controller="myController">
  <div class="container-fluid">
    <h2>My grid setup</h2>
    <my-grid config="config" data="data">
      <cell field="active">
        <label class="checkbox">
          <input type="checkbox" ng-model="row.active" />
        </label>
      </cell>
    </my-grid>
  </div>
</div>