I'm changing my existing AngularJS app from (AngularJS + ES5 + Webpack) to (AngularJS + Typescript + Webpack)
This is one of my module implementation:
login.controller.ts
import {ILoginScope} from "./login.scope";
import {LoginViewModel} from "../../../view-models/user/login.view-model";
export class LoginController implements ng.IController{
//#region Properties
public email: string = '123456';
//#endregion
//#region Constructor
/*
* Initialize controller with injectors.
* */
public constructor(public $scope: ILoginScope){
this.$scope.loginModel = new LoginViewModel();
this.$scope.loginModel.email = '123';
this.$scope.hasEmail = this.hasEmail;
}
//#endregion
//#region Methods
//#endregion
//#region Events
public clickLogin(): void {
console.log('Hello world');
}
public hasEmail(): boolean{
let loginModel = this.$scope.loginModel;
if (!loginModel || !loginModel.email)
return false;
return true;
}
//#endregion
}
login.scope.ts
import {LoginViewModel} from "../../../view-models/user/login.view-model";
export interface ILoginScope extends ng.IScope {
//#region Properties
/*
* Login information.
* */
loginModel: LoginViewModel;
//#endregion
//#region Methods
/*
* Called when login is clicked.
* */
clickLogin: Function;
hasEmail: () => boolean;
//#endregion
}
login.module.ts
import {StateProvider} from "#uirouter/angularjs";
import {UrlStatesConstant} from "../../../constants/url-states.constant";
import {module} from 'angular';
export class LoginModule {
//#region Constructors
public constructor(private $stateProvider: StateProvider) {
$stateProvider
.state(UrlStatesConstant.LoginStateName, {
url: UrlStatesConstant.LoginStateUrl,
controller: 'loginController',
templateProvider: ['$q', ($q) => {
// We have to inject $q service manually due to some reasons that ng-annotate cannot add $q service in production mode.
return $q((resolve) => {
// lazy load the view
require.ensure([], () => resolve(require('./login.html')));
});
}],
parent: UrlStatesConstant.AuthorizeLayoutName,
resolve: {
/*
* Load login controller.
* */
loadLoginController: ($q, $ocLazyLoad) => {
return $q((resolve) => {
require.ensure([], (require) => {
// load only controller module
let ngModule = module('account.login', []);
// Import controller file.
const {LoginController} = require("./login.controller");
ngModule.controller('loginController', LoginController);
$ocLazyLoad.load({name: ngModule.name});
resolve(ngModule.controller);
})
});
}
}
});
}
//#endregion
}
And this is how I registered my typescript angularjs module to existing es5 angularjs application:
module.exports = (ngModule) => {
// Load routes.
let login = require('./login');
ngModule.config(($stateProvider) => login.LoginModule($stateProvider));
};
When I run the application and access to login page
There is an error thrown in my developer console:
TypeError: Cannot read property 'loginModel' of undefined at ChildScope.LoginController.hasEmail (login.controller.ts:26)
This is the generated source map:
From what the error message says, the this.$scope at line 26 cannot be recognized even it has been declared as public $scope: ILoginScope in the constructor.
What wrong am I doing ?
Thank you,
Related
I have a TypeScript class and a JavaScript controller separately. The TypeScript class is working as a service, whereas the JavaScript controller needs the Typescript class in order to do something with it. My problem: how do I do this?
Thoughts: I know that the AngularJS javascript controller won't allow for import statements, so is it possible do access the TypeScript class and its public contents via dependency injection?
Javascript controller:
(function() {
'use strict';
angular
.module('testModule')
.controller('testModule.TestCtrl', TestCtrl);
TestCtrl.$inject = [
'testService' // <--- inject here? Note: not sure about this
];
function TestCtrl(testService) {
var ctrl = this;
ctrl.$onInit = function() {
// do stuff
};
ctrl.useTestService = function() {
this.result = testService.makeItRain();
};
}
})();
Typescript Service
import { SomeStuffThatIsNeeded } from 'some/folder/structure';
export class TestService {
static $name = 'TestService';
static $inject = ['$window', '$location'];
constructor(private $window: any, private $location: any) {
}
makeItRain() {
this.doTheWaterDance();
}
private doTheWaterDance(): string {
return 'get creative!'
}
}
Short answer, Yes. All you need to do, is to register that class with Angular as a Service.
import { TestService } from './TestService';
angular
.module('testModule')
.service('testService', TestService);
This is the error I'm seeing when trying to run my unit test ..
Expected undefined to be defined.
TypeError: undefined is not an object (evaluating '$rootScope.$digest')
Module 'ThirdPartyModule' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.
Any ideas how to mock the testService so that I can still compile my component?
test.component.spec.ts
import { TestModule } from '../index';
describe('Component: testComponent', () => {
let $rootScope: angular.IScope;
let element: angular.IAugmentedJQuery;
beforeEach(() => {
angular.mock.module('ui.router');
angular.mock.module(TestModule.name);
});
beforeEach(inject((
_$rootScope_: angular.IScope,
$compile: angular.ICompileService,
_$state_: angular.ui.IStateService) => {
$rootScope = _$rootScope_;
element = angular.element('<test></test>');
element = $compile(element)($rootScope);
}));
it('should verify component compiled and rendered template', () => {
expect(element).toBeDefined();
$rootScope.$digest();
let link = element.find('a');
expect(link.text()).toContain('Click this link!');
});
});
test.module.ts
import { TestComponent } from './test';
export let TestModule: ng.IModule = angular.module(
'test', // my module name
['ui.router', 'ThirdPartyModule']) // dependencies, ThirdPartyModule contains testService
.component('test', new TestComponent());
test.component.ts
import { TestComponentController } from './test.component.controller';
export class TestComponent implements ng.IComponentOptions {
public template: string = '<a ng-if="ctrl.serviceReturned">Click this link!</a>';
public controller: Function = TestComponentController;
public controllerAs: string = 'ctrl';
constructor() {}
}
test.component.controller.ts
export class TestComponentController {
public serviceReturned: boolean = false;
constructor(private testService: any) {
if (this.testService.isDone()) {
this.serviceReturned = true;
}
}
}
TestComponentController.$inject = ['testService'];
Don't you need to add 'ThirdPartyModule' in an angular.mock.module?
I'm using ES6 codestyle and implement a sample module like this:
import angular from 'angular';
import { IndicatorSelectorComponent } from './indicatorSelector.component';
import './indicatorSelector.css';
export const IndicatorSelectorModule = angular
.module('indicatorSelector', [])
.component('indicatorSelector', IndicatorSelectorComponent)
.name;
with this code for the component:
import templateUrl from './indicatorSelector.html';
export const IndicatorSelectorComponent = {
templateUrl,
controller: class IndicatorSelectorComponent {
contructor($http) {
'ngInject';
this.$http = $http;
}
$onInit() {
console.log(this.$http);
}
}
}
The console.log(this.$http) is returning undefined. I was suspecting that the problem was with my webpack code and ng-annotate, but I can see that the generated bunddle is including the annotation:
var IndicatorSelectorComponent = exports.IndicatorSelectorComponent = {
templateUrl: _indicatorSelector2.default,
controller: function () {
function IndicatorSelectorComponent() {
_classCallCheck(this, IndicatorSelectorComponent);
}
_createClass(IndicatorSelectorComponent, [{
key: 'contructor',
value: ["$http", function contructor($http) { // <==== HERE!
'ngInject';
this.$http = $http;
}]
}, {
key: '$onInit',
value: function $onInit() {
console.log(this.$http);
}
}]);
return IndicatorSelectorComponent;
}()
};
But still it is not working. I tried to inspect on the module code a console.log(IndicatorSelectorComponent);
I can see that $inject is empty. I don't know what else to do. I appreciate any help on this.
Object
controller :function IndicatorSelectorComponent()
$inject :Array[0]
arguments : (...)
caller : (...)
length : 0
name : "IndicatorSelectorComponent"
prototype : Object
__proto__ : function ()
[[FunctionLocation]] : indicatorSelector.component.js:5
[[Scopes]] : Scopes[3]
templateUrl : "C:/Projetos/IndicadoresPCM/client/src/app/components/goals/indicatorSelector/indicatorSelector.html"
__proto__ : Object
If using this way it will works.
import templateUrl from './indicatorSelector.html';
export const IndicatorSelectorComponent = {
templateUrl,
controller: function ($http) {
"ngInject";
this.$onInit = function () {
console.log($http);
};
}
}
But still, would be very interesting to make it work with the class expression...
I want to use resolve to check if user is present on localStorage before controller fires up but can't get it to work.
Example:
//loginDirective.js
import {LoginController as controller} from './login.controller';
import template from './login.html';
export const loginDirective = () =>{
return{
controller,
template,
scope:{},
controllerAs:'vm',
replace: true,
restrict: 'E'
}
}
//login.js
import {loginDirective} from './login.directive';
import angular from 'angular';
import uiRouter from 'angular-ui-router';
export const login = angular.module('admin', [uiRouter])
.config(($stateProvider) => {
$stateProvider.state('login', {
url : '/login',
template: '<login></login>',
resolve : {
loggedUser: function () {
if(localStorage.getItem('user')){
return true;
}
return false;
}
}
})
})
.directive('login', loginDirective);
//LoginController
'use strict';
class LoginController {
constructor($state, AuthenticationService, loggedUser) {
this.$state = $state;
this.AuthenticationService = AuthenticationService;
this.email = '';
this.password = '';
this.loggedIn = loggedUser;
}
login = () => {
this.AuthenticationService.Login(this.email, this.password).then((result) => {
this.loggedIn = result;
if (this.loggedIn === true) {
this.$state.go('home', {}, {reload: true});
}
});
}
logout = () => {
this.AuthenticationService.Logout();
this.loggedIn = false;
this.$state.go('login', {location: true});
}
}
LoginController.$inject = ['$state', 'AuthenticationService', 'loggedUser'];
export {LoginController}
This throws Unknown provider: loggedUserProvider <- loggedUser
Note:
I am using webpack, gulp and babel to transpile. WebPack has babel stage 1 enabled.
I can see the resolve method in this.$state but that is not the way I want to access it.
Your 'login' state definition has no controller. So ui-router doesn't instantiate the login controller. So it can't pass anything to its constructor.
The controller is not the controller of the state. It's the controller of a directive that happens to be used in the template of the state. So that can't work.
Read https://ui-router.github.io/guide/ng1/route-to-component to understand how you must do: either use a controller and a template, or use a component.
I have a Service that makes a request for some data:
/// <reference path="../../typings/reference.ts" />
module app {
'use strict';
export class VehicleMakeService {
static $inject = ['$http'];
constructor(private $http: ng.IHttpService) {}
getVehicles(): ng.IPromise<any> {
return this.$http.get('https://api.edmunds.com/api/vehicle/v2/makes?state=used&year=2015&view=basic&fmt=json')
.then(function(response) {
return response.data;
});
}
}
angular.module('app').service('VehicleMakeService', VehicleMakeService);
}
This works as expected, however when I attempt to retrieve the data in the controller I get 'Promise {$$state: object}'.
Here is the controller:
/// <reference path="../../typings/reference.ts" />
module app {
'use strict';
interface ISearchController {
vehicles: any;
setVehicles(): void;
}
class SearchController implements ISearchController {
vehicles: any;
static $inject = ['VehicleMakeService'];
constructor(private vehicleMakeService: VehicleMakeService) {
this.vehicles = {};
this.setVehicles();
}
setVehicles(): void {
this.vehicles = this.vehicleMakeService.getVehicles();
console.log(this.vehicles);
}
}
angular.module('app').controller('SearchController', SearchController);
}
I tried resolving it in the controller:
setVehicles(): void {
this.vehicleMakeService.getVehicles().then(function(data) {
this.vehicles = data;
console.log(this.vehicles);
});
}
But then I get 'TypeError: Cannot set property 'vehicles' of undefined'.
I normally handle this kind of thing in the resolve function in the module config but I can't on this occasion.
You can also use arrow function from TS/ES6
like this:
setVehicles(): void {
this.vehicleMakeService.getVehicles().then((data) => {
this.vehicles = data;
console.log(this.vehicles);
});
}
btw. you shouldn't use internal modules in TS its so bad ;)
you can check my example skeleton application with external modules Angular 1.x and TypeScript here.
Since getVehicles method returns promise object you need to use it properly and never forget about asynchronousy of HTTP requests. Also context of the callback in then will be different so you also need to take of it, for example with bind method:
setVehicles(): void {
this.vehicleMakeService.getVehicles().then(function(data) {
this.vehicles = data;
console.log(this.vehicles);
}.bind(this));
}