AngularJS extend service using factory and get extended scope - angularjs

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;
}

Related

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.
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'];
...
}

Angular: Child extends Parent Dependency Not Working

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!

Angularjs controller as via typescript invoke controller function from a directive

I'm trying to invoke a controller function from a directive and I run into the "this" context problem. The logSerivce is not accessible if the function gets called via directive.
Here the controller class:
class MainController implements IMainScope {
static $inject = ["LogService"];
constructor(private logService:ILogService) { }
sayHello(message:string) {
console.log("MainController sayHello", message);
// Cannot read property 'logService' of undefined if gets call from directive
this.logService.log(message);
}
}
Here the directive class:
class TestDirective implements ng.IDirective {
public restrict = "E";
public templateUrl = "test-directive.html";
public replace = true;
public scope:any = {
testFn: "&"
};
constructor() { }
public link:ng.IDirectiveLinkFn = (scope:TestDirectiveScope, element:ng.IAugmentedJQuery, attrs:ng.IAttributes):void => {
scope.hello = () => {
console.log("TestDirective", scope.firstName);
scope.testFn()(scope.firstName);
};
}
static factory():ng.IDirectiveFactory {
let directive:ng.IDirectiveFactory = () => new TestDirective();
return directive;
}
}
Here is a simple plunker example which covers my problem: http://embed.plnkr.co/Ov7crFZkkjDPzilX2BmL/
The way you are calling testFn from directive isn't correct way. To pass data with calling function you have to first use
ng-click="vm.sayHello(message)"
And while calling function from directive pass it in json/object format like {message: 'some value'} in parenthesis.
scope.testFn({message: scope.firstName});
Demo Plunkr
Methods that are supposed to be passed as callbacks (Angular binding fall into this category) should be bound to their context. For TypeScript the preferable way to do this defining a method as arrow function :
sayHello = (message:string) => { ... }

dependency injection into decorator functions in angularJS

When using angularJS you can register a decorating function for a service by using the $provide.decorator('thatService',decoratorFn).
Upon creating the service instance the $injector will pass it (the service instance) to the registered decorating function and will use the function's result as the decorated service.
Now suppose that thatService uses thatOtherService which it has injected into it.
How I can I get a reference to thatOtherService so that I will be able to use it in .myNewMethodForThatService() that my decoratorFN wants to add to thatService?
It depends on the exact usecase - more info is needed for a definitive answer.
(Unless I've misunderstood the requirements) here are two alternatives:
1) Expose ThatOtherService from ThatService:
.service('ThatService', function ThatServiceService($log, ThatOtherService) {
this._somethingElseDoer = ThatOtherService;
this.doSomething = function doSomething() {
$log.log('[SERVICE-1]: Doing something first...');
ThatOtherService.doSomethingElse();
};
})
.config(function configProvide($provide) {
$provide.decorator('ThatService', function decorateThatService($delegate, $log) {
// Let's add a new method to `ThatService`
$delegate.doSomethingNew = function doSomethingNew() {
$log.log('[SERVICE-1]: Let\'s try something new...');
// We still need to do something else afterwards, so let's use
// `ThatService`'s dependency (which is exposed as `_somethingElseDoer`)
$delegate._somethingElseDoer.doSomethingElse();
};
return $delegate;
});
});
2) Inject ThatOtherService in the decorator function:
.service('ThatService', function ThatServiceService($log, ThatOtherService) {
this.doSomething = function doSomething() {
$log.log('[SERVICE-1]: Doing something first...');
ThatOtherService.doSomethingElse();
};
})
.config(function configProvide($provide) {
$provide.decorator('ThatService', function decorateThatService($delegate, $log, ThatOtherService) {
// Let's add a new method to `ThatService`
$delegate.doSomethingNew = function doSomethingNew() {
$log.log('[SERVICE-2]: Let\'s try something new...');
// We still need to do something else afterwatds, so let's use
// the injected `ThatOtherService`
ThatOtherService.doSomethingElse();
};
return $delegate;
});
});
You can see both approaches in action in this demo.

Expose type of local TypeScript class outside factory function

I want to use a factory to manage some of my dependency injection and so I am creating a local class inside of a function. The JavaScript framework I am using (AngularJS) will inject the return value of the function into the constructor.
How can I reference the type of the returned class Record from outside the factory function?
/** Define factory to create Record classes */
export default function RecordFactory($window) {
return class Record { // I want to reference this class as a type
doSomething() {
// use $window service
}
}
}
/**
* Controller for page with returned class of RecordFactory injected
* into the constructor
*/
class RecordPageCtrl {
record
constructor(Record) { // How do I specify the type of Record here?
this.record = new Record();
}
}
// Dependency Injection is handled by AngularJS
angular.module('myApp', [])
.factory('Record', RecordFactory)
.controller('RecordPageCtrl', RecordPageCtrl)
Note: I'm trying to avoid maintaining a interface with all of the methods on the Record class.
This works for me.
namespace Factored {
// Class Record wrapped in namespace only to distinguish from other Record usages
export class Record { // I want to reference this class as a type
doSomething() {
// use $window service
}
};
}
/** Define factory to create Record classes */
export default function RecordFactory($window) {
return Factored.Record; // return class reference
}
/**
* Controller for page with returned class of RecordFactory injected
* into the constructor
*/
class RecordPageCtrl {
record: Factored.Record;
constructor(Record: typeof Factored.Record) { // Referencing namespaced class
this.record = new Record();
this.record.doSomething();
}
}
// Dependency Injection is handled by AngularJS
angular.module('myApp', [])
.factory('Record', RecordFactory)
.controller('RecordPageCtrl', RecordPageCtrl)
I've reconsidered how the factory is created and the services are injected into classes like Record. By passing the service in through the constructor, a factory can easily pass in the service and allow AngularJS to handle dependency injection.
/** Class where service needs to be injected */
class Record {
constructor(private myInjectedService) {}
doSomething() {
// use myService
this.myInjectedService.log('Hello World');
}
}
/** Define factory to create Record class instances */
function RecordFactory(MyService) {
return new Record(MyService); // return a new class
}
/**
* Controller for page
* RecordFactory return variable is injected into the constructor
*/
class RecordPageCtrl {
constructor(public Record: Record) {
this.Record.doSomething();
}
}
/** Service to inject into Record class */
class MyService {
log(message: string) {
console.log(message);
}
}
let myServiceInst = new MyService();
// directly instantiated
let factoryInstance = RecordFactory(myServiceInst);
new RecordPageCtrl(factoryInstance);
// Dependency Injection handled by AngularJS
angular.module('myApp', [])
.factory('Record', RecordFactory)
.service('MyService', MyService)
.controller('RecordPageCtrl', RecordPageCtrl)
Note, if you want to be able to create multiple instances of the Record class where the factory is being injected, you will need to have the factory return another factory which instantiates the function when called.

Resources