Renaming 3rd party Angular Directive using $provid

2019-06-25 10:14发布

I've using the excellent Angular UI bootstrap in a large site alongside lots of my own directives. For the sake of neatness I looked for a way of renaming a couple of the uib directives without touching their code and came across a couple of SO questions and what looked like a great solution. The trouble is that it's not working for me. Here's my code.

angular.module('s4p').config(function($provide, $compileProvider){

    console.log("I am logged in the console");

    $provide.decorator('uibPaginationDirective', function($delegate){

        console.log("I am not logged");

        $compileProvider.directive('s4pPaging', function(){
           return $delegate[0];
        });

        return function() { return angular.noop };

    });
});

I've put a couple of console.logs in there to see how far it's getting and I can see that it's not even running the decorator. At first I thought this might have been a simple spelling mistake in uiPaginationDirective but if I change even a single character then I get a big injector error in the console. So, it's definitely finding the original directive but the decorate function is not running.

Am I doing something really stupid?

Edit: Just to clarify, I'm using ng-annotate to simplify dependency injection, hence no array of dependencies in the .config()

Edit 2: The decorator doesn't run if the element isn't used on the page, which I didn't realise. So, if you use the old element then the decorator does run, but if you use the new alias then nothing happens at all. I'm beginning to think that this code I found may never have worked unless the original directive is used to kick the decorator into gear.

Edit 3: I've added a bounty to this because it would be nice to find a solution. Even if nobody can fix the example code there must be other ways of renaming a third party directive without changing it? I have found a plugin called "alias" which does this but I don't really want to use more 3rd party code and I want to understand the solution.

Edit 4: I have been playing with the code which I borrowed to make this which you can see in the following plunkr - http://plnkr.co/edit/3aDEed?p=preview I think it only works because the demo uses the original directive in the HTML followed by the renamed directive. If you remove the original one from the HTML then they both disappear. This I assume is because the directive decorator is only run the first time the directive is used. So, my quest is now to find a way to get the decorator to run without using the original directive, or to look for a better way to rename a 3rd party directive...

Edit 5: I tried something else which seems to be what the alias plugin seems to be doing under the hood.

angular.module('s4p').directive('s4pPaging', function(){

    return {
        restrict: 'EA',
        replace: true,
        template: '<uib-pagination class="s4pPaging"></uib-pagination>'
    }

});

...but unfortunately I get the following error.

Error: [$compile:multidir] Multiple directives [s4pPaging (module: s4p), uibPagination] asking for template on:...

I don't get why they are both asking for template. I would have assumed that the s4p-paging would get replaced with the uib-pagination and all of the attributes copied on to the uib-pagination element. Clearly I don't understand why something simple like that doesn't work.

2条回答
何必那么认真
2楼-- · 2019-06-25 10:35

Based on my old solution for another question, you would need to use at least one instance of the directive on the page since that is the way angular DI works. It lazily instantiates any factory only when it is used for the first time.

As i mention in my comment earlier you can get any directive configuration by getting the directive factory (directive name suffixed by the the word "Directive")$injector.get('directiveNameDirective').

Here is an abstraction for resolving the problem (remember this will resolve the naming conflict problem as well from the original thread also this is more modular and easy set up with just one config call in your app).

You can get the module from my repository

angular.module('renameDirectiveUtil', [])
.provider('renameDirective', ['$provide' , '$compileProvider' , function($provide, $compileProvider){
    var directiveSet;

    this.setConfig = function setConfig(config){
      directiveSet = config;

       angular.forEach(directiveSet, function iterator(targetDir, sourceDir){
          sourceDir +=  'Directive';
          //Set up decorators
          $provide.decorator(sourceDir, function decorate($delegate){

             //Used to register a directive. $delegate is the original directive definition.
             $compileProvider.directive(targetDir, function(){
                return $delegate[0];
             });

              //This will remove the original directive definition, if not just return the delegate as is.
              return function() { return angular.noop };
          });
      });
    };

    this.$get  = ['$injector', function renameDirectiveService($injector){
      return { 
        rename : function rename(){
          angular.forEach(directiveSet, function(_,dir){
             var sourceDir = dir + 'Directive';
            //Just return the original directive definition which will trigger the decorator and redefine itself while renaming the new one.
            $injector.get(sourceDir);
          });
        }
      }
    }];
}]).run(['renameDirective',function(renameDirective){
  renameDirective.rename();
}]);

All you would need to do now is to include this as dependency in your module.

 angular.module('plunker', ['renameDirectiveUtil']);

and configure it by providing a config object which is of the format {sourceDirectiveName : targetDirectiveName }.

app.config(['renameDirectiveProvider',function(renameDirectiveProvider){
  renameDirectiveProvider.setConfig({
    'property' : 'cmProperty'
  });
}]);

Here is a plunker

查看更多
淡お忘
3楼-- · 2019-06-25 10:37

Try this:

var app = angular.module('plunker', ['ui.bootstrap', 'myDirectives']);

angular.module('myDirectives', [])
.directive('myPagination', function($injector) {
  return angular.copy($injector.get('uibPaginationDirective'))[0];
});

app.controller('PaginationDemoController', function($scope) {});

And

<body ng-app="plunker">
  <div ng-controller="PaginationDemoController">
    <my-pagination total-items="20" ng-model="currentPage"></my-pagination>
  </div>
</body>

This works without having to use the original directive on the page first.

See the working plunker

查看更多
登录 后发表回答