In this fiddle (http://jsfiddle.net/edwardtanguay/6pn8tb83/1), Angular increments a number every second, yet when an alert window pops up, the counting stops and when I close the alert window, I get the error
Error: [$rootScope:inprog] $apply already in progress
http://errors.angularjs.org/1.2.1/$rootScope/inprog?p0=%24apply
This error link actually takes me to a page which seems to be explaining what I need to do, but since I'm not explicitly using $scope.$apply() or $scope.$digest(), I don't understand what I need to do so that Angular simply continues to increment and show the incremented number while the alert window is popped up.
<div ng-controller="MyCtrl">
<div>Angular is counting: {{counter}}</div>
<button ng-click="processFiles()">processFiles</button>
<div>{{message}}</div>
</div>
var myApp = angular.module('myApp',[]);
function MyCtrl($scope, $interval) {
var theTimer = $interval(function () {
$scope.counter++;
}, 1000);
$scope.counter = 0;
$scope.message = 'click button';
$scope.processFiles = function() {
alert('ok');
}
}
This is the default behaviour. Script execution stops when browser dialogues are open.
Workaround is to get the time difference between the alert and updating the counter with it, another one is to create your own modal
Related
How do you enforce read-only properties in a performant way in Angular?
Controller:
function MyCtrl($scope) {
$scope.clickCount = 0;
$scope.incrementCount = function() {
$scope.clickCount = $scope.clickCount + 1;
}
}
View:
<div ng-controller="MyCtrl">
Clicked {{clickCount}} times
<button ng-click="incrementCount()">Doober</button>
<input type="text" ng-model="clickCount" /><!-- how do I prevent this -->
</div>
I know I could make clickCount a getter function getClickCount(), but will that kill the performance since Angular will have to call this function on every digest cycle?
http://jsfiddle.net/zb05om1k/
Update
I'm looking for a way that makes it clear that the read only property should not be changed directly but instead through the provided function. Additionally, prevent the view from changing the property directly.
use the angular directive ng-readonly, it works just like it sounds...
https://docs.angularjs.org/api/ng/directive/ngReadonly
In your example, incrementCount() is called once and takes a negligible amount of time to execute, then Angular starts a digest cycle and the DOM rebuilds, which would happen anyway.
As a general rule, you don't need to optimize code whose time is bound by user input. Your example code looks fine to me.
If you really want to enforce separation of concerns by making the property read-only, you can use Object.defineProperty to prevent writes:
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function MyCtrl($scope) {
var clickCount = 0; // Private variable
Object.defineProperty($scope, 'clickCount', {
set: function() { // Prevent views from editing data
throw new Error('Operation not supported');
},
get: function() {
return clickCount;
}
});
$scope.incrementCount = function() {
clickCount++;
}
});
When you put data in the input, the controller throws an error and the change is immediately erased in the DOM.
Fiddle: http://jsfiddle.net/acbabis/5bq5oeq3/
Let's say there's a button that triggers some sort of data processing which takes a while. During processing, I want to tell the user where we are by displaying the messages like "Working...", "Working very hard...", "Almost there...", etc. It is guaranteed that all those messages will appear one after another before the processing is completed. The task is to check this scenario with Protractor.
So, here's an example code:
<div ng-controller="AppController">
<p>{{message}}</p>
<button type="button" ng-click="go()">Go!</button>
</div>
...
<script>
angular.module('app', [])
.controller("AppController", function($scope, $timeout) {
$scope.message = "";
$scope.go = function() {
$scope.message = "Working...";
$timeout(function() {
$scope.message = "Working very hard...";
$timeout(function() {
$scope.message = "Almost there...";
$timeout(function() {
$scope.message = "Done!!!";
}, 1000);
}, 1000);
}, 1000);
};
});
</script>
I understand this behavior is relatively easy to test with regular unit tests (Jasmine), yet let's pretend these $timeout calls are actually async updates sent by backend via a websocket.
Is it possible to somehow test this sequence of updates with Protractor?
The straightforward approach like this:
expect(element(by.binding('message')).getText()).toEqual('Working...');
expect(element(by.binding('message')).getText()).toEqual('Working very hard...');
doesn't work. In my case the test fails with:
Expected 'Working very hard...' to equal 'Working...'.
which is understandable, I assume that Protractor just waits for all pending things to finish before moving on.
One approach I can think of is, explicitly polling the DOM to monitor when element content gets changed. This I would like to avoid. Are there any better options available?
You can give a try to the new feature: expected conditions (was added 10 hours ago):
var EC = protractor.ExpectedConditions;
var message = element(by.binding('message');
browser.wait(EC.textToBePresentInElement(message, 'Working...'), 1000);
browser.wait(EC.textToBePresentInElement(message, 'Working very hard...'), 1000);
browser.wait(EC.textToBePresentInElement(message, 'Almost there...'), 1000);
browser.wait(EC.textToBePresentInElement(message, 'Done!!!'), 1000);
$scope.$watch('num',function(){
$scope.nums.push($scope.num)
})
Changing the above code breaks the watching value on clicked (I'm unclear because breakit still pushing the value to nums):
$scope.breakit = $scope.$watch('num',function(){
$scope.nums.push($scope.num)
})
But I'm unclear how does this break the watching value?
Full code:
<div ng-controller="MainController">
<div>Num: {{num}}</div>
<div>Nums: {{nums}}</div>
<button ng-click="increment()">Increment</button>
<button ng-click="breakit()">Break It</button>
</div>
var app = angular.module('app',[]);
app.controller('MainController',function($scope){
$scope.num = 0
$scope.nums = []
$scope.increment = function(){
$scope.num++;
}
$scope.breakit = $scope.$watch('num',function(){
$scope.nums.push($scope.num)
})
});
So, using $scope.breakit why does this stops pushing the num to nums clicking to break it button and continue clicking to increment button?
You may check this video.
The documentation for $scope.watch() says:
Returns
function() Returns a deregistration function for this listener
When you call $scope.watch(), it returns a function that you can call at any time to unregister it.
Your controller assigns that function to $scope.breakit, so when $scope.breakit is called, the watch is turned off.
Edit:
There was a question in the comments about how $scope.break pushes values to the nums array. The answer is that it doesn't. $scope.watch() takes care of that all on its own, and $scope.break is used as a way to turn that process off.
I'm new to programming, and have recently been playing around with AngularJS.
To practice, i've decided try and create a simple stopwatch.
Starting with an initial 'time' value of 0, i'm using $interval to increment the 'time' by 0.01, every 10 milliseconds. I can start and stop the stopwatch without any issues, UNLESS i click 'Start' twice. After doing so, 'Stop' no longer works.
I'm sure this is an awful way to create a stopwatch, but regardless, the issue still remains.
My html contains the timer and 'Start' and 'Stop' buttons, like so:
<div class="row" style="margin-left: 20px" ng-controller="timerCtrl">
<b>{{time | number : 2}}</b>
<button ng-click="startTimer()">Start</button>
<button ng-click="stopTimer()">Stop</button>
</div>
And the js:
.controller('timerCtrl', ['$scope', '$timeout', '$interval',
function($scope, $timeout, $interval) {
$scope.time = 0;
$scope.startTimer = function() {
$scope.counter = $interval(function(){
$scope.time += 0.01;
}, 10)
}
$scope.stopTimer = function() {
$interval.cancel($scope.counter);
}
}
])
What's the best way to solve this? Any help will be much appreciated, thanks!
The problem is that $interval returns a promise, and each time you run $scope.startTimer you are creating a new promise. When you run it a second time, it doesn't cancel the previous promise, it just re-assigns $scope.counter to the new promise.
Check out the AngularJS $interval page to see their example and method of avoiding this problem.
A simple solution is to check to see whether the var you're assigning your promise to has already been defined when you are about to create a new promise. For example:
if ( angular.isDefined($scope.counter) ) return;
You can also use a boolean var to maintain the status as to whether it has been started or not if you prefer that.
I'm running into a problem with AngularJS where I use a callback on a custom service in order to pass data from one controller to another.
In particular, I'm trying to create a very simple logging service in which one controller registers a listener function with the service to receive updates when a message is logged, and the other controller logs a message when a button in the UI is clicked (triggering the listener in the first controller).
The listener function then updates a $scope.messages variable to show the newly logged message in the UI. However, while the newly arrived message arrives in the listener function, the UI is not updated.
I believe this is because AngularJS is not aware that $scope.messages is being updated.
However, trying to wrap the update of $scope.messages with $scope.$apply has brought no avail ($apply already in progress).
HTML Code:
<div ng-app="myApp">
<div ng-controller="ButtonCtrl">
<button type="button" ng-click="logMessage()">Button</button>
</div>
<div id="console" ng-controller="ConsoleCtrl">
<div ng-repeat="consoleItem in messages">{{consoleItem.message}}</div>
</div>
</div>
Javascript Code:
var module = angular.module('myApp', []);
module.factory('MyConsole', function($rootScope) {
var listeners = [];
var myConsoleService = {};
myConsoleService.log = function(messageObj) {
angular.forEach(listeners, function(listener){
listener(messageObj);
});
};
myConsoleService.registerListener = function(listener) {
listeners.push(listener);
};
return myConsoleService;
});
function ConsoleCtrl($scope, MyConsole){
$scope.messages = [{"message":"First message!"}];
MyConsole.registerListener( function(msgObj){
// while this console.log call below works as expected, the $scope.messages.unshift call does not update the view
$scope.messages.unshift(msgObj.message);
console.log($scope.messages);
});
}
function ButtonCtrl($scope, MyConsole) {
$scope.logMessage = function () {
MyConsole.log({message:"myLogmessage"});
};
}
Code can also be found for your convenience on JSFiddle: http://jsfiddle.net/G5LAY/3/
Any help would be greatly appreciated :-)
Thanks!
Your code is working, it's just that this line: $scope.messages.unshift(msgObj.message); only adds the message string to $scope.messages when your template expects $scope.messages to be a list of objects instead. So changing it to $scope.messages.unshift(msgObj); should do the trick.