Angular and Typescript Memory leaks - angularjs

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.

Related

Reference 'this' is null on $rootScope.$on

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));

Pass function to directive

Here is my directive:
module app.directives {
interface IDmDeleteIconController {
doAction(): void;
}
class DmDeleteIconController implements IDmDeleteIconController {
static $inject = ['$scope'];
constructor(public $scope:any) {
}
doAction() {
console.log('1');
this.$scope.dmAction();
}
}
export class DmDeleteIcon implements ng.IDirective {
templateUrl = './app/common/directives/deleteIcon/deleteIcon.html';
controller = DmDeleteIconController;
controllerAs = 'dmDeleteIconVm';
scope = {
dmAction: '&'
};
link = (scope: any, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctrl: any) => {
console.log('2');
scope.dmAction();
}
static factory(): ng.IDirectiveFactory {
const directive = () => new DmDeleteIcon();
directive.$inject = [];
return directive;
}
}
angular.module('myApp').directive('dmDeleteIcon', DmDeleteIcon.factory());
}
Here I am trying to use it:
dm-delete-icon(dm-action="console.log('hello');")
When I am open page I'll get 2 in the console (from link function), but I don't get the hello from the function I have passed to directive.
Why and how can I fix it?
Update:
Here is a directive template:
a.item-detail-delete-icon(class="form-image-link" href="" ng-click="dmDeleteIconVm.doAction()")
Here is HTML to which my Jade compile:
<dm-delete-icon dm-action="console.log('hello');"></dm-delete-icon>
Update 2:
I was trying to use it like this:
<dm-delete-icon dm-action="vm.foo()"></dm-delete-icon>
where:
foo(): void {
console.log("hello");
}
the function in the controller.
Update 3:
If I am trying to debug this code I will get this:
The problem here is that you're passing to the directive an expression console.log('hello'); that will be executed on parent controller scope.
This basically means you'd need to have console object attached to scope and under that object a method log. Angular expressions don't work with globals (console in this case) automatically.
Simply ensure that the expression you pass to the directive is a valid angular expression and that should work. For example - create new method on app scope called myConsoleLog that prints something to the console and change the attribute value for the directive to dm-action="myConsoleLog();"

Calling angular service from ES6 class method

(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
}
}

Directive with Typescript and isolate scope function binding

I am writing a web app with AngularJS and, for the first time, using TypeScript. The issue I'm having is with a directive executing a function passed on its isolate scope.
I have a controller that is managing "work" of different types. I then have different directives to manage each type of work. The controller reserves the next piece of work, then places it on the scope for the appropriate directive. The controller is below.
module app.work {
interface IWorkScope {
workDone: boolean;
workCompleted(): void;
}
export class WorkController implements IWorkScope {
currentReservedWork: app.work.IReservedWork;
static $inject = ['app.work.WorkService', '$log'];
constructor(private workService: app.work.IWorkService,
private $log: ng.ILogService) {
}
private getNextWork() {
//...call service, reserve work, prep data...
}
public workCompleted(): void {
//...any cleanup tasks, reserve next piece of work....
}
}
angular.module('app.work')
.controller('app.work.WorkController', WorkController);
}
The directives then handling the actual workflow of executing the "work" of its type. An example is below.
module app.verify {
'use strict';
export interface IVerifyItemScope extends ng.IScope {
reserved: app.work.IReservedWork;
onItemComplete(): any;
saveButtonClicked(): void;
item: app.verify.IVerifyItem;
vm: app.verify.IVerifyItemController;
}
export interface IVerifyItemController {
getVerifyItem(): void;
}
class VerifyItemController implements IVerifyItemController{
item: app.verify.IVerifyItem;
statuses: app.verify.VerifyResultStatus[];
tempBind: number;
static $inject = ['$scope', 'app.verify.VerifyService', '$log'];
constructor(public $scope: IVerifyItemScope,
private verifyService: app.verify.IVerifyService,
private $log: ng.ILogService) {
}
saveButtonClicked(): void {
this.$log.debug('button clicked');
this.$scope.onItemComplete();
}
getVerifyItem(): void {
//...call service to get specific work details...
}
}
angular
.module('app.verify')
.directive('sfVerifyItem', verifyItem);
function verifyItem(): ng.IDirective {
var directive = <ng.IDirective> {
restrict: 'E',
link: link,
templateUrl: '....',
controller: VerifyItemController,
controllerAs: 'vm',
scope: {
reserved: '=',
onItemComplete: '&'
}
};
function link(scope: any, element: ng.IAugmentedJQuery, attributes: any, controller: IVerifyItemController): void {
//...do any prep...
}
return directive;
}
}
<data-sf-verify-item reserved="vm.currentReservedWork" onItemComplete="vm.workCompleted()"></data-sf-verify-item>
When the user compeletes the "work", the directive will do any necessary service calls. Last, it executes function onItemComplete that was passed on the scope to inform the controller that the work is done, then the controller can reserve more work and the process repeats.
The issue I'm having is the function bound on the scope isn't getting executed. I can debug and see it on the isolate scope, but when I execute it in the directive (saveButtonClicked() above), nothing happens in the parent controller.
Sorry for the long post, but any help or insight would be greatly appreciated.
See Hugues comment above, forgot to convert binding from camel case.

Implementing angularjs directives as classes in Typescript

So after taking a look at some of the examples of angularjs directives in typescript, it seems most people agree to use functions instead of classes when implementing them.
I would prefer to have them as a class and attempted to implement them as follows:
module directives
{
export class search implements ng.IDirective
{
public restrict: string;
public templateUrl: string;
constructor()
{
this.restrict = 'AE';
this.templateUrl = 'directives/search.html';
}
public link($scope: ng.IScope, element: JQuery, attributes: ng.IAttributes)
{
element.text("Hello world");
}
}
}
Now this works fine. However, I need to have an isolated scope with some attributes and I'm struggling to find out how to include that in the class itself.
logic dictates that since I can have
public restrict: string;
public templateUrl: string;
I should be able to have something like:
public scope;
But I'm not sure if this is correct or how to carry on from there (i.e how to add the attributes to the scope).
Anybody know how to solve this? (hopefully, without having to revert to a function if possible)
Thanks,
Creating directives as classes can be problematic since you still need to involve a factory function to wrap its instantiation. For example:
export class SomeDirective implements ng.IDirective {
public link = () => {
}
constructor() {}
}
What Doesn't Work
myModule.directive('someDirective', SomeDirective);
Since directives are not invoked using 'new' but are just called as factory functions. This will cause problems on what your constructor function actually returns.
What Does (with Caveats)
myModule.directive(() => new SomeDirective());
This works fine provided you don't have any IoC involved, but once you start introducing injectables, you have to maintain duplicate parameter lists for your factory function and your directive contstructor.
export class SomeDirective implements ng.IDirective {
...
constructor(someService: any) {}
}
myModule.directive('someDirective', ['someService', (someService) => new SomeDirective(someService)]);
Still an option if that is what you prefer, but is important to understand how the directive registration is actually consumed.
An alternative approach
The thing that is actually expected by angular is a directive factory function, so something like:
export var SomeDirectiveFactory = (someService: any): ng.IDirective => {
return {
link: () => {...}
};
}
SomeDirectiveFactory.$inject = ['someService']; //including $inject annotations
myModule.directive('someDirective', SomeDirectiveFactory);
This has the benefit of allowing the use of $inject annotations since angular needs it to be on the factory function in this case.
You could always return an instance of your class from the factory function as well:
export var SomeDirectiveFactory = (someService: any): ng.IDirective => {
return new SomeDirective(someService);
}
SomeDirectiveFactory.$inject = ['someService']; //including $inject annotations
But really depends on your use case, how much duplication of parameter lists you are okay with, etc.
Assuming that what you have works without an islolated scope, the following should work with an isolated scope:
module directives
{
export class search implements ng.IDirective
{
public restrict = 'AE';
public templateUrl = 'directives/search.html';
public scope = {
foo:'=',
bar:'#',
bas:'&'
};
public link($scope: ng.IScope, element: JQuery, attributes: ng.IAttributes)
{
element.text("Hello world");
}
}
}
Here is my proposal:
Directive:
import {directive} from '../../decorators/directive';
#directive('$location', '$rootScope')
export class StoryBoxDirective implements ng.IDirective {
public templateUrl:string = 'src/module/story/view/story-box.html';
public restrict:string = 'EA';
public scope:Object = {
story: '='
};
public link:Function = (scope:ng.IScope, element:ng.IAugmentedJQuery, attrs:ng.IAttributes):void => {
// console.info(scope, element, attrs, this.$location);
scope.$watch('test', () => {
return null;
});
};
constructor(private $location:ng.ILocationService, private $rootScope:ng.IScope) {
// console.log('Dependency injection', $location, $rootScope);
}
}
Module (registers directive...):
import {App} from '../../App';
import {StoryBoxDirective} from './../story/StoryBoxDirective';
import {StoryService} from './../story/StoryService';
const module:ng.IModule = App.module('app.story', []);
module.service('storyService', StoryService);
module.directive('storyBox', <any>StoryBoxDirective);
Decorator (adds inject and produce directive object):
export function directive(...values:string[]):any {
return (target:Function) => {
const directive:Function = (...args:any[]):Object => {
return ((classConstructor:Function, args:any[], ctor:any):Object => {
ctor.prototype = classConstructor.prototype;
const child:Object = new ctor;
const result:Object = classConstructor.apply(child, args);
return typeof result === 'object' ? result : child;
})(target, args, () => {
return null;
});
};
directive.$inject = values;
return directive;
};
}
I thinking about moving module.directive(...), module.service(...) to classes files e.g. StoryBoxDirective.ts but didn't make decision and refactor yet ;)
You can check full working example here: https://github.com/b091/ts-skeleton
Directive is here: https://github.com/b091/ts-skeleton/blob/master/src/module/story/StoryBoxDirective.ts
Here finally i got working a directive as class plus inheritance. In derived directive I extend the scope plus define the templateUrl.
You can override any methods from base directive
.
The key was to return from constructor the instance of directive.Angularjs calls constructor without new keyword. In this case this is of type window
I wrapped few lines to check the instance type of this and in case of window I create a new instance of directive. (See Activator class from sample below)
module Realty.directives {
export class BaseElementWithLabel implements ng.IDirective {
public restrict = 'E';
public replace = true;
public scope = {
label: '#',
model: '=',
icon: '#',
readonlyElement: '=',
remark: '#'
}
constructor(extendedScope) {
if (!(this instanceof Window)) {
extendedScope = extendedScope || {};
this.scope = angular.extend(this.scope, extendedScope);
}
}
link(scope: ng.IScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes, controller, transclude) {
scope['vm'] = {};
}
}
}
module Realty.directives {
export class textareaWithLabel extends Realty.directives.BaseElementWithLabel implements ng.IDirective {
templateUrl = 'directives/element-form/textarea-with-label-template.html';
constructor() {
super({
rows: '#'
});
return Realty.Activator.Activate(this, textareaWithLabel, arguments);
}
};
};
module Realty {
export class Activator {
public static Activate(instance: any, type, arguments) {
if (instance instanceof type) {
return instance;
}
return new(type.bind.apply(type, Array.prototype.concat.apply([null], arguments)));
}
}
}

Resources