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.
Two things to keep in mind:
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...)
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
In code above super requires arguments to be specified explicitly.
A more failproof way is to do all dependency assignments in current class:
constructor($q, $http){
super();
this.$q = $q;
this.$http = $http;
}
This can create problems if these services are used in parent constructor. It's not that easy to test arguments of parent constructor because this involves module mocks. A simple and relatively reliable way to test this is to assert:
expect(service.$q).toBe($q);
expect(service.$http).toBe($http);
This should be done in any Angular unit test, in fact, even if a class wasn't inherited.
A better way is to introduce base class that handles DI, considering that all that #ngInject does is creating $inject annotation:
class Base {
constructor(...deps) {
this.constructor.$inject.forEach((dep, i) => {
this[dep] = deps[i];
}
}
}
BaseService.$inject = [];
class myService extends Base {
/*#ngInject*/
constructor($q){
super(...arguments);
...
}
...
}
At this point it becomes obvious that #ngInject doesn't really help anymore and requires to mess with arguments. Without #ngInject, it becomes:
class myService extends Base {
static get $inject() {
return ['$q'];
}
constructor(...deps){
super(...deps);
...
}
...
}
If dependency assignments are the only things that are done in child constructor, a constructor can be efficiently omitted:
class myService extends Base {
static get $inject() {
return ['$q'];
}
...
}
It's even neater with class fields and Babel/TypeScript (no native support in browsers):
class myService extends Base {
static $inject = ['$q'];
...
}
Related
I have a child service that I want to extend from a parent service using the extends keyword. I'm having some trouble injecting another service MyService into the child service.
export class ParentService {
constructor($http) {}
get() {
this.$http.get('/someUrl').then(res => res.data);
}
}
export class ChildService extends ParentService {
constructor($http, private MyService) {
super($http);
}
get() {
const data = super.get();
return this.MyService.cleanData(data);
}
}
For some reason MyService is coming back as undefined in the ChildService, and I can only assume something is going wrong with the DI. If I remove the extends keyword however, MyService works as expected.
Any idea what might be going on here. Any help is appreciated. Thanks in advance!
Try renaming the get() methods within the ParentService and ChildService to anything else.
get is a keyword/binding-syntax that binds an object property to a function that is called when that property is looked up. The compiler is expecting a property or function name after the get and may be getting tripped up. Even in Plunkr get is highlighted by the IDE indicating a reserved word/syntax.
get could be used like this with ES6 classes:
class Todo {
constructor(task) {
this.task = task;
}
get task() {
return this.task.toUpperCase();
}
}
The get syntax binds an object property to a function that will be
called when that property is looked up.
Try changing get to getData or whatever you need.
export class ParentService {
constructor($http) {}
getData() {
return this.$http.get('/someUrl');
}
}
export class ChildService extends ParentService {
constructor($http, private MyService) {
super($http);
}
getData() {
return super.getData()
.then(data => this.MyService.cleanData(data))
.catch(error => console.log(error));
}
}
Here is a plunker demonstrating the functionality at a very basic level including a ParentService, ChildService, and a MyService, with MyService being injected into ChildService and ChildService extending ParentService. A console.log() is being executed in this example within the constructor() of the child to show that DI is occurring properly.
Hopefully this helps!
I want to extend a service to override some method and add some method too.
But, I can not get a scope in child service. I think the parent service is not injected to child class as well, I mean it seems like just an object, not instance.
Here is my example,
Parent service,
export class ParentService implements IModalMixin {
constructor(modalMixin: IModalMixin,
private _: _.LoDashStatic) {
}
doSomething() {}
parentMethod() {
return 'Okay';
}
}
Child service,
/* #ngInject */
export function childServiceFactory(ParentService: any) {
var extended = angular.extend(ParentService, {});
// override parent method
extended.doSomething = () => {
return this.parentMethod(); // error this.parentMethod is not a function
}
return extended;
}
// and load to module
module
.service('ParentService', MyService)
.factory('ChildService', childServiceFactory)
Please advise me what I'm doing wrong?
My angular version is 1.4.3.
You should not use arrow function in this case, otherwise context this is not extended object.
Normal function would work:
/* #ngInject */
export function childServiceFactory(ParentService: any) {
var extended = angular.extend(ParentService, {});
// override parent method
extended.doSomething = function () {
return this.parentMethod();
}
return extended;
}
Looking at the answer here: https://stackoverflow.com/a/19272093/2547709
Using the $inject syntax my controller ends up looking like this:
class MyCtrl {
public static $inject: string[] = ['$scope'];
constructor($scope){
// stuff
}
}
// register the controller
app.controller("MyCtrl", MyCtrl);
My question is- what happens if I want to pass my own custom arguments to the constructor as well as any injected variables?:
class MyCtrl {
public static $inject: string[] = ['$scope'];
constructor($scope, customArg){
// stuff
}
}
// Now how do I pass customArg in without it complaining?
app.controller("MyCtrl", MyCtrl(customArg)); // Nope
I feel like I'm missing something fundamental, using this syntax, does everything you pass in to the .controller() function have to be registered with angular and so I shouldn't be trying to pass in custom arguments at all? Or can I pass in an arbitrary value/object? And if so how?
customArg
You cannot pass in custom argument if angular is going to call the constructor. You can however register other things with Angular e.g. Services,Factories,Values(constants) that angular will pass to the controller for you.
More : https://www.youtube.com/watch?v=Yis8m3BdnEM&hd=1
Sorry for the answer I don't have enough points to comment.
I have the exact same scenario and here is my situation:
export abstract class DataService<T> {
static $inject = ['$resource'];
private svc: ng.resource.IResourceClass<ng.resource.IResource<T>>;
constructor(
protected $resource: ng.resource.IResourceService, url: string
) {
this.svc = <ng.resource.IResourceClass<ng.resource.IResource<T>>>this.$resource(url, { id: '#id' });
}
public get(id: number): ng.IPromise<T> {
return this.svc.get({ id: id }).$promise;
}
}
export class MyDataService
extends DataService<IItem> {
// big problem here!!!
constructor(
) {
super("/api/items/:id");
}
}
Looks like I would have to repeat the injection on every derived class and also pass in the super... so redundant
I am trying to understand how to bring a service into a controller when the service is used in both a parent and child controller and the parent and child HTML. Should I bring the service into the parent and then get a copy of that service from the scope in the child like this:
class AppController {
static $inject = [
'$scope',
'configService'
]
constructor(
public $scope: IAppControllerScope,
public config: IConfigService
) {
$scope.app = this;
$scope.config = config;
}
// this.config is used here
}
class AdminHomeController {
public app: AppController;
public config: ConfigService;
static $inject = [
'$scope'
];
constructor(
private $scope: IAdminHomeControllerScope
) {
this.app = $scope.app;
this.config = $scope.config;
$scope.home = this;
}
// this.config is used here
}
Or should I bring the service into both the parent and child controllers like this:
class AppController {
static $inject = [
'$scope',
'configService'
]
constructor(
public $scope: IAppControllerScope,
public config: IConfigService
) {
$scope.app = this;
$scope.config = config;
}
// this.config is used here
}
class AdminHomeController {
public app: AppController;
static $inject = [
'$scope',
'configService'
];
constructor(
public $scope: IAdminHomeControllerScope,
public config: IConfigService
) {
this.app = $scope.app;
$scope.home = this;
}
// this.config is used here
}
I would appreciate any advice and suggestions on which would be the best way to do this and if there are differences. One more question. Should the parameters on my constructors be declared as private or public?
It's your choice really, however you prefer to do it, but the one argument for including it in each controller directly is that you have no dependencies on anything else.
The issue with having it included by the AppController and inherited, is that if for whatever reason one developer decided "we don't kneed this in here anymore", you would then have all other controllers that assume the service is there break. - #basarat tells me that TypeScript would in fact simply fail to compile, but this is obviously still more of a problem than there not being one. In this scenario, you'd either have to put it back, or go into every controller that gave a compilation error and inject it into them, so you'd end up back at the first solution anyway.
By including it directly into each and every controller, it makes no difference to the rest of the application if you remove it from one of them. It just keeps things a bit safer.
If you really want to just include it once and use it everywhere, you should be able to use the angular run method to inject it into the rootScope when the app runs. You could do something like the following.
myApp.run(function($rootScope, myService) {
$rootScope.myService = myService;
})
// For declaring that myService exists on all instances of $scope:
module ng{
export interface IScope{
myService: MyService;
}
}
I find dependency injection for AngularJS services in TypeScript to be somewhat cumbersome. Currently, I define a factory method inside my service class, and have to repeat all dependency injection arguments three times:
class MyService {
public static Factory($rootScope, myController) { // 1st time
return new MyService($rootScope, myController); // 2nd time
}
constructor(public $rootScope, public myController) {} // 3rd time
}
myModule.factory('myService', MyService.Factory);
I would like to do the following, but that does not seem to work:
class MyService {
constructor(public $rootScope, public myController) {} // only once
}
myModule.factory('myService', MyService);
This approach works fine for controllers, but not so for services. Is there a better way?
Thanks in advance!
You should user service not factory :
class MyService {
constructor(public $rootScope) {} // only once
}
myModule.service('myService', MyService);
You could simply use angular's injector to create your controller instead of having a factory method.
Here is a sample in typescript
/// <reference path='d.ts/DefinitelyTyped/angularjs/angular.d.ts' />
class MyCtrl {
public phrase: string;
constructor($window) {
this.phrase = 'I was loaded by injector';
}
speak() {
alert(this.phrase);
}
}
function main() {
var injector = angular.injector(['ng']);
var ctrl = injector.instantiate(MyCtrl);
ctrl.speak();
}
And a fiddler to prove it works: http://jsfiddle.net/UPv5j/3/