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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With