Decorate Service in Directive based on Attributes

2019-07-20 21:05发布

问题:

Disclosure: I'm new to angular, so if I'm doing something that appears strange or just plain wrong, feel free to point that out.

I have a directive vpMap that I want to use like this:

<div my-map with-tile-layers with-geolocator></div>

I have a Map service that I want to decorate conditionally, based on the attributes of the directive that begin with with

It looks something like this:

angular.module('myApp.map',[])
   .service('Map',someServiceFunction)
   .directive('myMap',['Map','$provide',function(Map,$provide) {

      return {
        controller: function($scope,$element,$attrs) {

          angular.forEach($attrs,function(val,key) {
             if(key.match(/^with/)) {
               switch(key) {
                 case 'withTileLayers':
                 $provide.decorator(Map,someDecoratorFunction);
                 break;
               }
             }
          });

        }
      };

   }]);

It's at this point that I discover I can't access the $provide service in my directive, although I'm not sure why. According to the documentation you can inject anything into directives and, I thought $provide was one of those global angular services like $http

Is this bad architecture? What rules am I not understanding?

回答1:

Providers are meant to be used by the $injector in the configuration phase.

From the providers guide:

Once the configuration phase is over, interaction with providers is disallowed and the process of creating services starts. We call this part of the application life-cycle the run phase.

So you would have to use the config block for decorating.

Attempted Fix

To decorate a service conditionally, you may use some predicate inside the decorator's function, something along the lines of this (not tested):

// define a predicate that checks if an object starts with a key 
function keyInObject(obj, key) {
    var propKeys = Object.keys(obj),
        index = propKeys && propKeys.length || 0;
    while (index--) {
        if (propKeys[index].indexOf(key) === 0) {
            return true;
        }
    }
    return false;
}

app.config(function($provide) {

    // uhmm, decorate
    $provide.decorator('Map', function($delegate) {
        var service = $delegate;

        // only extend the service if the predicate fulfills        
        keyInObject(service, 'with') && angular.extend(service, {
            // whatever, man
        });

        return $delegate;
    });
});

However, even this won't help us, as we need to get the keys in the linking phase, which is well after the service have been injected (and decorated).

To make sure you fully understand the meaning of decoration here; we use decoration to permanently alter the object (the service) structure. Decoration refers to the object's meta data, not its state.

Solution

Given the above, the best choice would be to simply create a service with all the required functionality, and abandon decoration altogether. Later, while inside link you can use the iteration to decide which of the methods to call, e.g.:

// I took the liberty of switching the positions of key/val arguments
// for semantics sake...
angular.forEach($attrs, function(key, val) {
    if (val.match(/^with/)) {
        switch (val) {
            case 'withTileLayers':
                Map.useTileLayers(); // use specific API in the service
                break;
        }
    }
});