Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can the Angular $injector be decorated with $provide.decorator?

Tags:

Perhaps this is a terrible idea, but if it is then please tell me why and then pretend that it's an academic exercise that won't see the light of day in production.

I'd like to add some logic to the Angular $injector service, to monitor when certain services are injected into other services. Since it seems that Angular provides a mechanism for decorating services, I thought this would be the way to go. However, the following code throws an error.

(function () {
    'use strict';

    var app = angular.module('app');

    app.config(['$provide', function ($provide) {
        $provide.decorator('$injector', ['$log', '$delegate', addLoggingToInjector]);
    }]);

    function addLoggingToInjector($log, $delegate) {
        var baseInstantiate = $delegate.instantiate;
        var baseInvoke = $delegate.invoke;

        $delegate.instantiate = function (type, locals) {
            // $log.debug('Calling $injector.instantiate');

            baseInstantiate(type, locals);
        };

        $delegate.invoke = function (fn, self, locals) {
            // $log.debug('Calling $injector.invoke');

            baseInvoke(fn, self, locals);
        };

        return $delegate;
    };
})();

The specific error is:

Uncaught Error: [$injector:modulerr] Failed to instantiate module app due to: Error: [$injector:unpr] Unknown provider: $injectorProvider

like image 512
John Bledsoe Avatar asked Dec 12 '13 18:12

John Bledsoe


People also ask

What does the @injectable decorator do in angular?

The @Injectable decorator together with the ‘providedIn’ option means the service should not be added within the providers’ array of a module. The correct answers are 2 and 4. To find out why, keep reading this guide, where we will discuss the @Injectable decorator and its effect on DI in Angular.

What are the different types of decorators in angular?

In Angular, the Decorators are classified into 4 types. They are as follows: Method Decorators: @HostListener (This decorator is used for methods inside a class like a click, mouse hover, etc.) Parameter Decorators: @Inject (This decorator is used inside class constructor). Note: In Angular, each decorator has a unique role.

Where should I add the @injectable decorator?

The @Injectable decorator should be added to each of the services. The @Injectable decorator should be added to any service that uses dependency injection (DI). The @Injectable decorator is not compulsory to add if you don’t use the ‘providedIn’ option.

What is the use of @host decorator in angular?

But when the @Host decorator is used, the process stops at the first stage of resolving a dependency in element injectors within one component view. The @Host decorator is heavily used inside built-in form directives. For example, to inject a hosting form into the ngModel directive and register a form created by the directive with the form.


2 Answers

The answer is: no.


$provide.decorator is used to intercept service creation -- that is why it is called from .config block, when there is still time to configure all services, as none of them has been created. $provide.decorator basically gets the Provider of the service and swaps its $get with newly delivered decorFn.

$injector is not like other services. It is created, as the very first step of bootstrapping an application -- way before app.config is called. [look at functions: bootstrap and createInjector in angular source code]

But hey, you can achieve your goal quite easily by tweaking the source code just a bit :-) Particularly look at function invoke(fn, self, locals).


UPDATE I got some inspiration from @KayakDave. You actually do not have to dig in the source-code itself. You can use the following pattern to observe each call to any of $injector methods:

 app.config(['$injector', function ($injector) {

      $injector.proper =
      {
          get : $injector.get,
          invoke : $injector.invoke,
          instantiate : $injector.instantiate,
          annotate : $injector.annotate,
          has : $injector.has
      }

      function getDecorator(serviceName)
      {
          console.log("injector GET: ", serviceName);
          return this.proper.get(serviceName);
      }

      function invokeDecorator(fn, self, locals)
      {
          console.log("injector INVOKE: ", fn, self, locals);
          return this.proper.invoke(fn, self, locals);
      }

      function instantiateDecorator(Type, locals)
      {
          console.log("injector INSTANTIATE: ", Type, locals);
          return this.proper.instantiate(Type, locals);
      }

      function annotateDecorator (fn)
      {
          console.log("injector ANNOTATE: ", fn);
          return this.proper.annotate(fn);
      }

      function hasDecorator(name)
      {
          console.log("injector HAS: ", name);
          return this.proper.has(name);
      }

      $injector.get = getDecorator;
      $injector.invoke = invokeDecorator;
      $injector.instantiate = instantiateDecorator;
      $injector.annotate = annotateDecorator;
      $injector.has = hasDecorator;
  }]);

PLNKR

like image 137
artur grzesiak Avatar answered Oct 07 '22 02:10

artur grzesiak


You can't use the Angular decorator service on $injector. As Artur notes $injector is a bit different from other services. But we can create our own decorator.

Why we can't use Angular's decorator

At the code level the issue is that $injector doesn't have a constructor function- there's no $injectorProvider.

For example both of these return true:

$injector.has('$location');
$injector.has('$locationProvider') 

However, while this returns true:

$injector.has('$injector')

this returns false:

$injector.has('$injectorProvider')

We see the importance when we look at the Angular decorator function:

function decorator(serviceName, decorFn) {
   var origProvider = providerInjector.get(serviceName + providerSuffix),
       orig$get = origProvider.$get;

   origProvider.$get = function() {
      var origInstance = instanceInjector.invoke(orig$get, origProvider);
     return instanceInjector.invoke(decorFn, null, {$delegate: origInstance});
   };
}

And

providerSuffix = 'Provider'

So the Angular decorator expects to operate on the service's constructor (serviceName + providerSuffix). Pragmatically, since we don't have an $injectorProvider we can't use decorator.

Solution

What we can do is override the Angular injector's get function ourselves by replacing the injector's default get with one that calls the original, Angular defined, get followed by our function.

We'll apply this to $injector rather than the nonexistent $injectorProvider like so:

app.config(['$provide','$injector', function ($provide,$injector) {

    // The function we'll add to the injector
    myFunc = function () {
        console.log("injector called ", arguments);
    };

    // Get a copy of the injector's get function
    var origProvider = $injector,
        origGet = origProvider.get;

    //Override injector's get with our own
    origProvider.get = function() {

        // Call the original get function 
        var returnValue = origGet.apply(this, arguments);

        // Call our function
        myFunc.apply(this,arguments);

        return returnValue;
    }
}]);

You'll see the provider being injected is the first augment, so app.value('aValue', 'something'); yields the following log statement:

injector called  ["aValueProvider"]

Demo fiddle

like image 27
KayakDave Avatar answered Oct 07 '22 02:10

KayakDave