I am rewriting my AngularJS application using TypeScript. In my application I have used $scope to define variables and methods:
defectResource.defectByDefectId($scope.defectid).then(function (data) {
$scope.defect = data;
});
$scope.postDefect = function () {
defectResource.postDefect($scope.defect).then(function () {
$location.path('defects/' + $stateParams.itemid);
});
};
I can rewrite my controller something like this:
interface IDefectDetailModel {
}
class DefectDetailController implements IDefectDetailModel {
static $inject = ['$scope', '$stateParams', '$location', 'defectResource', 'entityProvider'];
constructor($scope: any, $stateParams: any, $location: any, defectResource: any, entityProvider: any) {
defectResource.defectByDefectId($scope.defectid).then(function (data) {
$scope.defect = data;
});
$scope.postDefect = function () {
defectResource.postDefect($scope.defect).then(function () {
$location.path('defects/' + $stateParams.itemid);
});
};
}
}
But if I understood correctly it is not good way. I need to create Interface and TypeScript class which will implement this interface. All my variables must be class variables (so $scope.defectid must be this.defectid) and all my $scope methods must be class methods (this.postDefect()).
Do I understood correctly? If I am using AngularJS with TypeScript the best way is not use $scope variables and methods and use only instance variables and methods (using this) ?
If I am using AngularJS with TypeScript the best way is not use $scope variables and methods and use only instance variables and methods (using this) ?
+100.
Key reason being that scope variables are susceptible to become disconnected due to angular's scope inheritance mechanism. Even without TypeScript it is recommended not to use scope varaibles and this is why the controller as syntax was created.
More on Angular controllers in typescript: https://www.youtube.com/watch?v=WdtVn_8K17E
Related
i am little bit familiar with angular. still on learning process. working with ng version 1.4.8. so i like to know how could i define constructor function in service and factory.
here is one sample service .now tell me how to define constructor in service or factory ?
angular.module('myApp').service('helloService',function($timeout){
this.sayHello=function(name){
$timeout(function(){
alert('Hello '+name);
},2000);
}
});
angular.module('myApp').controller('TestController',
function(helloService){
helloService.sayHello('AngularJS'); // Alerts Hello AngularJS
});
The function you pass to .service gets called with new, thus it is already basically a constructor. It is a "constructor function" and it implicitly returns an object, which is the singleton:
angular.module('myApp').service('helloService',function($timeout){
// This constructor function implicitly returns an object. It is called
// only once by Angular to create a singleton.
this.sayHello = function(name) {
// etc
}
});
Just to illustrate, if you passed an ES6 class to .service (which does have a constructor) instead of a constructor function, that constructor would be called when the singleton is created:
class HelloService {
constructor($timeout) {
// Called once when the singleton is created
}
sayHello(name) {
// etc
}
}
angular.module('myApp').service('helloService', HelloService);
Using .factory is similar, but it doesn't get called with new. So the function you use in this case has to return a singleton object explicitly:
angular.module('myApp').factory('helloService',function($timeout){
// This factory function explicitly returns an object. It is called
// only once by Angular to create a singleton.
return {
sayHello: function(name) {
// etc
}
};
});
Edit: As mentioned by #Vladimir Zdenek, these "constructors" cannot be used to configure the singleton externally. However, I interpreted the question to mean "Where can I put code that runs when the singleton is created?". Singletons may need to initialize data, so that initialization can go in the "constructor".
There is (probably in most cases) no need for a constructor when it comes to singletons. To require such a thing might be just pointing to a bad architectural design of your application.
That said, you can make a global configuration available for your service/factory by using a provider. You can find more on providers in the Official Documentation.
If you do not need a singleton and you wish to create a reusable piece of code, you can use something (in JavaScript) known as factory functions. You can see an example of such function below.
function MyFactory(myParams) {
const Factory = {
// Properties
myProperty: myParams.myProperty,
// Methods
getMyProperty: getMyProperty
};
return Factory;
function getMyProperty() {
return Factory.myProperty;
}
}
// usage
const myObj = MyFactory({ myProperty: 'Hello' });
Same issue as here: AngularJS directive binding a function with multiple arguments
Following this answer: https://stackoverflow.com/a/26244600/2892106
In as small of a nutshell as I can.
This works:
<my-directive on-save="ctrl.saveFn"></my-directive>
With:
angular.module('app')
.controller('MyController', function($scope) {
var vm = this;
vm.saveFn = function(value) { vm.doSomethingWithValue(value); }
});
But when I convert to Typescript and use real classes, this breaks.
class MyController {
constructor() {}
saveFn(value) {
this.doSomethingWithValue(value);
}
}
In the Typescript version when debugging, "this" is referencing the Window global. So my scope is messed up somehow, but I don't know how to fix it. How can I get "this" to refer to MyController as expected?
So my scope is messed up somehow
Use an arrow function instead of a method.
Fixed
class MyController {
constructor() {}
saveFn = (value) => {
this.doSomethingWithValue(value);
}
}
More
https://basarat.gitbooks.io/typescript/content/docs/arrow-functions.html
Our Angular project moved to ES6 and Reduxjs, and now I am struggling to get controller unit tests working. Specifically, I cant seem to mock correctly when it comes to the class constructor. From what i have researched, i cant spyOn an ES6 class constructor, so i need to mock its dependencies and also accommodate the binding to lexical 'this' that ngRedux.connect() facilitates.
My test makes it to the connect function in the constructor, and then gives me the error: "'connect' is not a function"
I think i may have several things wrong here. If i comment out the connect line in the constructor, it'll get to my runOnLoad function and the error will tell me that fromMyActions isnt a function. this is because the redux connect function binds the actions to 'this', so given these issues, I take it I cant mock redux unless i provide its implementation. any advice? I am relatively new to angular as well - and my weakest area is unit testing and DI.
Here is my module and controller:
export const myControllerModule = angular.module('my-controller-module',[]);
export class MyController {
constructor($ngRedux, $scope) {
'ngInject';
this.ngRedux = $ngRedux;
const unsubscribe = this.ngRedux.connect(this.mapState.bind(this), myActions)(this);
$scope.$on('$destroy', unsubscribe);
this.runOnLoad();
}
mapState(state) {
return {};
}
runOnLoad() {
this.fromMyActions(this.prop);
}
}
myControllerModule
.controller(controllerId, MyController
.directive('myDirective', () => {
return {
restrict: 'E',
controllerAs: 'vm',
controller: controllerId,
templateUrl: htmltemplate
bindToController: true,
scope: {
data: '=',
person: '='
}
};
});
export default myControllerModule.name;
and my test:
import {myControllerModule,MyController} from './myController';
import 'angular-mocks/angular-mocks';
describe('test', () => {
let controller, scope;
beforeEach(function() {
let reduxFuncs = {
connect: function(){}
}
angular.mock.module('my-controller-module', function ($provide) {
$provide.constant('$ngRedux',reduxFuncs);
});
angular.mock.inject(function (_$ngRedux_, _$controller_, _$rootScope_) {
scope = _$rootScope_.$new();
redux = _$ngRedux_;
var scopeData = {
data : {"test":"stuff"},
person : {"name":"thatDude"}
} ;
scope.$digest();
controller = _$controller_(MyController, {
$scope: scope,
$ngRedux: redux
}, scopeData);
});
});
});
The idea behind Redux is that most of your controllers have no, or very little logic. The logic will be in action creators, reducers and selectors mostly.
In the example you provide, most of your code is just wiring things.
I personally don't test wiring, because it adds very little value, and those kinds of test are generally very brittle.
With that said, if you want to test your controllers nonetheless you have two options:
Use functions instead of classes for controllers. For most controllers using a class adds no real value. Instead use a function, and isolate the logic you want to test in another pure function. You can then test this function without even needing mocks etc.
If you still want to use classes, you will need to use a stub of ng-redux, (something like this: https://gist.github.com/wbuchwalter/d1448395f0dee9212b70 (it's in TypeScript))
And use it like this:
let myState = {
someProp: 'someValue'
}
let ngReduxStub;
let myController;
beforeEach(angular.mock.inject($injector => {
ngReduxStub = new NgReduxStub();
//how the state should be initially
ngReduxStub.push(myState);
myController = new MyController(ngReduxStub, $injector.get('someOtherService');
}));
Note: I personnaly don't use mapDispatchToProps (I expose my action creators through an angular service), so the stub does not handle actions, but it should be easy to add.
I have been getting the following error while trying to write tests using Karma and Jasmine:
TypeError: undefined is not a constructor (evaluating
$ngRedux.connect(function (state) { ({}, state.editAudience); }, _actions2.default)')
I managed to make it work by roughly translating wbuch's stub into es6 and using it like this:
angular.mock.module( $provide => {
ngReduxStub = new NgReduxStub()
ngReduxStub.push(myState)
const dependencies = ['$scope', '$ngRedux']
dependencies.forEach( dependency => {
if(dependency =='$ngRedux') { return $provide.value('$ngRedux', ngReduxStub)}
return $provide.value(dependency, {})
})
})
angular.mock.inject( ($compile, $rootScope, $componentController) => {
scope = $rootScope.$new()
ctrl = $componentController('csEditAudience', {$scope: scope}, {})
})
Here's my version of the stub
I hope this helps!
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.
This is something I discovered when trying to make my controllers more reusable.
app.factory('BaseController', function(myService) {
return function(variable) {
this.variable = variable;
this.method = function() {
myService.doSomething(variable);
};
};
})
.controller("ChildController1", function($scope, BaseController) {
BaseController.call($scope, "variable1");
})
.controller("ChildController2", function($scope, BaseController) {
BaseController.call($scope, "variable2");
});
And now I can do something like this in my HTML (e.g inside ng-controller="ChildController1"): ng-click="method()"
The code simply works. But I don't know how it really works (what kind of pattern is it?) and would it be a good practice to do so?
Your solution works more or less like this:
app.factory('mixinBaseController', function(myService) {
return function (scope, variable) {
angular.extend(scope, {
variable: variable;
method: function() {
myService.doSomething(variable);
}
});
};
})
.controller("ChildController1", function($scope, mixinBaseController) {
mixinBaseController($scope, "variable1");
})
.controller("ChildController2", function($scope, mixinBaseController) {
mixinBaseController($scope, "variable2");
});
Can you see? By your .call($scope, ...) is just setting the context (this) to the real $scope and this way the BaseController just extends your scope with properties and functions.
This is only to demonstrate how your code works.
To achieve a more JavaScript like inheritance, please see:
Can an AngularJS controller inherit from another controller in the same module?
AngularJS controller inheritance
AngularJS Inheritance Patterns
Two Approaches to AngularJS Controller Inheritance
You should as well have a look at the "controllerAs" syntax introduced in AngularJS 1.2. I highly recommend to use controllerAs. This also should help to implement inheritance for your controllers.