将多个模板作为 HTML 注入指令并编译到指令范围
inject multiple templates as HTML into directive and compile to the directives scope
我正在开发一个自定义 angular 指令,其主要目标是显示数据网格。我想允许我的指令的用户将他们自己的 html 单元格模板注入其中,这将覆盖标准单元格渲染机制。我当然可以让用户在 json 配置对象中定义他们的单元格模板,并使用属性将其传输到我的指令中,但我认为更好的方法是让用户直接将他们的模板指定为 HTML.
预期用途
<my-grid config="config" data="data">
<cell field="active">
<!-- User templates given here for specific columns -->
</cell>
</my-grid>
现在,我在我的指令中使用 link 函数的 transclude 方法来收集指令控制器可用的 <cell>
模板。
但是transclude函数returns编译html,编译html的作用域当然是使用页面<my-grid>
的控制器。这意味着单元格使用给定网格指令的数据中的值不可能嵌入注入的模板。
为了实现这一点,<cell>
中的所有内容都需要未编译地注入。但这似乎是不可能的。我什至连接到 compile 方法以查看是否可以直接从 $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>
这不起作用。首先,它没有按应有的方式嵌入模板。它只嵌入我设置为 'default template' 的内容,并且它被嵌入为明文。未编译。如果我尝试在范围内编译它,angular 会产生一个不确定的摘要循环。
已修复。秘诀在于 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>
我正在开发一个自定义 angular 指令,其主要目标是显示数据网格。我想允许我的指令的用户将他们自己的 html 单元格模板注入其中,这将覆盖标准单元格渲染机制。我当然可以让用户在 json 配置对象中定义他们的单元格模板,并使用属性将其传输到我的指令中,但我认为更好的方法是让用户直接将他们的模板指定为 HTML.
预期用途
<my-grid config="config" data="data">
<cell field="active">
<!-- User templates given here for specific columns -->
</cell>
</my-grid>
现在,我在我的指令中使用 link 函数的 transclude 方法来收集指令控制器可用的 <cell>
模板。
但是transclude函数returns编译html,编译html的作用域当然是使用页面<my-grid>
的控制器。这意味着单元格使用给定网格指令的数据中的值不可能嵌入注入的模板。
为了实现这一点,<cell>
中的所有内容都需要未编译地注入。但这似乎是不可能的。我什至连接到 compile 方法以查看是否可以直接从 $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>
这不起作用。首先,它没有按应有的方式嵌入模板。它只嵌入我设置为 'default template' 的内容,并且它被嵌入为明文。未编译。如果我尝试在范围内编译它,angular 会产生一个不确定的摘要循环。
已修复。秘诀在于 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>