Declared the following constructor in my Typescript AngularJS controller.
static $inject: Array<string> = [
'$rootScope',
'BookingRepository'
];
constructor(
$rootScope: ng.IRootScopeService,
bookingRepository: soft.data.BookingRepository
) {
super();
this.$rootScope = $rootScope;
this.$rootScope.$on('accessController_onNavAccessSelected', this.accessController_onNavAccessSelected);
this.bookingRepository = bookingRepository;
}
BUT when accessController_onNavAccessSelected is called, I'm getting null when I reference 'this'. I'm expecting the instance of the controller.
// Listening Events
accessController_onNavAccessSelected(event: ng.IAngularEvent, accessViewModel: soft.data.AccessViewModel): void {
console.log(this);
}
What did I missed? Or How do I get a reference on the instance of my controller?
The context of the keyword this changes when using callbacks.
To prevent that, add .bind(this):
this.$rootScope.$on('accessController_onNavAccessSelected', this.accessController_onNavAccessSelected.bind(this));
or just use arrow function of ES6:
accessController_onNavAccessSelected = (event: ng.IAngularEvent, accessViewModel: soft.data.AccessViewModel) => {
console.log(this);
}
Try to change accessController_onNavAccessSelected declaration this way:
accessController_onNavAccessSelected = (event: ng.IAngularEvent, accessViewModel: soft.data.AccessViewModel): void => {
console.log(this);
}
Or use var self = this workaround:
var self = this;
accessController_onNavAccessSelected(event: ng.IAngularEvent, accessViewModel: soft.data.AccessViewModel): void {
console.log(self);
}
Or you can force to specify exactly this, you needed, with the help of bind:
this.$rootScope.$on('accessController_onNavAccessSelected', this.accessController_onNavAccessSelected.bind(this));
Related
I am writing an app using angular 1.6, typescript, webpack, karma and jasmine. I was able to create unit tests for angular services, but now I am having troubles for testing components. On SO(1) and (2) and on the net I found different examples (like this), but not a clear guide explaining how to test angular 1 components with the above technology set.
My component (HeaderComponent.ts):
import {IWeatherforecast} from '../models/weather-forecast';
import WeatherSearchService from '../search/weather-search.service';
import WeatherMapperService from '../common/mapping/weatherMapper.service';
export default class HeaderComponent implements ng.IComponentOptions {
public bindings: any;
public controller: any;
public controllerAs: string = 'vm';
public templateUrl: string;
public transclude: boolean = false;
constructor() {
this.bindings = {
};
this.controller = HeaderComponentController;
this.templateUrl = 'src/header/header.html';
}
}
export class HeaderComponentController {
public searchText:string
private weatherData : IWeatherforecast;
static $inject: Array<string> = ['weatherSearchService',
'$rootScope',
'weatherMapperService'];
constructor(private weatherSearchService: WeatherSearchService,
private $rootScope: ng.IRootScopeService,
private weatherMapperService: WeatherMapperService) {
}
public $onInit = () => {
this.searchText = '';
}
public searchCity = (searchName: string) : void => {
this.weatherSearchService.getWeatherForecast(searchName)
.then((weatherData : ng.IHttpPromiseCallbackArg<IWeatherforecast>) => {
let mappedData = this.weatherMapperService.ConvertSingleWeatherForecastToDto(weatherData.data);
sessionStorage.setItem('currentCityWeather', JSON.stringify(mappedData));
this.$rootScope.$broadcast('weatherDataFetched', mappedData);
})
.catch((error:any) => console.error('An error occurred: ' + JSON.stringify(error)));
}
}
The unit test:
import * as angular from 'angular';
import 'angular-mocks';
import HeaderComponent from '../../../src/header/header.component';
describe('Header Component', () => {
let $compile: ng.ICompileService;
let scope: ng.IRootScopeService;
let element: ng.IAugmentedJQuery;
beforeEach(angular.mock.module('weather'));
beforeEach(angular.mock.inject(function (_$compile_: ng.ICompileService, _$rootScope_: ng.IRootScopeService) {
$compile = _$compile_;
scope = _$rootScope_;
}));
beforeEach(() => {
element = $compile('<header-weather></header-weather>')(scope);
scope.$digest();
});
To me is not clear how to access the controller class, in order to test the component business logic. I tried injecting $componentController, but i keep getting the error "Uncaught TypeError: Cannot set property 'mock' of undefined", I think this is related to angular-mocks not properly injected.
Can anyone suggest an approach of solution or a site where to find further details about unit testing angular 1 components with typescript and webpack?
I was able to found a solution for my question. I post the edited code below, so others can benefit from it and compare the starting point (the question above) with the final code for the unit test(below, splitted in sections for the sake of explanation).
Test the component template :
import * as angular from 'angular';
import 'angular-mocks/angular-mocks';
import weatherModule from '../../../src/app/app.module';
import HeaderComponent, { HeaderComponentController } from '../../../src/header/header.component';
import WeatherSearchService from '../../../src/search/weather-search.service';
import WeatherMapper from '../../../src/common/mapping/weatherMapper.service';
describe('Header Component', () => {
let $rootScope: ng.IRootScopeService;
let compiledElement: any;
beforeEach(angular.mock.module(weatherModule));
beforeEach(angular.mock.module('templates'));
beforeEach(angular.mock.inject(($compile: ng.ICompileService,
_$rootScope_: ng.IRootScopeService) => {
$rootScope = _$rootScope_.$new();
let element = angular.element('<header-weather></header-weather>');
compiledElement = $compile(element)($rootScope)[0];
$rootScope.$digest();
}));
As for directives, also for components we need to compile the relative template and trigger a digest loop.
After this step, we can test the generated template code:
describe('WHEN the template is compiled', () => {
it('THEN the info label text should be displayed.', () => {
expect(compiledElement).toBeDefined();
let expectedLabelText = 'Here the text you want to test';
let targetLabel = angular.element(compiledElement.querySelector('.label-test'));
expect(targetLabel).toBeDefined();
expect(targetLabel.text()).toBe(expectedLabelText);
});
});
Test the component controller :
I created two mocked objects with jasmine.createSpyObj. In this way it is possible to create an instance of our controller and pass the mocked objects with the desired methods.
As the mocked method in my case was returning a promise, we need to use the callFake method from the jasmine.SpyAnd namespace and return a resolved promise.
describe('WHEN searchCity function is called', () => {
let searchMock: any;
let mapperMock: any;
let mockedExternalWeatherData: any;
beforeEach(() => {
searchMock = jasmine.createSpyObj('SearchServiceMock', ['getWeatherForecast']);
mapperMock = jasmine.createSpyObj('WeatherMapperMock', ['convertSingleWeatherForecastToDto']);
mockedExternalWeatherData = {}; //Here I pass a mocked POCO entity (removed for sake of clarity)
});
it('WITH proper city name THEN the search method should be invoked.', angular.mock.inject((_$q_: any) => {
//Arrange
let $q = _$q_;
let citySearchString = 'Roma';
searchMock.getWeatherForecast.and.callFake(() => $q.when(mockedExternalWeatherData));
mapperMock.convertSingleWeatherForecastToDto.and.callFake(() => $q.when(mockedExternalWeatherData));
let headerCtrl = new HeaderComponentController(searchMock, $rootScope, mapperMock);
//Act
headerCtrl.searchCity(citySearchString);
//Assert
expect(searchMock.getWeatherForecast).toHaveBeenCalledWith(citySearchString);
}));
});
});
Thanks for this post! I worked at the same time at the same problem and also found a solution. But this hero example doesn't require compiling the component (also no digest required) but uses the $componentController where also bindings can be defined.
The my-components module - my-components.module.ts:
import {IModule, module, ILogService} from 'angular';
import 'angular-material';
export let myComponents: IModule = module('my-components', ['ngMaterial']);
myComponents.run(function ($log: ILogService) {
'ngInject';
$log.debug('[my-components] module');
});
The hero component - my-hero.component.ts
import {myComponents} from './my-components.module';
import IController = angular.IController;
export default class MyHeroController implements IController {
public hero: string;
constructor() {
'ngInject';
}
}
myComponents.component('hero', {
template: `<span>Hero: {{$ctrl.hero}}</span>`,
controller: MyHeroController,
bindings: {
hero: '='
}
});
The hero spec file - my-hero.component.spec.ts
import MyHeroController from './my-hero.component';
import * as angular from 'angular';
import 'angular-mocks';
describe('Hero', function() {
let $componentController: any;
let createController: Function;
beforeEach(function() {
angular.mock.module('my-components');
angular.mock.inject(function(_$componentController_: any) {
$componentController = _$componentController_;
});
});
it('should expose a hero object', function() {
let bindings: any = {hero: 'Wolverine'};
let ctrl: any = $componentController('hero', null, bindings);
expect(ctrl.hero).toBe('Wolverine');
})
});
Note: It took some time to fix an error in testing the binding:
$compileProvider doesn't have method 'preAssignBindingsEnabled'
The reason was a version difference between angular and angular-mock. The solution was provide by: Ng-mock: $compileProvider doesn't have method 'preAssignBindingsEnabled`
I am trying to develop an application in angular es6 . I have a problem with directve.
Here is my code
export default class RoleDirective {
constructor() {
this.template="";
this.restrict = 'A';
this.scope = {
role :"#rolePermission"
};
this.controller = RoleDirectiveController;
this.controllerAs = 'ctrl';
this.bindToController = true;
}
// Directive compile function
compile(element,attrs,ctrl) {
console.log("df",this)
}
// Directive link function
link(scope,element,attrs,ctrl) {
console.log("dsf",ctrl.role)
}
}
// Directive's controller
class RoleDirectiveController {
constructor () {
console.log(this.role)
//console.log("role", commonService.userModule().getUserPermission("change_corsmodel"));
//$($element[0]).css('visibility', 'hidden');
}
}
export default angular
.module('common.directive', [])
.directive('rolePermission',[() => new RoleDirective()]);
The problem is i couldn't get the role value inside constructor.
here is my html implementation
<a ui-sref="event" class="button text-uppercase button-md" role-permission="dfsd" detail="sdfdsfsfdssd">Create event</a>
If i console this it will get the controller object. But it will not get any result while use this.role.
Ok, so I managed to find out how this works.
Basically, the scope values cannot be initialized on the controller's constructor (because this is the first thing executed on a new object) and there is also binding to be considered.
There is a hook that you can implement in your controller that can help you with your use case: $onInit:
class RoleDirectiveController {
constructor () {
// data not available yet on 'this' - they couldn't be
}
$onInit() {
console.log(this.role)
}
}
This should work. Note that this is angular1.5+ way of doing things when not relying on $scope to hold the model anymore. Because if you use the scope, you could have it in the controller's constructor (injected).
(Im using Babel to be able to use ES6)
When I call addConfigurationToCart() I get:
ReferenceError: Order is not defined.
But in the constructor I don't. Why is that? I get the same error if I add Order as a parameter to addConfigurationToCart
class ConfigCtrl {
constructor($state, api, Order) {
this.current = Order.current;
}
addConfigurationToCart() {
Order.saveConfiguration();
$state.go('order');
}
}
constructor and addConfigurationToCart functions have different scopes (in JS sense), and sure, the variable from one scope isn't available in another, unless the variable is assigned to either this property or the variable from parent scope.
Private variables are still aren't there in ES2015+, but there are some workarounds to do that.
The most obvious way is using local variables:
let $state, api, Order;
class ConfigCtrl {
static $inject = ['$state', 'api', 'Order'];
constructor(...args) {
[$state, api, Order] = [...args];
// ...
}
addConfigurationToCart() {
Order.saveConfiguration();
// ...
}
}
And more idiomatic approach that successfully provides private variables within class:
const [$state, api, Order] = [Symbol(), Symbol(), Symbol()];
class ConfigCtrl {
static $inject = ['$state', 'api', 'Order'];
constructor(...args) {
[$state, api, Order].forEach((v, i) => this[v] = args[i]);
// ...
}
addConfigurationToCart() {
this[Order].saveConfiguration();
// ...
}
}
You have to make the Service public to the rest of your class.
class ConfigCtrl {
constructor($state, api, Order) {
...
this.Order = Order;
}
addConfigurationToCart() {
this.Order.saveConfiguration();
...
}
}
controller: class {
constructor($http, Restangular, $state) {
Object.assign(this, {$http, Restangular, $state});
}
doIt() {
// use this.$http, this.Restangular & this.$state freely here
}
}
I've created a custom angular form validator in TypeScript. Everything works fine on the browser but typescript is complaining that "Property 'compareTo' does not exist on type 'IModelValidators'" at this line:
ngModel.$validators.compareTo = modelValue => (modelValue === scope.otherModelValue); Which makes sense, because I am basically creating a new validator called "comparedTo" that doesn't exist and just attaching it to the model. This is totally valid in javascript but since Typescript is strongly typed it didn't like it that much. Does anyone have any idea of how to add my "compareTo" validation to the ngModel.$validators in a typescript safe way? Thanks!
'use strict';
module App.Directives {
export interface ILoZScope extends angular.IScope {
otherModelValue: string;
}
export class CompareToDirective implements angular.IDirective {
// Directive parameters.
public restrict = 'A';
public require = 'ngModel';
public scope = { otherModelValue: '=compareTo' }
// Constructor
public static $inject = ['$window'];
constructor($window) {}
// Link function
public link(scope: ILoZScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes, ngModel: angular.INgModelController) {
ngModel.$validators.compareTo = modelValue => (modelValue === scope.otherModelValue);
scope.$watch('otherModelValue', () => { ngModel.$validate(); });
}
// Creates an instance of the compareToDirective class.
public static factory(): angular.IDirectiveFactory {
const directive = ($window: angular.IWindowService) => new CompareToDirective($window);
directive.$inject = ['$window'];
return directive;
}
}
angular
.module('app')
.directive('compareTo', CompareToDirective.factory());
}
If you just want to skip the typescript error, just create a custom definitely typed file and add something like this.
interface IModelValidators {
comparedTo: any;
}
If you want to get proper intellisense, use something like this in your custom d.ts file.
interface IModelValidators {
comparedTo: (modelValue: any, viewValue: any) => boolean;
}
an alternate solution would be
ngModel.$validators["compareTo"] = (modelValue, viewValue) : boolean => {
if(modelValue....) {
return true
}
return false;
}
In a AngularJS 1.2.5 using TypeScript 0.9.1 app, we are seeing that when we change routes, the private methods on a controller class remain in the heap and leave detached DOM trees in chromes profiler.
If we navigate /#/view1 to /#/view2 and back to /3/view1, we end up with view1 controller class in the heap twice and view2 controller class in the heap as well.
Our workaround has been to not use private methods anymore.
The code generally looks like:
module views {
app.controller("view1Ctrl", function($scope, $routeParams) {
return new view1Ctrl($scope, $routeParams);
});
interface Scope extends ng.IScope {
TrackingTab: any;
}
class view1Ctrl {
constructor(private $scope: Scope, $routeParams: any) {
$scope.TrackingTab = $routeParams["tab"];
$scope.$watch("showTab", (newValue: TrackingTab): void => {
if (newValue === undefined) return;
});
}
private changeTabToNew(): void {
this.$scope.TrackingTab = "new"
}
}
}
we have to change to something along the lines of:
module views {
app.controller("view1Ctrl", function($scope, $routeParams) {
return new view1Ctrl($scope, $routeParams);
});
interface Scope extends ng.IScope {
TrackingTab: any;
}
class view1Ctrl {
constructor(private $scope: Scope, $routeParams: any) {
$scope.TrackingTab = $routeParams["tab"];
$scope.$watch("showTab", (newValue: TrackingTab): void => {
if (newValue === undefined) return;
});
$scope.changeTabToNew(): void {
this.$scope.TrackingTab = "new"
};
}
}
Thanks in advance
If you want to make functions private in javascript, please refer to:
http://javascript.crockford.com/private.html
From the above code I think that the code:
private changeTabToNew(): void {
this.$scope.TrackingTab = "new"
}
is simply creating a function changeTabToNew() on the global (or root) scope (the private keyword is not having the effect you are expecting btw). Since this is not part of the scope that exists in the controller, you are creating a reference to your 'TrackingTab' in global scope and thus the controller cannot be garbage collected.