unloading/destroying Angular lazy loaded components - javascript

I have a setup that is similar to the post found here http://ify.io/lazy-loading-in-angularjs/ to handle lazy loading various components of my app in Angular.
The problem I'm having is that the more components one loads, the memory footprint of the app grows (obvious, I know). Is there a clean way to 'unload' or 'destroy' angular services / controllers/ directives / etc that have been lazy loaded?
simple example (for reference)
app.js
var app = angular.module('parentApp', ['ui.router']).config(function ($stateProvider, $urlRouterProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
var app = angular.module('app', ['ui.router'])
.config(function ($stateProvider, $urlRouterProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
app.lazy = {
controller: $controllerProvider.register,
directive: $compileProvider.directive,
filter: $filterProvider.register,
factory: $provide.factory,
service: $provide.service
};
$urlRouterProvider.otherwise('/home');
$stateProvider
.state('home', {
url: '/home',
templateUrl: 'views/main.html',
controller: 'MainCtrl'
})
.state('lazyLoad', {
url: '/lazyLoad',
templateUrl: 'views/lazyLoad.html',
controller: 'lazyLoadCtrl',
resolve: {
promiseObj: function ($q, $rootScope) {
var deferred = $q.defer();
var dependencies = [
'scripts/controllers/lazyLoad.js'
];
$script(dependencies, function () {
$rootScope.$apply(function () {
deferred.resolve();
});
});
return deferred.promise;
}
}
});
});
and the lazy loaded controller lazyLoad.js
angular.module('app')
.lazy.controller('lazyLoadCtrl', function($scope){
$scope.greeting = 'hi there';
}
);
If a user navigates to #/lazyLoad, the app will load in the controller (and view), and what ever else it is told it needs. But when the user navigates away from #/lazyload, I want all the previously loaded components to be unloaded / destroyed.
How would I entirely de-register / destroy / unload (not sure what the correct verb would be here...) the 'lazyLoadCtrl' controller. As stated above, Ideally I'd like to be able to unload the lazy loaded controllers, directives, filters, factories and services, once they are no longer needed.
Thanks!

Maybe you can try calling scope.$destroy()
From the angularjs scope doc https://docs.angularjs.org/api/ng/type/$rootScope.Scope
$destroy();
Removes the current scope (and all of its children) from the parent scope. Removal implies that calls to $digest() will no longer propagate to the current scope and its children. Removal also implies that the current scope is eligible for garbage collection.
The $destroy() is usually used by directives such as ngRepeat for managing the unrolling of the loop.
Just before a scope is destroyed, a $destroy event is broadcasted on this scope. Application code can register a $destroy event handler that will give it a chance to perform any necessary cleanup.
Note that, in AngularJS, there is also a $destroy jQuery event, which can be used to clean up DOM bindings before an element is removed from the DOM.

Related

AngularJS undefined Controller using require

I am using requireJS in my application.
Whenever i tried to register controller on my module it said that the controller is not defined. Here is my controller which resides on login.controller.js
function LoginController() {
}
and here's my module code:
require('angular')
require('#uirouter/angularjs');
require('./service/storage')
require('./controller/login.controller')
angular.module('SecurityModule', ['ui.router'])
.controller('LoginController', LoginController);
// Routing
angular.module('SecurityModule')
.config(function ($stateProvider, $urlRouterProvider, $locationProvider) {
$locationProvider.hashPrefix('');
$stateProvider.state('login', {
url: '/login',
templateUrl: '/app/resources/view/security/login.html',
controller: 'LoginController',
});
})
;
When i checked my bundled.js the declaration of LoginController appears first. So why is it still undefine?
Thanks.
NOTE that im using browserify (which then uses commonJS) to bundle my files.
As the documentation states:
A module is a collection of configuration and run blocks which get
applied to the application during the bootstrap process. In its
simplest form the module consist of collection of two kinds of blocks:
Configuration blocks - get executed during the provider registrations
and configuration phase. Only providers and constants can be injected
into configuration blocks. This is to prevent accidental instantiation
of services before they have been fully configured.
angular.module('myModule', []).
config(function(injectables) { // provider-injector
// This is an example of config block.
// You can have as many of these as you want.
// You can only inject Providers (not instances)
// into config blocks.
}).
run(function(injectables) { // instance-injector
// This is an example of a run block.
// You can have as many of these as you want.
// You can only inject instances (not Providers)
// into run blocks
});

AngularJS 1.5 Controllers in Separate Files

I have a hard time understanding this. I'm attempting to put controllers in separate files so that they only deal with 1 thing, ideally, a partial view
My folder structure is like this...
My app.js file is like this.
angular.module('mikevarela', ['ui.router', 'mikevarela.controller.home', 'mikevarela.controller.about', 'mikevarela.controller.audio'])
.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/home');
$stateProvider
.state('home', {
url: '/home',
templateUrl: '../partials/home.partial.html',
controller: 'HomeController'
})
.state('about', {
url: '/about',
templateUrl: '../partials/about.partial.html',
controller: 'AboutController'
})
.state('audio', {
url: '/audio',
templateUrl: '../partials/audio.partial.html',
controller: 'AudioController'
});
});
and my controllers each have a module like this...
angular.module('mikevarela.controller.home', [])
.controller('HomeController', ['$scope', function($scope) {
$scope.title = 'Mike Varela Home Page';
}]);
My issues comes with the intial app declaration. I don't want to have to inject all the controllers in the main array app definition, that would be cumbersome and long winded. Isn't there a way to define the controller at the controller file. Kind of like this
angular.module('mikevarela', []).controller('HomeController', ['$scope', function($scope) {
// stuff here
}]);
Use angular.module('mikevarela').controller..... in subsequent files.
angular.module('mikevarela',[]).controller.....
is equivalent to redefining your app. The second param is requires array.
Quoting official angular.module docs
requires
(optional)
!Array.=
If specified then new module is being created. If unspecified then the module is being retrieved for further configuration.
About your Controllers...
I think you're loading the controllers incorrectly.
You don't need to declare controllers as a dependency. Rather stating module.controller('yourController)` makes that controller available throughout the module.
If your controllers are in separate files, all you need to do to make it available is load it in with a script tag. e.g.
<script src="app.js"></script>
<script src="controller1.js"></script>
<script src="controller2.js"></script>
About your Application Structure...
This is not related to your question, but just coming from someone who's developed using Angular, I'd recommend not grouping your application by controllers/ by rather by feature. See: https://scotch.io/tutorials/angularjs-best-practices-directory-structure

Several angular modules for routing

Can i create several modules for routing in AngularJS app like:
1. First route management file:
angular.module('app.p.routes', ['ngRoute'])
.config(function ($routeProvider, $locationProvider) {
$routeProvider
.when('/forbidden',
{
templateUrl: 'app/views/pages/forbidden.html'
})
.......................
2. Second route management file:
angular.module('app.admin.routes', ['ngRoute'])
.config(function ($routeProvider) {
$routeProvider
.when('/admin-dashboard',
{
templateUrl: 'app/views/pages/admin/dashboard.html',
controller: 'dashboardController',
controllerAs: 'dashboard'
})
.............................
3. Main app file:
angular.module('mainApp',
[
'ngAnimate', //animating
'app.p.routes', //public routing
'app.admin.routes',
'ui.bootstrap',
'ngParallax', //parallax effect
'ngFileUpload'
])
When i tried to use this approach page hangs and angular throws error:
> VM188:30554 WARNING: Tried to load angular more than once.
I need an approach to split public and admin routing management.
You can have as many AngularJS modules as you like. There are no rules against that, however, you've attempted to include the Angular source twice which is why you're seeing this warning...
> VM188:30554 WARNING: Tried to load angular more than once.
The simplest solution to your issue that I can think of, is to add an event listener to the $routeChangeStart event. With this you'll be able to verify that the current user has the correct permissions to view anything before they actually to do so.
A simple Service to store some basic information on the current user could like this.
var app = angular.module('app', ['ngRoute']);
app.service('AuthenticationService', function () {
// Set the User object
this.setUser = function () {
this.$user = user;
};
// Get the User object
this.getUser = function (user) {
return this.$user
};
});
And then upon receiving the $routeChangeStart event, you can retrieve the user object and confirm that they are allowed to proceed to the chosen resource.
Here's an example, whereupon a user needs to be an Administrator to view any route that has "/admin" in it.
app.run(function ($rootScope, $location, AuthenticationService) {
// Create a listener for the "$routeChangeStart" event
$rootScope.$on('$routeChangeStart', function () {
// Is the user is an Administrator? Are they attempting to access a restricted route?
if ($location.url().indexOf('/admin') && !AuthenticationService.getUser().isAdmin) {
// Redirect the user to the login page
$location.path('/login');
}
});
});
If you want a more advanced solution however, have a look at this: https://github.com/Narzerus/angular-permission
This will enable you to achieve a more in-depth ACL implementation across your application.

Stop Controller's activity when switch route

I create single page app by AngularJS and I found my problem. I have function refresh data every 2 minutes by jQuery in route A. When I change to other route, that function in controller is still working. This is my code.
App.js
var newsapp = angular.module('newsAppMD', ['ngRoute']);
newsapp.config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/news', {
templateUrl: 'templates/news.html',
controller: 'imageNewsCtrl'
}).
when('/news/:nameCat', {
templateUrl: 'templates/news-thumbnail.html',
controller: 'newsPageCtrl'
}).
otherwise({
redirectTo: '/news'
});
}]);
newsapp.controller('imageNewsCtrl', function($scope, $http, $interval, $timeout ) {
$('#bottom-bar').find('#epg').hide();
$scope.updateTimeEPG = 120000;
$scope.fetchFeed = function() {
$http.get("http://wi.th/thaipbs_tv_backend/epg_forJS.php").success(function(response) {
$scope.items = response.Schedule;
console.log($scope.items);
$timeout(function() { $scope.fetchFeed(); }, $scope.updateTimeEPG);
}).then(function() {
$('#bottom-bar').find('.loading').hide();
$('#bottom-bar').find('#epg').show();
});
};
$scope.fetchFeed();
});
newsapp.controller('newsPageCtrl', function($scope, $http, $location) {
// blah blah blah
}]);
I choose /news imageNewsCtrl work. And when I switch to other route, function in imageNewsCtrl still work (I see function print console.log when I changed route). I want to stop function in controller when change route. Thanks for your suggestion everyone. :)
I am not too entirely sure, but try using $stateProvider instead of $routeProvider. If you do, then you need to npm install angular-ui-router (it is a powerful third party module) and replace ngroute. I only user $routeProvider for the .otherwise function. You can also do a lot more cool stuff like onEnter and onExit with $stateProvider. Another thing is I would recommend you to use only Angular instead of jQuery. I do not really see a point of you using both. Use Angular's two-way data binding! Also, if you really want to get into Angular, then I recommend John Papa's style guide. This guys knows what he is talking about for making a great Angular app. I hope this info helps!

Configure AngularJS routes to deep path using $window

I have a Rails app which has some complex routing. My Angular application exists in a deep URL such as /quizzes/1
I was hoping to do this the Angular was by injecting $window into my routes configuration and then sniffing $window.location.pathName. This does not seem possible as the application throws an "Unknown provider: $window from myApp" at this stage.
Is there a best-practice way to handle this with Angular? The reason I would like to do this is to use HTML5 mode while the app lives in a deep directory.
Here's an example of what I was hoping for, http://jsfiddle.net/UwhWN/. I realize that I can use window.location.pathname at this point in the program if it's the only option.
HTML:
<div ng-app="myApp"></div>
JS:
var app = angular.module('myApp', [])
app.config([
'$window', '$routeProvider', '$locationProvider',
function($window, $routeProvider, $locationProvider) {
var path = $window.location.pathname
// Coming Soon
// $locationProvider.html5Mode(true)
$routeProvider
.when(path + '/start', {
controller: 'splashScreenController',
templateUrl: 'partials/splash-screen.html'
})
.when(path + '/question/:id', {
controller: 'questionController',
templateUrl: 'partials/question-loader.html'
})
.otherwise({
redirectTo: path + '/start'
})
}])
Only constants and providers can be injected into config block. $window isn't injectable into your config block because $window is a service.
From Angular docs:
Configuration blocks - get executed during the provider registrations and configuration phase. Only providers and constants can be injected into configuration blocks. This is to prevent accidental instantiation of services before they have been fully configured.
And, you don't need $window service there anyway. Just use <base> tag:
<base href="/quizzes/1/" />
and keep your routes relative to it.

Categories