Recently I was working on a nested directive, i.e., having a
directive inside another directive. In my scenario the typical ng-transclude
does not work due to the dynamic templates. Hence I used the transclude
function, but still I had to spend some time to get this working. The fix was
simple. Below is my problem and my solution for using transclusion in a nested
directives with dynamic templates.
On a high level my requirement is developer will be send an
array of objects and their types. Based on the type in the edit mode I need to
show different UI controls, such as textbox, select, textarea etc. When in the
view (non-edit) mode I have to show the text they pass to me (transclusion).
Below is the html for my directive.
<notes-input input-list="[{model: vm.model.Compliant,
type: 'text'}
, {model:
vm.model.DOB, type: 'date'}
, {model:
vm.model.Mentation, type:'dropdown'}
, {model:
vm.model.Summary, type: 'textarea'}]">
{{vm.model.Mentation.Value}}
</notes-input>
With the above html, I have to show the Mentation value in
the view mode or show the UI controls in the edit mode. I am using xeditable
for this and it is going a good job.
I decided to use two directive, one for looping through the
array of objects and another which shows different controls based on the object
type. The parent directive is easy and straight forward. Here is the code I
used:
angular.module('pos').directive('notesInput',
notesInput);
notesInput.$inject
= [];
function notesInput() {
return {
restrict: 'E',
transclude: true,
scope: {
inputList: '=',
},
template: '<notes-input-item ng-repeat="item in
inputList" item="item">'
+ '<span
ng-if="$first" ng-transclude></span>'
+ '</notes-input-item>'
}
}
As you use I am using ng-repeat to loop through the array
and invoking the child directive, notesInputItem. I am also doing a
transclusion only for the first item using ng-if. This is straight forward.
In the child directive, based on the type I am dynamically
creating the controls. Hence I cannot use the template property of the
directive definition object. I am dynamically generating the template html and
then compiling it to generate the html as shown below:
var elementHtml = getTemplate(scope.item); //based on the item type creating the control
element.html(elementHtml);
$compile(element.contents())(scope);
This compilation does not understand the ng-transclude at
the parent level. So I used transclude function and in that attaching the
transclusion to the element as shown below
transclude(scope, function (clone, scope) {
element.children().append(clone);
});
With the above code, I don’t see the transclusion text in
the browser. Angular is unable to bind the transclusion. After spending some
time I understood that I need to specify the parent directive scope instead of
child’s.
transclude(scope.$parent, function (clone, scope) {
element.children().append(clone);
});
When parent scope is specified for the transclusion, Angular
was able to bind the transcluded content and the directive performed as
expected.
Here is the entire code for my directives:
'use
strict'
angular.module('pos').directive('notesInput',
notesInput);
notesInput.$inject
= [];
function notesInput() {
return {
restrict: 'E',
transclude: true,
scope: {
inputList: '=',
},
template: '<notes-input-item ng-repeat="item in
inputList" item="item">'
+ '<span ng-if="$first"
ng-transclude></span>'
+ '</notes-input-item>'
}
}
angular.module('pos').directive('notesInputItem', notesInputItem);
notesInputItem.$inject
= ['$compile', 'session', 'constants'];
function notesInputItem($compile, session, constants) {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
item: '=',
},
link: link
}
function getTemplate(item) {
var editableSpan = '<span'
switch (item.type) {
case "textarea":
editableSpan += ' editable-textarea="item.model.Value"
e-rows="3"'
break;
case "dropdown":
item.model.selectoptions =
session.lookups[item.model.LookUpFieldName]
editableSpan += ' editable-select="item.model.Value"
e-ng-options="lookup.FieldDescription as lookup.FieldValue for lookup in
item.model.selectoptions"'
break;
case "date":
editableSpan += ' editable-bsdate="item.model.Value"'
break;
default:
editableSpan += ' editable-text="item.model.Value"'
}
editableSpan += ' e-name="{{item.model.Name}}"></span>'
return editableSpan;
}
function link(scope, element, attrs, nullController, transclude) {
var elementHtml = getTemplate(scope.item); //based on the item type creating the control
element.html(elementHtml);
$compile(element.contents())(scope);
transclude(scope.$parent, function (clone, scope) {
element.children().append(clone);
});
}
}