I'm having an issue where old controllers seem to stay active, even after using ui-router's $state.go function to go to a different state with another controller.
I've tested this by adding an interval to each controller, logging it's name to the console. After changing the state multiple times, all the previously visited controllers stay active:
$interval(function () {
console.info("ACCOUNT")
}, 1000)
Based on the official documentation available here, intervals created using $interval are not automatically destroyed when the controller's scope is destroyed.
Note: Intervals created by this service must be explicitly destroyed
when you are finished with them. In particular they are not
automatically destroyed when a controller's scope or a directive's
element are destroyed. You should take this into consideration and
make sure to always cancel the interval at the appropriate moment.
It is a good practice to listen to the scope's $destroy event and destroy all the intervals that were created in the controller.
Here is a good way to do it.
var intervalRef;
$scope.someFunction = function () {
// Save a reference to the interval's promise so that it can be canceled later
intervalRef = $interval(function () {
console.info("ACCOUNT")
}, 1000);
}
$scope.$on("$destroy", function () {
// When the scope of the controller is destroyed, cancel the interval
if (intervalRef) {
$interval.cancel(intervalRef);
}
});
Related
I have this small piece of code that it is not working:
$scope.$on('play', function(e, data) {
$scope.tick = (data.time*100)/2.5;
});
console.log($scope.tick);
If I log $scope.tick outside $on return undefined. I can't understand why, I need to access that var outside the event listener.
This is the code triggering the $on
angular.module('videoCtrl', ['vjs.video'])
.controller('videoController', ['$scope', 'Timeline', function ($scope, Timeline) {
$scope.mediaToggle = {
sources: [
{
src: 'http://static.videogular.com/assets/videos/videogular.mp4',
type: 'video/mp4'
}
],
};
//listen for when the vjs-media object changes
$scope.$on('vjsVideoReady', function (e, videoData) {
videoData.player.on('timeupdate', function () {
var time = {
time: this.currentTime()
};
$scope.$broadcast('play', time);
})
});
}]);
and here the one receiving it:
angular.module('mediaTimelineCtrl', ['mt.media-timeline'])
.controller('DemoMediaTimelineController', function ($scope, Timeline) {
$scope.tick = 100;
$scope.disable = false;
$scope.timelines = Timeline.getTimelines();
$scope.$on('play', function(e, data) {
$scope.tick = (data.time*100)/2.5;
});
console.log($scope.tick);
});
Thanks to everyone
$on is a method provided by angularjs that allows you to listen for a specific broadcast at which point the provided function will be executed.
You log the variable right after you registered your callback that sets the tick variable. At this point your callback simply hasn't been called yet. For this to happen you must use $scope.$broadcast('play', data) somewhere in your code(Depending on wher you call $broadcast you might have to use $rootScope instead of $scope, because broadcast only sends the events to child scopes.(See angular docs here for more info)
Edit: This has been resolved in chat now. The callback provided in $on was called correctly, but the video library that is being used here called the event outside of an angular $digest cycle. Wrapping the assignment of $scope.tick into $scope.$apply([...]) did the trick.
$scope.$on use to listen to the events that fired by the $broadcast or an $emit. until one of those event fires, on function is not gonna fire and the content of the on function will not gonna execute.
But since the console log is outside of the on function it will execute whether scope.on fires or not. that is why conosole.log shows undefined.
if you put the console inside the scope.on function then it will execute only when the event fires
The value of $scope.tick is updated only when the event is fired. At the end of first digest cycle the value of $scope.tick is still undefined. You have to initialize the value of $scope.tick first or use $watch [$watch(watchExpression, listener, [objectEquality]);]. watchExp The expression being watched. It can be a function or a string, it is evaluated at every digest cycle. listener A callback, fired when the watch is first set, and then each time that during the digest cycle that a change for watchExpâs value is detected. The initial call on setup is meant to store an initial value for the expression.
I have a module called LegalModule, there are three components that subscribe to the same module, basically:
Both components have their own folder and each have an index.js file where they bootstrap like:
angular.module('LegalModule')
.component('person', require('person.component.js')
.controller('PersonController', require('person.controller.js');
and another file like
var component = {
templateUrl: 'person-tamplate.html',
controller: 'PersonController',
bindings: {info: '<'}
}
module.exports = component;
Then in that controller i have something like :
var controller = ['PersonRepository','$stateParams', function(PersonRepository, $stateParams)
{
var vm = this;
//other code
function Save(){
//code that saved
}
function onSuccess(){
//Let another component know this happened and call its refresh function.
}
}];
Other component / controller
angular.module('LegalModule')
.component('buildings', require('buildings.component.js')
.controller('BuildingController', require('buildings.controller.js');
and the component
var component = {
templateUrl: 'building-template.html'
controller: 'BuildingController'
}
Controller
var controller = ['BuildingReader',function(BuildingReader){
function refreshBuildings(){
//this needs to be called on success of the save of the Person Repository
}
}];
On the main tamplate:
<div class="LegalFacilities">
<person></person>
<buildings></buildings>
</div>
So i am new to components and i am not sure how to make in a way that when something is saved in the person controller, on it's success, that it can trigger the refresh function in the building controller to fire.
I really do not want to use $scope or anything like that , there is gotta be a cleaner way?. (not sure but i would appreciate any inputs).
Since you have two components that are not on the same DOM element, your methods of communicating between them are more limited. You still have several ways that you can do it:
onSuccess() emits an event on the $rootScope and all interested controllers listen for that event (just make sure to unsubscribe to the event on $destroy).
Create one or more services that contain the all the non-UI shared application state. All controllers that need access to state inject the service that contains that state. And controllers can also $watch a variable on the service to be notified when something changes and something needs to be refreshed.
Pass state around using the parent scope. Ie- each child scope declares a scope variable that is bound to the same variable in the parent scope. And if the state changes in one of the child scopes, the $digest cycle will ensure that the state is propagated to the other child scope.
In general, my preference is #2. The reason is that this keeps a clear separation between application state and UI state. And it becomes very easy to ensure that all parts of your application can share bits that they need to.
In your case, since you need to notify that an action happened, you can trigger this through changing a successHash number (an opaque number that just gets incremented on every save such that all watchers are notified).
Edit: a very simple example of sharing state using services.
angular.module('mymod').service('myService', function() {
this.val = 9;
});
angular.module('mymod').directive('dir1', function(myService, scope) {
scope.doSomething().then(res => myService.val = res);
});
angular.module('mymod').directive('dir2', function(myService, scope) {
scope.$watch(() => myService.val, () => console.log(`It happened! ${myService.val});
});
Imagine I have a controller which handles, for example, view changes:
function Controller($scope){
var viewModel = this;
viewModel.goBack= function(){
viewModel.visible = visibleLinks.pop(); //get last visible link
viewModel.swipeDirection = 'left';// for view change animation
}
}
But I want to handle it not only for example with HTML buttons inside <body>, but also with Back button on device. So I have to add Event Listener for deviceready event, and also explicit call $scope.$apply() in order to fact, that it is called outside of AngularJS context, like this:
document.addEventListener("deviceready", function(){
document.addEventListener("backbutton", function(){
viewModel.goBack();
$scope.$apply();
}, false);
}, false);
}
But I also want to follow (relatively :) ) new controllerAssyntax, cause this is recommended now e.g. by Todd Motto: Opinionated AngularJS styleguide for teams and it allows to remove $scope from controllers when things like $emit or $on are not used. But I can't do it, case I have to call $apply() cause my context is not Angular context when user clicks on device back button. I thought about creating a Service which can be wrapper facade for cordova and inject $scope to this service but as I read here: Injecting $scope into an angular service function() it is not possible. I saw this: Angular JS & Phonegap back button event and accepted solution also contains $apply() which makes $scope unremovable. Anybody knows a solution to remove Cordova specific events outside Angular controller, in order to remove $scope from controllers when not explicity needed? Thank you in advance.
I don't see a reason why to remove the $scope from the controller. It is fine to follow the best practice and to remove it if not needed, but as you said you still need it for $emit, $on, $watch.. and you can add it $apply() in the list for sure.
What I can suggest here as an alternative solution is to implement a helper function that will handle that. We can place it in a service and use $rootScope service which is injectable.
app.factory('utilService', function ($rootScope) {
return {
justApply: function () {
$rootScope.$apply();
},
createNgAware: function (fnCallback) {
return function () {
fnCallback.apply(this, arguments);
$rootScope.$apply();
};
}
};
});
// use it
app.controller('SampleCtrl', function(utilService) {
var backBtnHandler1 = function () {
viewModel.goBack();
utilService.justApply(); // instead of $scope.$apply();
}
// or
var backBtnHandler2 = utilService.createNgAware(function(){
viewModel.goBack();
});
document.addEventListener("backbutton", backBtnHandler2, false);
});
In my case I was simply forwarding Cordova events with the help of Angular $broadcast firing it on the $rootScope. Basically any application controller would then receive this custom event. Listeners are attached on the configuration phase - in the run block, before any controller gets initialized. Here is an example:
angular
.module('app', [])
.run(function ($rootScope, $document) {
$document.on('backbutton', function (e) {
// block original system back button behavior for the entire application
e.preventDefault();
e.stopPropagation();
// forward the event
$rootScope.$broadcast('SYSTEM_BACKBUTTON', e);
});
})
.controller('AppCtrl', function ($scope) {
$scope.$on('SYSTEM_BACKBUTTON', function () {
// do stuff
viewModel.goBack();
});
});
Obviously in the $scope.$on handler you do not have to call $scope.$apply().
Pros of this solution are:
you'll be able to modify an event or do something else for the entire application before the event will be broadcasted to all the controllers;
when you use $document.on() every time controller is instantiated, the event handler stays in the memory unless you manually unsibscribe from this event; using $scope.$on cares about it automatically;
if the way a system dispatches Cordova event changes, you'll have to change it in one place
Cons:
you'll have to be careful when inheriting controllers which already have an event handler attached on initialization phase, and if you want your own handler in a child.
Where to place the listeners and the forwarder is up to you and it highly depends on your application structure. If your app allows you could even keep all the logic for the backbutton event in the run block and get rid of it in controllers. Another way to organize it is to specify a single global callback attached to $rootScope for example, which can be overriden inside controllers, if they have different behavior for the back button, not to mess with events.
I am not sure about deviceready event though, it fires once in the very beginning. In my case I was first waiting for the deviceready event to fire and then was manually bootstrapping AngularJS application to provide a sequential load of the app and prevent any conflicts:
document.addEventListener('deviceready', function onDeviceReady() {
angular.element(document).ready(function () {
angular.bootstrap(document.body, ['app']);
});
}, false);
From my point of view the logic of the app and how you bootstrap it should be separated from each other. That's why I've moved listener for backbutton to a run block.
Angular, UI-router. Using $interval in a controller of a state like so:
$scope.Timer = null;
$scope.startTimer = function () {
$scope.Timer = $interval($scope.Foo, 30000);
};
$scope.stopTimer = function () {
if (angular.isDefined($scope.Timer)) {
$interval.cancel($scope.Timer);
}
};
The problem? The timer persists upon leaving the state. My understanding was that the $scope and the controller are essentially "destroyed" when a state is left. So, based on that, the timer should stop (Within the controller, I am cancelling the timer when moving around, that works - but it persists if I navigate to a diff state). What am I misunderstanding here?
I guess since interval and timeout are services in angular, they are available everywhere, but I still don't understand how they see functions in the not-initialized controller, unless it's copied. Is my solution to just use regular good-old js interval?
clear interval on $destroy
Like this
$scope.$on("$destroy",function(){
if (angular.isDefined($scope.Timer)) {
$interval.cancel($scope.Timer);
}
});
Within an angular controller I am attaching to a websocket service. When the controllers scope is destroyed I obviously want to remove the subscription.
Is it safe to pass the current scope to my service subscription function so it can auto remove on scope destroy? If I dont then each controller who attaches to a socket listener has to also remember to clean up.
Basically is it safe to pass current $scope to a service function or is there a better way of doing this?
I had similar need in my project. Below is the object returned in a AngularJS factory (which initializes WebSocket). The onmessage method automatically unsubscribes a callback if you pass in its associated scope in the second argument.
io =
onmessage: (callback, scope) ->
listeners.push callback
if scope then scope.$on "$destroy", => #offmessage callback
offmessage: (callback) -> listeners.remove callback
The JavaScript equivalence is below.
var io = {
onmessage: function(callback, scope) {
var _this = this;
listeners.push(callback);
if (scope) {
scope.$on("$destroy", function() {
_this.offmessage(callback);
});
}
},
offmessage: function(callback) {
listeners.remove(callback);
}
};
I would not pass the scope. Instead, I would explicitly, in your controller, hook up the unsubscribe.
From http://odetocode.com/blogs/scott/archive/2013/07/16/angularjs-listening-for-destroy.aspx :
$scope.$on("$destroy", function() {
if (timer) {
$timeout.cancel(timer);
}
});
I think having this done explicitly is not as magical, and easier to follow the logic. I think the service would be doing too much if it were to also unsubscribe. What if a controller wants to unsubscribe early?
However, if you do have a very specific use case that's used everywhere, it would be fine to pass the scope in. The amount of time the service needs the scope is very small, basically when the controller first executes so that the service can listen to the $destroy event.