Orphaned $firebase references causing memory leak - angularjs

I have an angular service that creates synced $firebase references. These references are ultimately passed into a controller, and then to a directive with an isolate scope.
When the $scope of the controller is destroyed by navigating to a different state, the references appear to stay in memory and never get GCed.
Code Sample:
var app = angular.module('app', ['firebase', 'ui.router']);
app.service('service', function($firebase) {
var ref = new Firebase('https://ease-bugreport.firebaseio.com/tasks');
return {
find: function(taskId) {
// Creating orphan refs after states are changed. Not getting $destroy()-ed as the corresponding scope is destroyed?
return $firebase(ref.child(taskId)).$asObject();
}
}
});
app.controller('ctrl', function($scope, service) {
$scope.tasks = [];
/*
* In the real application, this list of ids is grabbed from an index of ids.
*/
var taskIds = [
'-JVMmByyk5wvYdVJQ_JT',
'-JVMmBz4hue-5QytQwWb',
'-JVMmBz8aAt5WDUQ4H1R',
'-JVMmC-Q8QEGB6zZuitb',
'-JVMmC-UkMAiyi6v6bcK',
'-JVMmC-WyOrlNKZTjnqH',
'-JVMmC-Y29ncf14G1rkA',
'-JVMmC0coVLi1FUfrbKD',
'-JVMmC1hDrs07XdwcgLh',
'-JVMmC1k-GYz_DWw3dDj',
'-JVMmC2aCuzOIZ2nf1B-',
'-JVMmC2cQKNkOBxhJ5vP',
'-JVMmC2giV_IlXrKXVFw',
'-JVMmC3fXQYfjtXdTk_p',
'-JVMmC3ibcUPT88hcD6Q',
'-JVMmC3mDKms0BVpAcdq',
'-JVMmC4jFwfPNe1-istd',
'-JVMmC4m3ZGAiS7xnXHP',
'-JVMmC4rp3pNfeTgIUCJ',
'-JVMmC4uaH7MdkTZbQVm',
'-JVMmC5ttFy3ojD1bt3t',
'-JVMmC5v_iTwWS02PF9h',
'-JVMmC5xFYPS0zvaU4bi',
'-JVMmC75NA1H1e7dYGdM',
'-JVMmC77o5mBUACibaUG',
'-JVMmC7AmuYy6VDNn9B1',
'-JVMmC85nVa6NexPJLLP',
'-JVMmC88XIFUqq98gexw',
'-JVMmC89h4HLaXxmHld8',
'-JVMmC8CNJ55Olt8D57w'
];
angular.forEach(taskIds, function(taskId) {
$scope.tasks.push(service.find(taskId));
});
});
app.directive('taskPanel', function() {
return {
scope: {
task: '='
},
restrict: 'E',
replace: true,
template: '<div>{{task.name}} - {{task.createdAt | date}}</div>',
link: function(scope, element, attrs) {
}
};
});
app.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('main', {
url: '/main',
controller: function() {},
template: '<div>MAIN PAGE!</div>'
})
.state('list', {
url: '/list',
controller: 'ctrl',
templateUrl: 'list.html'
});
$urlRouterProvider.otherwise('/main');
});
Here is a codepen demonstrating the issue: http://codepen.io/rabhw/pen/ADiKz
The issue is far more exaggerated in my application as each reference is using the 'objectFactory' option to attach additional instance methods via a factory.
Should I be taking a different approach to my services?
Any advice is appreciated.

Try adding following code to your controller:
$scope.$on('$destroy', function() {
$scope.tasks = undefined; // or []
});
Should automatically be recognized in directive's task.

Related

Angular Component Bindings Undefined

I'm trying to put together my first angular component with ngRoute and so far I'm unable to get data to resolve.
config:
.when('/myfirstcomponent', {
template: '<myfirstcomponent claimKeys="$resolve.claimKeys"></myfirstcomponent>',
resolve: {
claimKeys: ['$http', function($http) {
$http.get('server/claimkeys.json').then((response) => {
var claimKeys = response.data.DATASET.TABLE;
return claimKeys;
});
}]
}
})
Component:
.component('myfirstcomponent', {
bindings: {
'claimKeys': '#'
},
templateUrl: 'components/component.html',
controller: [function() {
this.$onInit = function() {
var vm = this;
console.log(vm.claimKeys);
};
}]
The html for the component simply has a p element with some random text that's all.
I can see when debugging that I am retrieving data but I cannot access it on the component controller...
EDIT: Thanks to the accepted answer below I have fixed my issue. It didn't have anything to do with an issue with asynchronous calls but with how I had defined my route and the component. See below code for fix. Thanks again.
some issues:
as you said claimKeys within directive should be claim-keys
its binding should be '<' (one way binding) or '=' (two way binding), but not '#' which just passes to directive a string found between quotes
in your directive's controller var vm = this; should be above
$onInit function and not inside it (the scopes are different)
resolve.claimkeys should return $http's promise and not just call
it
claimKeys should be received by router's controller as injection and passed to its template
controllerAs: '$resolve' should be used by router
app.component('myfirstcomponent', {
bindings: {
'claimKeys': '='
},
template: 'components/component.html',
controller: function() {
var vm = this;
this.$onInit = function() {
console.log(vm.claimKeys);
};
}
});
app.config(function ($stateProvider) {
$stateProvider.state('myfirstcomponent', {
url: '/myfirstcomponent',
template: '<myfirstcomponent claim-keys="$resolve.claimKeys"></myfirstcomponent>',
resolve: {
claimKeys: ['$http', function($http) {
return $http.get('claimkeys.json').then((response) => {
return response.data.DATASET.TABLE;
});
}]
},
controller: function (claimKeys) {
this.claimKeys = claimKeys;
},
controllerAs: '$resolve'
})
});
plunker: http://plnkr.co/edit/Nam4D9zGpHvdWaTCYHSL?p=preview, I used here .state and not .when for routing.

moving angular modal controllers into separate files

I have a page controller on my site that includes several modals. Some of the modal controllers are getting robust, and I would like to move them into separate controllers without losing the scope of the outer page controller (the modal controller uses some of the page controllers functions and properties) - is this possible? So far I am getting errors to anything which references the outer controller. Here is a simple example of how I have it set up:
the page controller:
angular.module('myApp')
.controller('outerCtrl', function(MetaService) {
var thisCtrl = this;
thisCtrl.someFunction = function() {
//some cool functionality that will elvaluate something
}
function optionsModal() {
var PageCtrl = thisCtrl;
$uibModal.open({
'controller': 'scripts/controllers/optionsModal.js',
'controllerAs': 'ModalCtrl',
'templateUrl': 'views/modals/optionsModal.html',
'size': 'md'
});
}
});
the modal controller:
angular.module('myApp')
.controller('optionsModalCtrl', function(MetaService) {
var modalCtrl = this;
function giveOptions() {
if (PageCtrl.someFunction()) {
//offer some option
} else {
//offer a different option
}
}
});
Here is the example from official page:
var modalInstance = $uibModal.open({
animation: $scope.animationsEnabled,
templateUrl: 'myModalContent.html',
controller: 'ModalInstanceCtrl',
size: size,
scope: $scope, // pass parent scope to modal instance
resolve: {
items: function () {
return $scope.items;
}
}
});

Angular ui-router: $stateParams empty inside my directive

In my angular app, I have created a custom directive for a navbar, which controller takes in $stateParams to access a variable called lang, as so:
.config(function($stateProvider, $urlRouterProvider, LANG) {
$urlRouterProvider
.otherwise('/' + LANG['EN'].shortName);
$stateProvider
.state('proverb-list', {
url: '/:lang',
templateUrl: 'components/proverb-list/proverb-list.html',
controller: 'ProverbListCtrl as vm'
})
.state('proverb-single', {
url: '/:lang/:proverbId',
templateUrl: 'components/proverb-single/proverb-single.html',
controller: 'ProverbCtrl as vm'
});
});
When I access the state proverb-list, the controller named ProverbListCtrl does see $stateParams.lang correctly, but my navbar directive cannot. When I console.log($stateParams) all I get is an empty object.
This navbar is outside my ui-view:
<navbar proverbial-navbar></navbar>
<div ui-view></div>
<footer proverbial-footer></footer>
Is that the problem? How can I access the actual $stateParams inside my directive?
EDIT: directive code below, as asked:
(function() {
'use strict';
angular
.module('proverbial')
.directive('proverbialNavbar', directive);
function directive() {
var directive = {
restrict: 'EA',
templateUrl: 'components/shared/navbar/navbar.html',
scope: {
},
link: linkFunc,
controller: Controller,
controllerAs: 'vm',
bindToController: true
};
return directive;
function linkFunc(scope, el, attr, ctrl) {
}
}
Controller.$inject = ['LANG', 'ProverbFactory', '$stateParams'];
function Controller(LANG, ProverbFactory, $stateParams) {
var vm = this;
vm.languages = LANG;
console.log($stateParams);
vm.currentLang = LANG[$stateParams.lang.toUpperCase()];
activate();
function activate() {
vm.alphabet = ProverbFactory.getAlphabet();
}
}
})();
You should not access $stateParams in this directive when it is independent of the state. Since your navbar is outside of the ui-view, its controller can be called before the ui-router has initialized the $stateParams of the state you are interested in. Remember that your navbar controller is called only once, when the navbar is initialized and not every time the state changes.
Alternative: What you can do is turn your currentLang field into a function. The function can retrieve the value from the $stateParams when needed:
vm.currentLang = function() { return LANG[$stateParams.lang.toUpperCase()] };
Make sure you change currentLang to currentLang() everywhere in your template.

Two way binding of a model in a directive does not work

Based on a comment of another question from me I tried to create a directive to reduce my code. Here what I got:
Directive (very small for testing. Later it will be more elements):
BebuApp.directive('inputText', function(){
return {
restrict: 'E',
scope: {
model: '='
},
template: '<input type="text" ng-model="model" />'
}
});
State:
.state('app', {
abstract: true,
url: '',
templateUrl: 'layout.html',
resolve: {
authorize: function ($http) {
return $http.post(API.URL_PING);
}
}
})
.state('app.application-detail', {
url: "/bewerbungen/{id}",
templateUrl: "views/application-detail/application-detail.html",
data: {pageTitle: 'Meine Bewerbungen', pageSubTitle: ''},
controller: "ApplicationDetailController",
resolve: {
prm: function ($http, $stateParams) {
// $http returns a promise for the url data
return $http.get(API.URL_JOBAPPLICATION_GET_DETAILS + "/" + $stateParams.id);
}
}
})
Controller:
'use strict';
BebuApp.controller('ApplicationDetailController', function($rootScope, $scope, $http, $stateParams, API, prm) {
$scope.jobApplication = prm.data;
console.log(prm);
$scope.$on('$viewContentLoaded', function() {
// initialize core components
App.initAjax();
});
});
Template / View:
<div class="margin-top-10">
{{ jobApplication }}
<input-text model="jobApplication.description"></input-text>
</div>
When the page is loaded I can see the correct model (output by {{jobApplication}}), but the input field is empty. I need a normal two way binding. When the model changes in the scope it should also change in the directive and vice versa. As far as I understand the model is retrieved by the resolve callback / function in the state, so it should be "there" when the template is compiled.
Where is my problem?
I found the problem after an closer look to the model (thanks to the comments!). In fact the model I received from my backend was a collection with just one entry. It looked like this:
[{id:"xxx", description:"test".....}]
Of course it must look like this:
{id:"xxx", description:"test"...}
After fixing this stupid mistake, everything works fine!

Access parent controller from the child state

I have a controller in AngluarJS defined like so:
'use strict';
var app = angular.module('app');
app.controller('AppCtrl', ['sth',
function (sth) {
this.inverse = false;
}
]);
here is routes deffinition:
$stateProvider.
state('app', {
abstract: true,
templateUrl: 'app/views/layout.html',
controller: 'AppCtrl',
controllerAs: 'app',
resolve: {}
}).
state('app.settings', {
abstract: true,
url: '/settings',
template: '<ui-view/>',
onEnter: function () {
}
});
How to access inverse variable from AppCtrl in app.settings route?
If you want to share data between 2 controllers, a service/factory is your best bet. Below is a an example of how you would do it. I have written it free-hand, so there may be syntax errors, but you get the idea.
app.controller('AppCtrl', ['sth', 'SharedData',
function (sth, SharedData) {
SharedData.inverse = false;
}
]);
app.factory('SharedDate', function() {
var data = {
inverse: true // or whatever default value you want to set
};
return data;
})
Now, you can inject SharedData factory in any controller, where you want to use that data.

Resources