12

I'm trying to extend the angular-ui tabset functionality and I'm running into issues with wrapping it.

This plunker is the tabset directive un-wrapped:

http://plnkr.co/edit/AhG3WVNxCal5fZOUbSu6?p=preview

This plunker contains my first attempt at wrapping the tabset directive:

http://plnkr.co/edit/naKXbeVOS8nizwDPUrkT?p=preview

The initial wrapping approach is straight-forward wrapping. But... I introduce extra divs in the replacement template to avoid the "Multiple directives asking for isolated scope" and "Multiple directives asking for transclusion" angular errors and to make sure transclusion happens.

Key code snippets:

.directive('urlTabset', function() {
  return {
    restrict: 'E',
    transclude: true,
    replace: true,
    scope: {
      tabManager: '='
    },
    controller: [ "$scope", function($scope) {
      var tabManager = $scope.tabManager;
    }],
    template:
      '<div>' +
        '<tabset>' +
          '<div ng-transclude>' +
          '</div>' +
        '</tabset>' +
      '</div>'
  };
})

.directive('urlTab', function() {
  return {
    require: '^urlTabset',
    restrict: 'E',
    transclude: true,
    replace: true,
    scope: { tabName: '@' },
    link: function(scope, element, attrs, urlTabsetCtrl) {
    },
    template:
      '<div>' +
        '<tab>' +
          '<div ng-transclude>' +
          '</div>' +                  
        '</tab>' +
      '</div>'
  };
});

However, I think the extra divs in the template are causing issues. Here is the unwrapped tabset with extra divs in the places my template would add them.

http://plnkr.co/edit/kjDs7xJcZqltCAqUSAmX?p=preview

So the logical thing is to eliminate the divs... but this is where I need the help. Does anyone know of a clean way to do this without hitting the "Multiple directives asking for isolated scope" and "Multiple directives asking for transclusion" angular errors. Here is one failed attempt.

http://plnkr.co/edit/0C6lFNhfdTVcF7ahuN3G?p=preview

Error: Multiple directives [urlTab, tab] asking for transclusion on: <tab class="ng-isolate-scope ng-scope">

BTW, in case you're wondering what I'm trying to do, my end goal is to use the tabManager attribute passed to urlTabset to auto-populate fields in the tab directive (wrapped by urlTab). To be more concrete this is what I'm aiming for:

.directive('urlTab', function() {
  return {
    require: '^urlTabset',
    restrict: 'E',
    transclude: true,
    replace: true,
    scope: { tabName: '@' },
    link: function(scope, element, attrs, urlTabsetCtrl) {
      scope.tabs = urlTabsetCtrl.tabs;
      scope.tabSelected = urlTabsetCtrl.tabSelected;
    },
    template:
      '<tab active="tabs[tabName].active" disabled="tabs[tabName].disabled" select="tabSelected(tabName)" ng-transclude>' +
      '</tab>'
  };
});

The template above obviously does not work, but it gives you the gist of what I'm trying to do.

And I'm okay with a solution that requires the wrapping directive not have an isolated scope. I can get around this by storing state in the controller context.

Amir
  • 720
  • 8
  • 18

1 Answers1

6

If you are trying to augment angular-ui's functionality, you may be better off doing it with attribute directives rather than brand new elements. I may be mistaken but it looks like you're not intending to alter the general stucture of the DOM other than to replace your directive with angular-ui's ones. For instance, using the HTML

<tabset url-tabset>
    <tab url-tab>
        <tab-heading>
            <i class="icon-list"></i> Details
        </tab-heading>
        Details content.
    </tab>
    <tab url-tab>
        <tab-heading>
            <i class="icon-thumbs-up"></i> Impact
        </tab-heading>
        Impact tab content.
    </tab>                    
</tabset>

would mean you no longer need to perform any transclusion or template replacement. This would avoid that problem all together.

This leaves the problem of isolated scope for attributes you want to use for the augmentation. Instead of using this, you can use scope: true to grab the same isolated scope as tab and tabset (though you cannot define bindings here) and you can get attributes as you would use normal bound values by using $parse and attrs.

Your directives (with the functionality from your second plunker) then end up looking something like this.

angular.module('plunker', ['ui.bootstrap'])

.directive('urlTabset', function() {
  return {
    restrict: 'A',
    require: 'tabset', // Confirm the directive is only being used on tabsets
    controller: [ "$scope", "$attrs", function($scope, $attrs) {
      var tabManagerGetter = $parse($attrs.tabManager); // '='
      this.getTabManager = function() {
        return tabManagerGetter($scope);
      };

      // fun stuff here
    }]
  };
})

.directive('urlTab', function() {
  return {
    require: ['tab', '^urlTabset'],
    restrict: 'A',
    link: function(scope, element, attrs, ctrls) {
      var urlTabsetCtrl = ctrls[1];

      function getTabName() {
        return attrs.tabName; // '@'
      }

      var tabManager = urlTabsetCtrl.getTabManager();

      // fun stuff here
    }
  };
});
Andyrooger
  • 6,748
  • 1
  • 43
  • 44
  • Thanks first of all for your response. I appreciate it. My plunker was simplified from what I wanted to do - which is to use the tabManager state (specified in urlTabset) to auto-populate attributes in the tab directive (wrapped by urlTab). So it's not a strict augmentation. Meaning this approach will not work for me. Any other ideas? – Amir Nov 24 '13 at 07:09
  • I think if it's just adding attributes you can do that through `attrs` rather than using the template. So long as your directive is on the same element as the one you wish to alter. – Andyrooger Nov 24 '13 at 09:11
  • Interesting. You're suggesting in the compile phase add attributes so when the wrapped directive processes them in it's compile / link function it will see them. But this won't handle variables in the isolated scope of the wrapped directive like 'select' for tab. Or maybe since compilations are ordered it will all workout. Still it's a bit hacky. I'd like to not have to do something like this "". I want true wrapping so the html looks clean "". But I appreciate your effort / idea sharing. I'm hoping there is a straightforward way to achieve clean wrapping. – Amir Nov 24 '13 at 09:56
  • 1
    Well, for expression attributes you can still add the expression in your compile phase, but you'd need to add scope variables to the parent scope, which I agree is very hacky. Ideally the `tabset` and `tab` controllers would expose functions to use from your directive (like using `ngModel`) but all you can really do at the moment is `tabsetCtrl.select` or possible `.addTab` and `.removeTab`. Essentially, if you are looking to replace your directive in-place with the angular-ui ones you will both be using the same scope, and only one of you will be able to transclude or ask for isolation. – Andyrooger Nov 24 '13 at 13:38
  • Thanks again for your detailed response. For these wrapped directives I don't really need the isolated scope (the controller context is sufficient for me). As for transclusion I'm happy to let the wrapped directive take care of transclusion so it only happens once. I'm hoping there is a way to say replace this element with this and then continue compiling... – Amir Nov 25 '13 at 12:32
  • 1
    Thanks for the bounty @Amir! Shame no-one was able to provide a simpler/less messy design for this though. – Andyrooger Dec 03 '13 at 00:11
  • @Andyrooger- Thanks for your thorough responses. I really appreciate you taking out the time to assist. And yes... maybe one day someone will come up with a slightly simpler solution - angular 1.3+ maybe :) – Amir Dec 03 '13 at 13:29