In IE 11, I have an Angularjs 1.5 modal component as below. The modal opens and on render event it calls a function outside of the angular app with a callback function contained in this component. This outside function initiates a install process which kicks off an embedded object as shown below and this then periodically calls the callback function.
The issue I am having is the binding is not being updated in the template on each callback function called from the embedded object call. The console.log is executed and i can see the message in the console.
The binding is initially updated with 'starting process' so binding is correct
<span ng-bind="$ctrl.messages[$ctrl.messages.length - 1]"></span>
I tried calling scope.apply as below but nothing happens. Only when the initiateprocess is completed, the binding is then updated with the last message shown from the final callback call. So the initiateprocess function is blocking the binding but no blocking the console.log's
is this the correct way to handle multiple callbacks and updating bindings
angular.module('components')
.component('testModal', {
bindings:{
modalInstance: '<',
resolve: '=',
dismiss: '&',
close: '&'
},
controller: TestController,
templateUrl: 'scripts/components/TestModal.html'
});
TestController.$inject = ['$scope'];
function TestController($scope) {
var ctrl = this;
ctrl.$onInit = function(){
ctrl.messages = [];
ctrl.messages.push('starting process');
};
ctrl.modalInstance.rendered.then(function(){
CallVanillaJSFunction(callback);
});
function callback(message){
ctrl.messages.push(message);
console.log(ctrl.messages[ctrl.messages.length - 1]);
CheckScopeBeforeApply();
}
function CheckScopeBeforeApply() {
if(!$scope.$$phase) {
$scope.$apply();
console.log('scope applied');
}
};
}
Vanilla Function
var globalCallback;
function CallVanillaJSFunction(callback){
globalCallback = callback;
var complete = initiateprocess();
globalCallback(complete);
}
Embedded Object
<OBJECT ID="testObj" CLASS......
<SCRIPT language=javascript for=testObj event="OnEvent(message);">
if(navigator.userAgent.indexOf("Trident") != -1)
{
globalCallback(message);
}
</SCRIPT>
This question has been marked as duplicate but having looked at the duplicates I don't think it is the same. The global callback function can be called multiple times and the angular application does not know how many times it will be called.
Use the $timeout service to force a browser tick:
function callback(message){
$timeout(function() {
ctrl.messages.push(message);
console.log(ctrl.messages[ctrl.messages.length - 1]);
});
̶C̶h̶e̶c̶k̶S̶c̶o̶p̶e̶B̶e̶f̶o̶r̶e̶A̶p̶p̶l̶y̶(̶)̶;̶
}
If the updates to the message occur all in the same browser tick, only the last update will be rendered. The $timeout service does both a framework digest cycle and a browser rendering cycle.
For more information, see AngularJS $timeout Service API Reference
Related
In my controller for a mpbile app based on Angular1 is have (for example) the following function:
var getItem = function() {
// Initialize $scope
$scope.url = "(url to get my data)";
$http.get($scope.url).success(function(data) {
$scope.itemDetails = data; // get data from json
});
};
getItem();
and this works just fine.. with one problem.. it doesnt update. Even if I switch pages and come back, if the scope hasnt changed, it doesnt reflect new data in the scope.
So, i built in an $interval refresh to look for changes in the scope, this works fine EXCEPT, when i leave the page to go to another, that interval keeps polling. This is obviously a bad idea in a mobile app where data and battery usage may be an issue.
So.. how can I keep checking the scope for 'live changes' when ON that page only OR what is best practice for the scope to refresh on data changes.
I have read about digests and apply but these still seem to be interval checks which I suspect will keep operation after switching pages.
Or on angular apps with live data, is constantly polling the API the 'thing to do' (admittedly the data the page pulls is only 629 bytes, but i have a few pages to keep live data on, so it will add up)
Thanks
When you create a controller, the function's in it are declared, but not run. and since at the end of the controller you are calling getItem(); it is run once.
Moving to another page, and coming back is not going to refresh it.
The only way to refresh is to call that function again, In your HTML or JS.
For example:
<button ng-click="getItem()">Refresh</button>
Really nice question, I have been wondering the same thing, so I checked a lot of related SO posts and wrote kind of a function that can be used.
Note: I am testing the function with a simple console.log(), please insert your function logic and check.
The concept is
$interval is used to repeatedly run the function($scope.getItem) for a period (in the below example for 1 second), A timeout is also actively running to watch for inactive time, this parameter is defined by timeoutValue (in the example its set to 5 seconds), the document is being watched for multiple events, when any event is triggered, the timeout is reset, if the timeoutValue time is exceeded without any events in the document another function is called where the interval is stopped. then on any event in the document after this, the interval is started back again.
var myModule = angular.module('myapp',[]);
myModule.controller("TextController", function($scope, $interval, $document, $timeout){
//function to call
$scope.getItem = function() {
console.log("function");
};
//main function
//functionName - specify the function that needs to be repeated for the intervalTime
//intervalTime - the value is in milliseconds, the functionName is continuously repeated for this time.
//timeoutValue - the value is in milliseconds, when this value is exceeded the function given in functionName is stopped
monitorTimeout($scope.getItem, 1000 ,5000);
function monitorTimeout(functionName, intervalTime, timeoutValue){
//initialization parameters
timeoutValue = timeoutValue || 5000;
intervalTime = intervalTime || 1000;
// Start a timeout
var TimeOut_Thread = $timeout(function(){ TimerExpired() } , timeoutValue);
var bodyElement = angular.element($document);
/// Keyboard Events
bodyElement.bind('keydown', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('keyup', function (e) { TimeOut_Resetter(e) });
/// Mouse Events
bodyElement.bind('click', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('mousemove', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('DOMMouseScroll', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('mousewheel', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('mousedown', function (e) { TimeOut_Resetter(e) });
/// Touch Events
bodyElement.bind('touchstart', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('touchmove', function (e) { TimeOut_Resetter(e) });
/// Common Events
bodyElement.bind('scroll', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('focus', function (e) { TimeOut_Resetter(e) });
function TimerExpired(){
if(theInterval) {
$interval.cancel(theInterval);
theInterval = undefined;
}
}
function TimeOut_Resetter(e){
if(!theInterval){
theInterval = $interval(function(){
functionName();
}.bind(this), intervalTime);
}
/// Stop the pending timeout
$timeout.cancel(TimeOut_Thread);
/// Reset the timeout
TimeOut_Thread = $timeout(function(){ TimerExpired() } , timeoutValue);
}
var theInterval = $interval(function(){
functionName();
}.bind(this), intervalTime);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myapp">
<div ng-controller="TextController">
</div>
</div>
Depending on the router you are using, you have to tell the controller to reload when the route changed or updated, because the function you pass when declaring a controller is only a factory, and once the controller is constructed it won't run again because the router caches it (unless you tell angularjs to do so, which is rarely a good idea).
So your best bet is to use the router to reload the state when the route changes. You can do this using the router event change and update that is broadcast in the scope.
If you are using angularjs' router (a.k.a., ngRoute):
$scope.$on('$routeChangeUpdate', getItem);
$scope.$on('$routeChangeSuccess', getItem);
If you are using ui.router:
$scope.$on('$stateChangeUpdate', getItem);
$scope.$on('$stateChangeSuccess', getItem);
Note: in ui.router you can add cache: false on the state declaration and it'll prevent the controller and the view to be cached.
I'm rather new to angular and I'm trying to integrate np-autocomplete in my application (https://github.com/ng-pros/np-autocomplete). However I can only get it to work when I'm passing a html string as a template inside the $scope.options and it doesn't work when I want to load it from a separate html.
the Code for my app looks as follows:
var eventsApp = angular.module('eventsApp',['ng-pros.directive.autocomplete'])
eventsApp.run(function($templateCache, $http) {
$http.get('test.html', {
cache: $templateCache
});
console.log($templateCache.get('test.html')) // --> returns undefined
setTimeout(function() {
console.log($templateCache.get('test.html')) // --> works fine
}, 1000);
//$templateCache.put('test.html', 'html string') //Would solve my issue in the controller,
//but I would rather prefer to load it from a separate html as I'm trying above
Inside my controller I am setting the options for autocomplete as follows:
controllers.createNewEventController = function ($scope) {
$scope.options = {
url: 'https://api.github.com/search/repositories',
delay: 300,
dataHolder: 'items',
searchParam: 'q',
itemTemplateUrl: 'test.html', // <-- Does not work
};
//other stuff...
}
however, it seems that test.html is undefined by the time np-autocomplete wants to use it (as it is also in first console.log above).
So my intuition tells me that the test.html is probably accessed in the controller before it is loaded in eventsApp.run(...). However I am not sure how to solve that?
Any help would be highly appreciated.
You are most likely correct in your assumption.
The call by $http is asynchronous, but the run block will not wait for it to finish. It will continue to execute and the execution will hit the controller etc before the template has been retrieved and cached.
One solution is to first retrieve all templates that you need then manually bootstrap your application.
Another way that should work is to defer the execution of the np-autocomplete directive until the template has been retrieved.
To prevent np-autocomplete from running too early you can use ng-if:
<div np-autocomplete="options" ng-if="viewModel.isReady"></div>
When the template has been retrieved you can fire an event:
$http.get('test.html', {
cache: $templateCache
}).success(function() {
$rootScope.$broadcast('templateIsReady');
});
In your controller listen for the event and react:
$scope.$on('templateIsReady', function () {
$scope.viewModel.isReady = true;
});
If you want you can stop listening immediately since the event should only fire once anyway:
var stopListening = $scope.$on('templateIsReady', function() {
$scope.viewModel.isReady = true;
stopListening();
});
I have a custom directive datepicker that uses scope.apply and works well. I cut out most of it to avoid cluttering the question, here is a simple version
appAdmin.directive("datepickerPss", ["$compile", "$parse", function ($compile, $parse) {
return {
$element.datepicker($scope.options).on("changeDate", function (ev) {
$scope.$apply(function () {
ngModel.$setViewValue(ev.date);
});
});
}
}]);
I have the custom datepicker in a modal, I simply want to initialize the value so in my controller I did the following at the top and had the "$digest already in progress" error
$scope.sDate = Date.now();
So reading up on this issue and the scope apply I changed it to the following in my controller
$timeout(function() {
$scope.sDate = Date.now();
});
However I still get the $digest in progress error. I'm not sure where to go from here. All the posts I have read have had their issues resolved by using $timeout.
Remove $scope.$apply and just use $timeout instead.
$element.datepicker($scope.options).on("changeDate", function (ev) {
$timeout(function () {
ngModel.$setViewValue(ev.date);
}, 0);
});
$scope.$apply starts a new $digest cycle on $rootScope, so calling it inside of your directive starts another $digest cycle while one is already occurring. By wrapping your call in $timeout, it'll wait until the previous $digest cycle finishes before applying your changes.
In addition, if you are trying to initialize a value AFTER you've already bound bound your change event in your directive, you could run into issues since the directive's digest cycle might still be in progress while your controller is being parsed and executed.
I'm using a module from the UI Boostrap extensions (http://angular-ui.github.io/bootstrap). The module actually serves as a loading dialog and is automatically closed when a set of web service data is returned to my Angular code. As the data on this page is loaded automatically the dialog comes up immediately.
All this works great when I hit the page in question for the the first time or simply refresh it. The problem occurs when I go to a deeper page and then try and navigate back to the original page (with the dialog) via the browser's back button. The dialog never goes away despite all the fact that all the data is returned and the module's dismiss() call has been made.
I've traced this down to the promise to open the dialog appears to be happening after the dismiss call but, again, only when the page is loaded via the back button. The dismiss call never closes anything because it hasn't been added yet (I've confirmed this in the debugger).
The question I have is how could I handle this? Is there a solid way to catch the completion of the page loading via Angular and double check that the dialog closed? Is there a better way via UI Bootstrap's api?
I know this is rather unusual case but any thoughts on it would be great.
Thanks!
#HankScorpio's solution is good, but I think there may be a simplified option now.
There is no need to store the current modal anymore, if you register either a $locationChangeStart or $routeChangeStart listener with $uibModalStack injected and call $uibModalStack.dismissAll(). $locationChangeStart has the benefit of working for both ngRoute and uiRoute.
i.e. If only for the one page, then in your controller you'd have:
angular.module('app')
.controller('ctrl', ['$scope', '$uibModalStack', ctrl]);
function ctrl($scope, $uibModalStack) {
$scope.$on('$locationChangeStart', handleLocationChange);
function handleLocationChange() {
$uibModalStack.dismissAll();
}
}
If you want to do this for all pages then define this in a factory that is always loaded or just an app.run code segment:
angular.module('app')
.run(['$rootScope', '$uibModalStack', setupUibModal]);
setupUibModal($rootScope, $uibModalStack) {
$rootScope.$on('$locationChangeStart', handleLocationChange);
function handleLocationChange() {
$uibModalStack.dismissAll();
}
}
Here is the simple solution when using ui-router for state change
Closing modal popup on the back button click in angularjs
App.run(['$rootScope', '$modalStack', function ($rootScope, $modalStack) {
$rootScope.$on('$stateChangeStart', function (event) {
var top = $modalStack.getTop();
if (top) {
$modalStack.dismiss(top.key);
}
})
}]);
hope this will save lot of time for people who are breaking heads
I've run into this same problem. Here's how I fixed it.
1) Create a service to abstract the opening and closing of a modal and track which one is open (necessary for step 2). Instead of calling $modal.open() directly, call ModalService.open().
Here you go, you can have the one I wrote:
(function () {
'use strict';
var theModule = angular.module('services.modalService', ['ui.bootstrap']);
theModule.factory('ModalService', function ($modal) {
var service = {};
var currentModal;
var clearModal = function () {
currentModal = undefined;
};
service.getCurrentModal = function () {
return currentModal;
};
service.open = function (options) {
currentModal = $modal.open(options);
currentModal.result['finally'](clearModal);
return currentModal;
};
return service;
});
}());
2) In a controller, add an event listener to $routeChangeStart, this event will fire whenever someone hits the back button.
$scope.$on('$routeChangeStart', function(){
var currentModal = ModalService.getCurrentModal();
if(angular.isDefined(currentModal)){
currentModal.dismiss('cancel');
}
});
3) Your modals should now close when a user hits back.
4) Enjoy.
IMPROVEMENT:
I found the answer from HankScorpio to be the best out there. I wanted to include this snippet for those using ui-router and their recommendation for stateful modals.
1) I wanted the result.finally(...) to jump to a parent state;
2) I wanted to control the closing of the modal from $stateProvider config, NOT through rigging a controller and adding a listener to $routeChangeStart
Here is an example of a state that opens (and closes) it's modal:
.state('product.detail', {
url: '/detail/{productId}',
onEnter: /*open-modal logic*/,
onExit: ['ModalService', function (ModalService) { ModalService.close()} ]
})
I made ModalService aware of $state so that the result of closing a modal could jump to a parent view:
a. Add an isStateful flag to modalService.open(...):
service.open = function (options, isStateful) {
currentModal = $uibModal.open(options);
currentModal.result.finally(function () {
clearModal(isStateful);
});
return currentModal;
};
so that clearModal will return to previous state:
var clearModal = function (isStateful) {
currentModal = undefined;
if (isStateful)
$state.go('^');
};
Finally, add the closeModal() function called above (not a "stateful" close, simply a dismissal):
service.close = function() {
if (currentModal) {
currentModal.dismiss().then(function () {
clearModal();
})
}
}
The benefits of this are that back button functionality is controlled at state config level, not through a listener.
I have a file upload directive that fires events (through scope.$emit) while the file is being uploaded that is then caught in my controller (through scope.$on) to update a progress bar. The file upload directive gets ng-switch-ed out so the user can fill in other information about the file while the file is being uploaded. I want the progress bar to keep getting updated while they're doing this. This was all working just fine with version 1.0.2, but it seems to be broken now in 1.0.4.
The jsFiddle I created is greatly simplified, but it shows the problem. When the page first loads, the event is fired from the directive and it gets caught in the controller and everything gets updated as expected. However, if you change the dropdown so that the directive gets ng-switch-ed out, the event stops getting caught. If you look in the console, the event is still gettting $emit-ed, but the controller just stops catching it. Even if I put the $on on the $rootScope(http://jsfiddle.net/uysAM/1/), it still has the same problem. If the event is still being $emit-ed, why would the scopes stop catching it?
If you change the src in the <script> tag at the top to use 1.0.2, it works just fine. Is this some new bug with the latest version? Thanks for any help.
This is a scope problem. Here is how I understand it.
The scope chain when foo=1
MyCtrl's scope <- We catch here
ngSwitch's scope for foo="1"
myDirective's scope <- We emit here
The scope chain is intact and we can catch the emitted event without problem.
The scope chain changes when foo=2
MyCtrl's scope
null <- Is destroyed and set to null for memory management.
myDirective's scope
The scope chain is no longer intact. The emitted event doesn't propagate past myDirective's cleaned up parent scope. It is possible that this cleanup was added as a feature in 1.0.4.
Here is my solution to the problem. I use a fileLoader service to share state between the directive and the controller.http://jsfiddle.net/apBZX/
var myApp = angular.module("myApp", [])
myApp.controller('MyCtrl', function ($scope, $rootScope, fileLoader) {
$scope.$watch(function () {
return fileLoader.numberOfloadedFiles;
}, function (numberOfLoadedFiles) {
$scope.numberOfLoadedFiles = numberOfLoadedFiles;
});
$scope.foo = 1;
});
myApp.directive("myDirective", function (fileLoader) {
return {
restrict: 'A',
replace: true,
scope: {},
template: '<div>I am the directive</div>',
link: function (scope, element, attrs) {
if (!fileLoader.isLoading) {
fileLoader.loadFiles(scope);
}
}
}
});
myApp.service("fileLoader", function () {
return {
numberOfloadedFiles: 0,
isLoading: false,
loadFiles: function (scope) {
var self = this;
setInterval(function () {
scope.$apply(function(){
self.numberOfloadedFiles++;
});
}, 1000);
self.isLoading = true;
}
};
});