I set up a class with a bunch of static utility functions.
// utils.ts
class Utils {
public static blank(anything) {
if (_.isNil(anything) || anything === "") {
return true;
}
return false;
};
// ...
}
window.Utils = Utils;
I inject this into my angular app as a constant:
angular.module("myApp", []).constant("Utils", window.Utils);
Then I can use it in my controllers or in a provider:
class MyCtrl {
constructor(private $scope, private Utils: Utils, private MyService: MyService) { }
// ...
}
angular.module("myApp").controller("MyCtrl", MyCtrl);
However, when I compile I get this error:
javascripts/admin/controllers/my_ctrl.ts(6,29): error TS2339:
Property 'blank' does not exist on type 'Utils'.
How can I get the correct type information on static methods when injecting a constant like this into an angular controller?
The correct type of injected private Utils is not Utils, which would imply an instantiated instance of the class Utils.
The correct type is typeof Utils, which tells TypeScript that private Utils is the class itself (or rather, something with the same type as the class Utils), not an instance of the class.
You will also need to use another name for the type Utils or the parameter Utils. The unary expression passed for TypeScripts typeof will include the parameters in its scope, so simply changing :Utils to :typeof Utils will cause error about circular reference.
Something like this should do the trick
constructor(private $scope, private Utils: typeof window.Utils, private MyService: MyService){ }
Related
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'];
...
}
I'm getting a TypeError: this.$resource is not a function. This is my code
export class DataAccessService
implements IDataAccessService {
static $inject = ["$resource"];
constructor(private $resource: ng.resource.IResourceService) {
}
getTravelExpenseType(): ng.resource.IResourceClass<T> {
return this.$resource('URL:id', {}, {});
}
}
common.service("dataAccessService",
[DataAccessService]);
common.service("dataAccessService",
[DataAccessService]);
The problem is that you're passing an array as your service definition. When you do that, you're supposed to list the dependencies to inject in the array first, and your constructor as the last element. Since you're not listing any dependencies to inject, nothing gets injected.
Just get rid of the array:
common.service("dataAccessService", DataAccessService);
Trying to inject angularjs' $filter service into the directive, with typescript :
full code
export class MyDirective implements ng.IDirective {
public static $inject: string[] = ["$filter"];
constructor (public $filter: ng.IFilterService) {
var result = this.$filter('orderBy')([{"price":1}], ["price"])
return this;
}
}
Got error:
angular.js:11655 TypeError: this.$filter is not a function
why $filter is undefined?
Try :
var result =$filter('orderBy')([{"price":1}], ["price"])
$filter is a parameter of your constructor... it is not yet a member of your class. If I replace 'this.$filter' by '$filter' it works.
If you want to use this.$filter you will need to add it to your class and same the argument to the member '$filter':
public static $inject: string[] = ["$filter"];
$filter: ng.IFilterService;
constructor (public $filterParameter: ng.IFilterService) {
this.$filter = $filterParameter;
var result = this.$filter('orderBy')([{"price":1}], ["price"])
return this;
}
I change the parameter in the constructor to try to clarify to $filterParameter.
I proposed the change on your full code on github. Let us know if that helps.
You can change the constructor signature to
constructor (private $filter: ng.IFilterService) {
which would automatically make the filter a class property and this. reference should work from that point on.
Ok. the fix I've just done by following these recomondations: https://github.com/dilumich/style_typescript_angularjs
removed the line:public static $inject: string[] = ["$filter"];
added the factory to the directive:
static factory(): ng.IDirectiveFactory {
var directive = (filterService: ng.IFilterService) => new OfferingsTableDirective(filterService); directive.$inject = ['$filter'];
return directive;
}
then registered it in my sample app like:
angular.module('app.directives').directive('offeringsTableDirective', OfferingsTableDirective.factory());
Then $filter became functional - not undefined.
I want to cast an angular scope variable to a type:
It might look something like this:
app.controller( 'PageLayoutController', function ( $scope )
{
// Scope properties
(PageMap)$scope.PageMap;
or
app.controller( 'PageLayoutController', function ( $scope )
{
// Scope properties
$scope.PageMap: PageMap;
Is it possible or must i declare a separate variable?:
var pageMap: PageMap;
$scope.PageMap = pageMap;
This is for typeahead sake.
You can do it this way, create a type deriving from IScope and add your properties to it. Example:
interface PageLayoutVM extends ng.IScope{
PageMap : PageMap;
}
and use it
app.controller( 'PageLayoutController', function ( $scope:PageLayoutVM)
You will get ng.IScope only if you have angular.d.ts typings used in your application.
Also to add on, with the usage of TypeScript it is much easier to use it with controllerAs syntax and defining properties on the controller itself rather than using scope to set view model properties.
Something like:
export interface PageLayoutVM {
PageMap : PageMap;
someChangehandler:(val:string)=>void;
}
class PageLayoutController implements PageLayoutVM {
PageMap : PageMap; //public is implicit
static $inject = ['$scope', 'myService']; //Just for demo if you are using scope specific methods like watch
constructor(private $scope:ng.IScope,
private myService:IMyService){
}
someChangehandler(val:string){ //Another public method
this.myService.doSomething(val);
}
private doSomePrivateStuff(){
//Do stuff...
}
}
With this when you write unit tests for your controller, you can write the test in TS itself with the type definitions it will be easy to lookup controller properties while writing tests.
Ideally you should define an interface and declare $scope with it:
interface IPageLayoutScope extends ng.IScope {
PageMap: PageMap;
}
app.controller( 'PageLayoutController', function ( $scope: IPageLayoutScope)
That way $scope.PageMap will be defined already.
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