AngularJS Service in Typescript - javascript

I have an AngularJS service written in JavaScript and i would like to convert this to TypeScript. The Javascript version returns a function immediately. Why would it do this and how would I implement this in TypeScript?
JavaScript
Geisha.service("loginModal", ["$uibModal", "$rootScope", function ($uibModal: angular.ui.bootstrap.IModalService, $rootScope: ICustomRootScopeService) {
function assignCurrentUser(user: User) {
$rootScope.currentUser = user;
return user;
}
return function () {
var instance = $uibModal.open({
templateUrl: "/Angular/Security/Views/loginTemplate.html",
controller: "loginModalCtrl"
});
return instance.result.then(assignCurrentUser);
}}]);
TypeScript
In TypeScript I have only gone as far as creating the constructor and one of the methods but not sure how to implement the immediate return function
module G.Services {
export class LoginModal {
$rootScope: ICustomRootScopeService;
$uibModal: angular.ui.bootstrap.IModalService;
static $inject = ["$rootScope", "$uibModal"];
constructor($rootScope: ICustomRootScopeService, $uibModal: angular.ui.bootstrap.IModalService) {
this.$rootScope = $rootScope;
this.$uibModal = $uibModal;
}
assignCurrentUser(user: User): User {
this.$rootScope.currentUser = user;
return user;
}
}
Geisha.service("loginModal", LoginModal);}

How to implement the immediate return function
In your case you have a service that is just a function. If you use a class it will be something that has functions. Better if you just use function as you had.
Note
If you are willing to do the refactoring have loginModal.assignCurrentUser(...stuff...) elsewhere instead of loginModal(...stuff...)

Related

Using dependency injection with typescript in AngularJs

I'm converting a component (AngularJS 1.6) from javascript to typescript.
class NewProjectCtrl {
price: number;
static $inject = ['$http'];
constructor($http) {
let ctrl = this;
ctrl.price = '50';
...
}
createProject() {
$http.post('/api/project', ctrl.project)
...
}
}
angular
.module('app.projects-selection')
.component('newProject', {
templateUrl: 'app/projects-selection/components/new-project/new-project.tmpl.html',
controller: NewProjectCtrl,
bindings: {
user: '<',
}
});
Typescript complains on the $http in my createProject method (Cannot find name $http). All the examples I can find only uses dependency injections in the constructor.
You've forgot this. After injecting use this.$http
Also let me suggest you some approach according to styleguides:
namespace myNameSpace {
class NewProjectCtrl {
private static $inject = ['$http'];
public price: number;
constructor(private $http) {}
createProject() {
this.$http.post('/api/project', ctrl.project)
}
}
class NewProjectComponent implements ng.IComponentOptions {
constructor() {
this.bindings: {
user: '<',
};
this.templateUrl: 'app/projects-selection/components/new-project/new-project.tmpl.html';
this.controller: NewProjectCtrl;
}
}
angular
.module('app.projects-selection', [])
.component('newProject', new NewProjectCtrl());
}
You're creating a class, not a mere function. $http is injected into your constructor function and exists there, but it does not exist in createProject. You need to store it on the instance as property:
private $http; // should be declared first too, preferably with type hint
constructor($http) {
this.$http = $http;
}
createProject() {
this.$http.post('/api/project', ctrl.project)
}
TypeScript has a shorthand for that:
constructor(private $http) {
}
createProject() {
this.$http.post('/api/project', ctrl.project)
}
let ctrl = this also seems rather weird, I don't think that makes a lot of sense. I don't know what else you're doing in the constructor, but I'd refactor what you have to this:
class NewProjectCtrl {
price: number = 50;
static $inject = ['$http'];
constructor(private $http) { }
createProject() {
this.$http.post('/api/project', ctrl.project);
}
}

AngularJS tests - inject -> module -> inject

I'm trying to test a service documentViewer that depends on some other service authService
angular
.module('someModule')
.service('documentViewer', DocumentViewer);
/* #ngInject */
function DocumentViewer($q, authService) {
// ...
this.view = function(doc) {
//...
}
}
This is what my test looks like at the moment
it('test', inject(function($q) {
var doc = {
view: function() {
return $q.resolve(0);
}
};
var auth = {
refreshProfileData: function() {
return $q.resolve(0);
},
};
var viewer = createViewer(auth);
}));
function createViewer(auth) {
var viewer;
module({
authService: auth
});
inject(function(documentViewer) {
viewer = documentViewer;
});
return viewer;
}
The problem is I need to call inject to grab a $q, then use it to create my mocks, register my mocks with module, and then call inject again to grab the unit under test.
This results in
Error: Injector already created, can not register a module! in bower_components/angular-mocks/angular-mocks.js (line 2278)
I've seen lots of answers here on SO saying you can't call module after inject, but they don't offer any alternative to a scenario like the above.
What's the correct approach here?
PS: I'd like to avoid using beforeEach, I want each test to be self-contained.
module is used to define which modules will be loaded with inject and cannot be called after inject, this is chicken-egg situation.
The object accepted by module is used to define mocked services with $provide.value:
If an object literal is passed each key-value pair will be registered on the module via $provide.value, the key being the string name (or token) to associate with the value on the injector.
There can be no more than 1 function like createViewer that calls both module and inject. If this means that this kind of self-contained test is an antipattern, there is nothing that can be done about that. Angular testing works best with usual habits, including beforeEach and local variables.
In order to eliminate the dependency on $q, mocked service can be made a factory.
it('test', function () {
var authFactory = function ($q) {
return {
refreshProfileData: function() {
return $q.resolve(0);
},
};
};
// mocks defined first
module(function ($provide) {
$provide.factory('authService': authFactory);
});
var viewer;
inject(function(documentViewer) {
viewer = documentViewer;
});
// no module(...) is allowed after this point
var $q;
inject(function(_$q_) {
$q = _$q_;
});
var doc = {
view: function() {
return $q.resolve(0);
}
};
});

How do I make services available outside of constructor in angular?

I have the following problem. When I have a class in ionic with angular I can access angular services in the constructor after injecting them:
export class HomeController {
static $inject = [
'$scope',
'$state'
];
constructor(
public $scope : any,
public $state : any
) {
this.$state.go('test');
this.$scope.changeController = this.changeController;
}
changeController() {
console.log("change controller");
};
}
However when I change it to the change controller function, it doesn't work
export class HomeController {
static $inject = [
'$scope',
'$state'
];
constructor(
public $scope : any,
public $state : any
) {
this.$scope.changeController = this.changeController;
}
changeController() {
console.log("change controller");
this.$state.go('test'); // Fails here
};
}
This is the error:
Error: undefined is not an object (evaluating 'this.$state.go')
What can I do?
On top of that, is it correct add the changeController function to the scope or is there an easier method to make it available to a template?
Thx in advance
In your case the value of this is incorrect which is a very common issue due to misunderstanding of ES6 classes. Try using lambda to have lexical scoping:
changeController = () => {
console.log("change controller");
this.$state.go('test'); // Fails here
};

Angular ui-router resolve value as string

With ui-router, I add all resolve logic in state function like this;
//my-ctrl.js
var MyCtrl = function($scope, customers) {
$scope.customers = customers;
}
//routing.js
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: { // <-- I feel this must define as like controller
customers: function(Customer, $stateParams) {
return Customer.get($stateParams.id);
}
}
});
However IMO, resolve object must belong to a controller, and it's easy to read and maintain if it is defined within a controller file.
//my-ctrl.js
var MyCtrl = function($scope, customers) {
$scope.customers = customers;
}
MyCtrl.resolve = {
customers: function(Customer, $stateParams) {
return Customer.get($stateParams.id);
};
};
//routing.js
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: 'MyCtrl.resolve' //<--- Error: 'invocables' must be an object.
});
However, When I define it as MyCtrl.resolve, because of IIFE, I get the following error.
Failed to instantiate module due to: ReferenceError: MyCtrl is not defined
When I define that one as string 'MyCtrl.resolve', I get this
Error: 'invocables' must be an object.
I see that controller is defined as string, so I think it's also possible to provide the value as string by using a decorator or something.
Has anyone done this approach? So that I can keep my routings.js clean and putting relevant info. in a relevant file?
It sounds like a neat way to build the resolve, but I just don't think you can do it.
Aside from the fact that "resolve" requires an object, it is defined in a phase where all you have available are providers. At this time, the controller doesn't even exist yet.
Even worse, though, the "resolve" is meant to define inputs to the controller, itself. To define the resolve in the controller, then expect it to be evaluated before the controller is created is a circular dependency.
In the past, I have defined resolve functions outside of the $stateProvider definition, at least allowing them to be reused. I never tried to get any fancier than that.
var customerResolve = ['Customer', '$stateParams',
function(Customer, $stateParams) {
return Customer.get($stateParams.id);
}
];
// ....
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: {
customers: customerResolve
}
});
This question is about features of ui-router package. By default ui-router doesn't support strings for resolve parameter. But if you look at the source code of ui-router you will see, that it's possible to implement this functionality without making direct changes to it's code.
Now, I will show the logic behind suggested method and it's implementation
Analyzing the code
First let's take a look at $state.transitionTo function angular-ui-router/src/urlRouter.js. Inside that function we will see this code
for (var l = keep; l < toPath.length; l++, state = toPath[l]) {
locals = toLocals[l] = inherit(locals);
resolved = resolveState(state, toParams, state === to, resolved, locals, options);
}
Obviously this is where "resolve" parameters are resolved for every parent state. Next, let's take a look at resolveState function at the same file. We will find this line there:
dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state);
var promises = [dst.resolve.then(function (globals) {
dst.globals = globals;
})];
This is specifically where promises for resolve parameters are retrieved. What's good for use, the function that does this is taken out to a separate service. This means we can hook and alter it's behavior with decorator.
For reference the implementation of $resolve is in angular-ui-router/src/resolve.js file
Implementing the hook
The signature for resolve function of $resolve is
this.resolve = function (invocables, locals, parent, self) {
Where "invocables" is the object from our declaration of state. So we need to check if "invocables" is string. And if it is we will get a controller function by string and invoke function after "." character
//1.1 Main hook for $resolve
$provide.decorator('$resolve', ['$delegate', '$window', function ($delegate, $window){
var service = $delegate;
var oldResolve = service.resolve;
service.resolve = function(invocables, locals, parent, self){
if (typeof(invocables) == 'string') {
var resolveStrs = invocables.split('.');
var controllerName = resolveStrs[0];
var methodName = resolveStrs[1];
//By default the $controller service saves controller functions on window objec
var controllerFunc = $window[controllerName];
var controllerResolveObj = controllerFunc[methodName]();
return oldResolve.apply(this, [controllerResolveObj, locals, parent, self]);
} else {
return oldResolve.apply(this, [invocables, locals, parent, self]);
}
};
return $delegate;
}]);
EDIT:
You can also override $controllerProvider with provider like this:
app.provider("$controller", function () {
}
This way it becomes possible to add a new function getConstructor, that will return controller constructor by name. And so you will avoid using $window object in the hook:
$provide.decorator('$resolve', ['$delegate', function ($delegate){
var service = $delegate;
var oldResolve = service.resolve;
service.resolve = function(invocables, locals, parent, self){
if (typeof(invocables) == 'string') {
var resolveStrs = invocables.split('.');
var controllerName = resolveStrs[0];
var methodName = resolveStrs[1];
var controllerFunc = $controllerProvider.getConstructor(controllerName);
var controllerResolveObj = controllerFunc[methodName]();
return oldResolve.apply(this, [controllerResolveObj, locals, parent, self]);
} else {
return oldResolve.apply(this, [invocables, locals, parent, self]);
}
};
Full code demonstrating this method http://plnkr.co/edit/f3dCSLn14pkul7BzrMvH?p=preview
You need to make sure the controller is within the same closure as the state config. This doesn't mean they need to be defined in the same file.
So instead of a string, use a the static property of the controller:
resolve: MyCtrl.resolve,
Update
Then for your Controller file:
var MyCtrl;
(function(MyCtrl, yourModule) {
MyCtrl = function() { // your contructor function}
MyCtrl.resolve = { // your resolve object }
yourModule.controller('MyCtrl', MyCtrl);
})(MyCtrl, yourModule)
And then when you define your states in another file, that is included or concatenated or required after the controller file:
(function(MyCtrl, yourModule) {
configStates.$inject = ['$stateProvider'];
function configStates($stateProvider) {
// state config has access to MyCtrl.resolve
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: MyCtrl.resolve
});
}
yourModule.config(configStates);
})(MyCtrl, yourModule);
For production code you will still want to wrap all these IIFEs within another IIFEs. Gulp or Grunt can do this for you.
If the intention is to have the resolver in the same file as the controller, the simplest way to do so is to declare the resolver at the controller file as a function:
//my-ctrl.js
var MyCtrl = function($scope, customers) {
$scope.customers = customers;
}
var resolverMyCtrl_customers = (['Customer','$stateParams', function(Customer, $stateParams) {
return Customer.get($stateParams.id);
}]);
//routing.js
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: resolverMyCtrl_customers
});
This should work.
//my-ctrl.js
var MyCtrl = function($scope, customer) {
$scope.customer = customer;
};
//routing.js
$stateProvider
.state('customers.show', {
url: '/customers/:id',
template: template,
resolve: {
customer: function(CustomerService, $stateParams){
return CustomerService.get($stateParams.id)
}
},
controller: 'MyCtrl'
});
//service.js
function CustomerService() {
var _customers = {};
this.get = function (id) {
return _customers[id];
};
}

AngularJs + typescript directives dependency injection

I trie to implement a new directive in my app. Directive's code :
module Myapp.NV.Directives {
export interface placeHolderScope extends ng.IScope {
txt: string;
}
/**
* PlaceHolder
*
* #class
* #classdesc This directive is use to simulate placeholder HTML5 attributes
*/
export class PlaceHolder implements IDirective {
static $inject = ['$log','$timeout'];
constructor($log: ng.ILogService, $timeout: ng.ITimeoutService) {
var txt;
var directive: any = {
restrict: "A",
scope: { txt: "#ngPlaceholder" },
link: function (scope: placeHolderScope, elem: ng.IAugmentedJQuery, attrs: ng.IAttributes, $log: ng.ILogService, $timeout: ng.ITimeoutService) {
console.log($log);
console.log($timeout);
}
}
return directive;
}
}
}
Myapp.NV.registerDirective('PlaceHolder', ['$log', '$timeout']);
My probleme is log and timeout are always undefined...
static $inject = ['$log','$timeout'];
Won't work...
The code for registerDirective function :
export function registerDirective(className: string, services = []) {
var directive = className[0].toLowerCase() + className.slice(1);
services.push(() => new Myapp.NV.Directives[className]());
angular.module('Myapp.NV.Directives').directive(directive, services);
}
Thanks for help me :)
As boindill points out in the original answer, dependencies are injected through the TypeScript constructor, not through the link function.
This is my solution, myDirective depends on myService:
export class MyDirective implements ng.IDirective {
static $inject = ["myService"];
constructor(private myService: IMyService) {
}
restrict = "A";
scope = {};
link = (scope: IMyScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes) => {
// directive implementation
// Access myService through 'this.myService'.
};
}
Register the directive in Angular the following way. Here ng.IDirectiveFactory is implemented as an anonymous function:
angular.module("app", []).directive("myDirective", (myService: IMyService) => {
return new MyDirective(myService);
});
Here the Angular dependency injection works because it recognizes the constructor parameter names (myService).
To make sure dependency injection still recognizes the dependencies when the generated JavaScript is minified the static $inject property provides their names in a string array. Just as with plain JavaScript, make sure the constructor parameters and the $inject array members are in the same order.
You cannot inject dependencies in the link function of a directive. The link function has a fixed signature function link(scope, element, attrs) { ... } this is taken from the official angularjs documentation.
What you is to define a controller for your directive and inject the dependencies into the controller. Afterwards just use this controller function.

Categories