H W H W -3 years ago 99
AngularJS Question

AngularJS: Inherited dependencies need to be duplicated?

Using Angular 1.6 in combination with ES6-classes i ran into the following issue:

I wrote a service with some dependencies (surprise!)

class myService {

/*@ngInject*/
constructor($q){
this.$q = $q;
this.creationDate = new Date();
}

doStuff(data){
return this.$q.when(data);
}
}

angular.module('app').service('myService', myService)


However i got a build-target in which the service needed to be a bit fancier, so i extended it and used the extended service in that case instead:

class myFancyService extends myService{

/*@ngInject*/
constructor($q, $http){
super($q);
this.$http = $http;
}

doFancyStuff(data){
console.log(this.creationDate);
return this.doStuff(data)
.then((stuff) => this.$http.post('api.myapp', stuff));
}
}

angular.module('app').service('myService', myFancyService)


This works fine so far, but has a major drawback:

By calling
super(dependencies)
, the dependencies of my base-class can't get injected automatically from
@ngInject
. Thus i need to be extremely aware that anytime i change the dependencies of
myService
, the dependencies of
myFancyService
(and any other potential future child-class) need to be changed as well.

I can not use Composition instead of Inheritance because
myService
is not registered as angular-service and thus can't be injected as dependency.

Question:

Is there a way to inject dependencies of the baseclass automatically anyways?

If not, is there at least a way to let my unittests remind me that i need to update the dependencies of
myFancyService
? I couldn't find a way yet to test with karma/jasmine if the arguments (or maybe just the number of arguments) of
super($q)
equal the (number of) arguments of the myService-
constructor
.

Answer Source

Two things to keep in mind:

  1. in Inheritance Pattern having interface consistency is essential, child classes can re-implement methods or properties but they cannot change how a method is invoked (arguments, etc...)
  2. You are still registering BaseService to the dependency injection but you might don't need for that, because it looks like an abstract class for you.

This could solve your problem (run script to see what's happening) You basically need to extend the static $inject property in each derived class and use destructuring in each child constructor:

  • Benefits: You don't need to know what's dependencies a parent class has.
  • Constrains: Always use first parameters in your child class (because rest operator must be the last)

function logger(LogService) {
  LogService.log('Hello World');
}

class BaseService {
  static get $inject() { 
    return ['$q']; 
  }

  constructor($q) {
    this.$q = $q;
  }
  
  log() {
    console.log('BaseService.$q: ', typeof this.$q, this.$q.name);
  }
}

class ExtendedService extends BaseService {
  static get $inject() { 
    return ['$http'].concat(BaseService.$inject); 
  }

  constructor($http, ...rest) {
    super(...rest);
    this.$http = $http;
  }
  
  log() {
    super.log();
    console.log('ExtendedService.$http: ', typeof this.$http, this.$http.name);
  }
}


class LogService extends ExtendedService {
  static get $inject() { 
    return ['$log', '$timeout'].concat(ExtendedService.$inject); 
  }

  constructor($log, $timeout, ...rest) {
    super(...rest);
    this.$log = $log;
    this.$timeout = $timeout;
  }
  
  log(what) {
    super.log();
    this.$timeout(() => {
      this.$log.log('LogService.log', what);
    }, 1000);
  }
}

angular
  .module('test', [])
  .service('BaseService', BaseService)
  .service('ExtendedService', ExtendedService)
  .service('LogService', LogService)
  .run(logger)
;
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.js"></script>

<section ng-app="test"></section>


I have also opened a feature request in babel-plugin-angularjs-annotate: https://github.com/schmod/babel-plugin-angularjs-annotate/issues/28

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download