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
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.
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;
}
}
});
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.
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!
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.