Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic Content in Dynamic Tab (Angular, UI Bootstrap)

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

like image 891
vdiaz1130 Avatar asked Nov 13 '15 00:11

vdiaz1130


2 Answers

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

like image 114
tasseKATT Avatar answered Oct 16 '22 08:10

tasseKATT


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

like image 31
Nik Dow Avatar answered Oct 16 '22 10:10

Nik Dow