I'd like to use ng-include in the content of a dynamically generated tab using AngularJs and UI Bootstrap.
I have a Plunker here:
http://plnkr.co/edit/2mpbovsu2eDrUdu8t7SM?p=preview
<div id="mainCntr" style="padding: 20px;">
<uib-tabset>
<uib-tab ng-repeat="tab in tabs" active="tab.active" disable="tab.disabled">
<uib-tab-heading>
{{tab.title}} <i class="glyphicon glyphicon-remove-sign" ng-click="removeTab($index)"></i>
</uib-tab-heading>
{{tab.content}}
</uib-tab>
</uib-tabset>
</div>
JS Code:
$scope.addTab = function() {
var len = $scope.tabs.length + 1;
var numLbl = '' + ((len > 9) ? '' : '0') + String(len);
var mrkUp = '<div>' +
'<h1>New Tab ' + numLbl + ' {{foo}}</h1>' +
'<div ng-include="tab.tabUrl" class="ng-scope"></div>' +
'</div>';
$scope.tabs.push({title: 'Tab ' + numLbl, content: $compile(angular.element(mrkUp))($scope)});
}
In the Plunker, click the "Add Tab" button. It calls a function in $scope that pushes a new tab to the collection but passing in some dynamically generated content that includes a ng-include directive. The expected output is that the ng-include will be displayed inside of the tab content area.
Thanks
In your Plunker you are using ng-bind-html
which doesn't compile the HTML for you. You can create a new directive that does that for you.
Source code for ng-bind-html
:
var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) {
return {
restrict: 'A',
compile: function ngBindHtmlCompile(tElement, tAttrs) {
var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml);
var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) {
return (value || '').toString();
});
$compile.$$addBindingClass(tElement);
return function ngBindHtmlLink(scope, element, attr) {
$compile.$$addBindingInfo(element, attr.ngBindHtml);
scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
// we re-evaluate the expr because we want a TrustedValueHolderType
// for $sce, not a string
element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || '');
});
};
}
};
}];
Pick a name for the new directive, for example compile-html
.
Replace tAttrs.ngBindHtml
with tAttrs.compileHtml
(or whatever name you picked).
You need to replace $sce.getTrustedHtml
with $sce.trustAsHtml
, or you will get Error: [$sce:unsafe] Attempting to use an unsafe value in a safe context.
Then you need to call $compile
:
$compile(element.contents())(scope);
Full directive:
app.directive('compileHtml', ['$sce', '$parse', '$compile',
function($sce, $parse, $compile) {
return {
restrict: 'A',
compile: function ngBindHtmlCompile(tElement, tAttrs) {
var ngBindHtmlGetter = $parse(tAttrs.compileHtml);
var ngBindHtmlWatch = $parse(tAttrs.compileHtml, function getStringValue(value) {
return (value || '').toString();
});
$compile.$$addBindingClass(tElement);
return function ngBindHtmlLink(scope, element, attr) {
$compile.$$addBindingInfo(element, attr.compileHtml);
scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
element.html($sce.trustAsHtml(ngBindHtmlGetter(scope)) || '');
$compile(element.contents())(scope);
});
};
}
};
}
]);
Usage:
<div compile-html="tab.content"></div>
Demo: http://plnkr.co/edit/TRYAaxeEPMTAay6rqEXp?p=preview
My situation might not be as complex, so this simple solution works:
sdo.tabs:{
data:[],
active:0,
reset: function(){
var tabs = this.data;
while( tabs.length > 0 ) {
this.removeTab( tabs[tabs.length-1].child.name);
}
this.active = 0;
},
childExists: function( childName ) {
var fromTheTop = this.data.length,
parentName = ( this.active > 0 ? this.data[ this.active - 1 ].child.name : 'zero' );
while( fromTheTop > this.active ) {
var child = this.data[ fromTheTop-1 ].child;
if( child && child.parent === parentName && child.name === childName ) return fromTheTop;
fromTheTop--;
}
return false;
},
removeTab: function( name ) { // will remove any descendents of this tab as well, see recursive call near end
var fromTheTop = this.data.length;
while( fromTheTop > 0 ) {
var tab = this.data[fromTheTop - 1];
if( tab.child.name === name ) {
angular.element( '#'+name ).empty();
this.data.splice( fromTheTop - 1);
return;
}
if( tab.child.parent === name) this.removeTab( tab.child.name );
fromTheTop--;
};
},
/*
* tab is string identifies tab but doesn't show in the UI
* tempmlate is HTML template
* scope is used to compile template
* title is string or function for UI tab title, appears in the tab row
*/
create: function( tab, template, scope, title ) {
var childName = tab;
var tabs = this.data;
tab = this.childExists( childName );
if( tab === false ) {
tab = tabs.length + 1;
} else { // recycling a tab, kill it & its descendents
this.removeTab( childName );
}
tabs[tab-1] = {
title:function(){
if( angular.isFunction(title) ) return title();
return title;
},
child: {
parent:( this.active > 0 ? this.data[ this.active - 1 ].child.name : 'zero' ),
name:childName
}
};
var ct = $timeout( function() {
angular.element( '#'+tabs[tab-1].child.name ).html( $compile( template )( scope ) );
sdo.tabs.active = tab;
return; // return nothing to avoid memory leak
});
scope.$on('$destroy', function() {
$timeout.cancel( ct );
});
return ct; // ct is a promise
}
}
HTML is
<uib-tabset active="tabs.active">
<uib-tab index='0' heading="{{title}}">
<ng-view></ng-view>
</uib-tab>
<uib-tab ng-repeat="tab in tabs.data track by tab.child.name" heading="{{tab.title()}}" index='$index+1' >
<div id="{{tab.child.name}}"></div>
</uib-tab>
</uib-tabset>
In my case the first tab is populated by the Angular router, which is why the tab array is one index out from tabs.active