Web worker with Angular not updating view - javascript

I have two files 1) app.js 2) worker.js
I try to update the $scope.time but it is not showing in the view. It is my first time with webworkers.
app.js
angular.module('App', [])
.controller('MainCtrl', ['$scope', '$window', function($scope, $window) {
$scope.time = 100;
var worker = new Worker('worker.js');
worker.onmessage = function(e) {
$scope.time = e.data.time;
};
worker.postMessage($scope.time);
}]);
worker.js
self.onmessage = function(e) {
var time = e.data;
var timer = setInterval(toDo,1000);
function toDo(){
time = time-1;
postMessage({
time:time
});
}
}

When worker.onmessage is triggered it is going to be outside the Angular digest cycle. So even though you have updated the model, Angular does not know that it needs to update the views. In order for you to notify Angular that a new digest cycle has to happen you need to call $scope.$apply()
worker.onmessage = function(e) {
$scope.$apply(function(){
//do model changes here
$scope.time = e.data.time;
});
};
Instead of passing an anonymous function to $scope.$apply you could just do the changes and then call $scope.$apply() with no arguments. But I believe it is preferred that you use the anonymous function with $apply as it does under the hood work like wrapping it in try...catch.
$scope.time = e.data.time;
$scope.$apply();

Related

Angular countdown service

I am trying to create an Angular service which uses a web worker to change countdown variable using set interval.
What I want to do is to show the count down in the view.
I can easily do this by putting all the code in controller, which works but I got struck in creating the service
I am struck. I dont know how to proceed.
I have tried this plunkr here
script.js
angular.module('app', []).
controller('mainCtrl', mainCtrl);
function mainCtrl($scope,timer) {
$scope.time = 100;
console.log(timer.timeValue.time);
}
mainCtrl.$inject = ['$scope','timer'];
timer.js
angular.module('app')
.service('timer', timer);
function timer() {
var time;
this.timeValue = function(value) {
var worker = new Worker('worker.js');
worker.onmessage = function(e) {
//console.log('From Main:'+ e.data.time);
time = e.data.time;
};
worker.postMessage(time);
return time;
};
}
worker.js
angular.module('app')
.service('timer', timer);
function timer() {
var time;
this.timeValue = function(value) {
var worker = new Worker('worker.js');
worker.onmessage = function(e) {
//console.log('From Main:'+ e.data.time);
time = e.data.time;
};
worker.postMessage(time);
return time;
};
}
What I want to do is like this. This is my earlier plunk.This do the same thing using controller.
plunkr here
I found out why it's not working with your code. Just for the record, a countdown is not something you want to do with a Webworker, but anyway!
First of all in timer.js:
angular.module('app')
.service('timer', timer);
timer.$inject=['$rootScope']
function timer($rootScope) {
this.timeValue = function(value) {
var time = value;
var worker = new Worker('worker.js');
worker.onmessage = function(e) {
time = e.data.time;
$rootScope.$broadcast('timerUpdate', time)
};
worker.postMessage(time);
};
}
You have to start the var time with a value.
I injected $rootScope to the service, so i can $broadcast a message back to the main scope.
In the main script I did this:
function mainCtrl($scope,timer) {
function init() {
timer.timeValue(100);
}
$scope.time = 100;
$scope.$on('timerUpdate', function(event, time) {
$scope.$apply(function() {
$scope.time = time;
})
})
init();
}
mainCtrl.$inject = ['$scope','timer'];
So, i made a Init function that gets triggered once in the beginning. That triggers your service into making a webworker.
Once the webworker gives back the message(time). The timerService sends out a $rootScope.$broadcast picked up by $scope.$on().
The $scope.$apply is not really the best thing to have in a simple script like this, but it's the only thing that will force digest(Angular page update) the page and give the $scope.time a new value.
and last the webworker:
self.onmessage = function(e) {
var time = e.data;
var timer = setInterval(toDo, 1000);
function toDo() {
time--;
postMessage({
time: time
});
}
}
(Only thing i did was change time = time - 1 to time--; (shorthand version, looks beter !)
Hope this helps !
(also, just for the record, try no to use the $rootScope or the $scope.$apply function! It's not the best way to do stuff I hear, but I'm also new to Angular and haven't found anything beter for these things..)
And the plunker:
https://plnkr.co/edit/7IoGxFaaqQRH4AErGenl?p=preview

Loading view configuration

I would like to do something like this:
app.config(function($routeProvider){
$routeProvider.when('products/list', {
controller: 'ProductListCtrl',
templateUrl : 'products/list/view.html',
resolve : { data : function(){
...
},
loadingTemplateUrl : 'general/loader.html'
}
});
I would like to have the loading page in a different view.
This would make the code in the view and controller of every page cleaner, (no <...ng-include ng-show="loading"...>). This would also mean that I don't have to $scope.$watch the data for changes. Is there a clean solution to do something similar (not necessarily in the .config method) or an alternative library to do this?
Assuming you want to show some general template for all state transitions while the data is resolved, my suggestion is to listen to the events fired by the routing library. This allows to use one central point to handle all state transitions instead of polluting the routing config (which I think will not be that easy to do).
Please see the docs for $routeChangeStart, $routeChangeSuccess and of course $routeChangeError at the angular router docs
Maybe someone could be interested in what I did: I created a new service and a new view directive. It could seem like a lot of work, but doing this was much easier than I had expected. The new service enables me to separate the main view from the loading view, that I could reuse in all pages of the application. I also provided the possibility to configure an error template url and error controller, for when the loading failed.
The Angular $injector, $templateRequest and $controller services do most of the work. I just had to connect a directive, that depends on these services, to the right event ($locationChangeSuccess), and to the promise, retrieved (using $q.all) from the resolve object's functions. This connection was done in the route service. The service selects the right template url and comtroller, and passes it on for the directive to handle.
A shortened version (with the getCurrentConfig method left out):
RouteService:
(function () {
'use strict';
// provider:
angular.module('pikcachu')
.provider('pikaRouteService', [function () {
var routeConfigArray;
var otherwiseRouteConfig;
//configuration methods
this.when = function (url, routeConfig){
routeConfigArray.push({url: url, routeConfig: routeConfig});
return this;
}
this.otherwise = function(routeConfig){
otherwiseRouteConfig = routeConfig;
return this;
}
// service factory:
this.$get = ['$rootScope', '$location', '$q', '$injector', '$templateRequest',
function ($rootScope, $location, $q, $injector, $templateRequest) {
function RouteService() {
this.setViewDirectiveUpdateFn = function(){ /*...*/ }
function init(){
$rootScope.$on('$locationChangeSuccess', onLocationChangeSuccess);
}
function onLocationChangeSuccess(){
// get the configuration based on the current url
// getCurrentConfig is a long function, because it involves parsing the templateUrl string parameters, so it's left out for brevity
var currentConfig = getCurrentConfig($location.url());
if(currentConfig.resolve !== undefined){
// update view directive to display loading view
viewDirectiveUpdateFn(currentConfig.loadingTemplateUrl, currentConfig.loadingController);
// resolve
var promises = [];
var resolveKeys = [];
for(var resolveKey in currentConfig.resolve){
resolveKeys.push(resolveKey);
promises.push($injector.invoke(resolve[resolveKey]));
}
$q.all(promises).then(resolveSuccess, resolveError);
function resolveSucces(resolutionArray){
// put resolve results in an object
var resolutionObject = {};
for(var i = 0; i< promises.length;++i){
resolved[resolveKeys[i]] = resolutionArray[i];
}
viewDirectiveUpdateFn(currentConfig.errorTemplateUrl, currentConfig.errorController);
}
function resolveError(){
viewDirectiveUpdateFn(currentConfig.errorTemplateUrl, currentConfig.errorController);
}
}
}
init();
}
return new RouteService();
}]
})();
View directive
(function () {
'use strict';
angular.module('pikachu')
.directive('pikaView', ['$templateRequest', '$compile', '$controller', 'pikaRouteService', function ($templateRequest, $compile, $controller, pikaRouteService) {
return function (scope, jQdirective, attrs) {
var viewScope;
function init() {
pikaRouteService.listen(updateView);
}
function updateView(templateUrl, controllerName, resolved) {
if(viewScope!== undefined){
viewScope.$destroy();
}
viewScope = scope.$new();
viewScope.resolved = resolved;
var controller = $controller(controllerName, { $scope: viewScope });
$templateRequest(templateUrl).then(onTemplateLoaded);
function onTemplateLoaded(template, newScope) {
jQdirective.empty();
var compiledTemplate = $compile(template)(newScope);
jQdirective.append(compiledTemplate);
}
}
init();
};
}
]);
})();

Update variable in every tick using setInterval() with AngularJS

I'm trying to count up a variable every x seconds in JS using setInterval() and show it in my view binding this variable the Angular way. The problem is, in the model the var is counted up but the progress is just shown as soon as I stop the Interval. How can I update the var in the view on every tick?
<span>{{number}}</span>
and:
$scope.number = 0;
$scope.interval;
$scope.run = function(){
$scope.interval = setInterval(function(){
$scope.number++;
}, 1000);
};
$scope.stop = function(){
clearInterval($scope.interval);
}
Fiddle
You should be using Angular's implementation of setInterval called $interval.
Not only will this will ensure any code within the callback calls a digest, but it will also help you easily test your code:
$scope.run = function() {
$scope.interval = $interval(function() {
$scope.number++;
}, 1000);
};
$scope.stop = function() {
$interval.cancel($scope.interval);
};
I would also avoid attaching your interval variable to the $scope. I can't see any reason your view would need to be aware of it. A private var interval in the controller scope would suffice.
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', function ($scope, $interval) {
$scope.number = 0;
$scope.run = function (){
$scope.interval = $interval(function(){
$scope.number++;
}, 1000);
};
$scope.stop = function() {
$interval.cancel($scope.interval);
};
});

Cannot get Karma/Jasmine to work with my angular controller

Ok. I have spent several hours trying in vain to get Karma to work with my Angular controller. Whatever I do, I get the following error. It seems that even if I remove the expectGET() calls, I still get the error; as soon as I call $http.flush();
TypeError: Cannot set property 'totalBeforeDiscounts' of undefined
The code for my controller is as follows:
var quotePadControllers = angular.module('quotePadControllers', []);
quotePadControllers.controller('QuotesController', ['$scope', '$http', '$q', function($scope, $http, $q){
var blankAddon;
// Setup initial state and default values
var ajaxGetAddOns = $http.get('/?ajax=dbase&where=aons'),
ajaxGetFrames = $http.get('/?ajax=dbase&where=fcats');
$q.all([ajaxGetAddOns, ajaxGetFrames]).then(function(results){
$scope.addons = results[0].data;
$scope.frames = results[1].data;
$scope.pairs = [
{
"frames" : angular.copy($scope.frames),
"addons" : angular.copy($scope.addons),
}
];
});
// Function for the 'add pair' button
$scope.addPair = function()
{
$scope.pairs.push({
"frames" : angular.copy($scope.frames),
"addons" : angular.copy($scope.addons)
});
};
// Function for the 'remove pair' button
$scope.removePair = function()
{
if ( $scope.pairs.length > 1 )
{
$scope.pairs.pop();
}
};
// Continually update the subtotal and total
$scope.$watch('pairs', function(pairs) {
var totalBeforeDiscounts = 0;
angular.forEach(pairs, function(pair) {
var subTotal = 0;
angular.forEach(pair.addons, function(addon) {
subTotal += addon.added ? addon.price : 0;
});
subTotal += pair.currentFrame !== undefined ? pair.currentFrame.price : 0;
pair.subTotal = subTotal;
totalBeforeDiscounts += subTotal;
});
pairs.totalBeforeDiscounts = totalBeforeDiscounts;
}, true);
}]);
and my test code:
describe('QuotesController', function()
{
beforeEach(module('quotePadApp'));
var ctrl, $scope, $http, frameCatsHandler, addOnsHandler, createController;
// Setup tests
beforeEach(inject(function($controller, $rootScope, $httpBackend, _$q_) {
$scope = $rootScope.$new();
$http = $httpBackend;
frameCatsResponse = [{"id":145,"price":25,"brand":"mybrand"},
{"id":147,"price":45,"brand":"mybrand"},
{"id":148,"price":69,"brand":"mybrand"}];
addOnsHandler = [{"id":1,"name":"addon1","price":30,"includeIn241":0,"description":null},
{"id":2,"name":"addon2","price":60,"includeIn241":0,"description":null}];
frameCatsHandler = $http.when('GET', '/?ajax=dbase&where=fcats').respond(frameCatsResponse);
addOnsHandler = $http.when('GET', '/?ajax=dbase&where=aons').respond(addOnsHandler);
createController = function()
{
return $controller('QuotesController', {'$scope' : $scope });
};
}));
it('Should request frame cats and addons from the database', function()
{
$http.expectGET('/?ajax=dbase&where=aons');
$http.expectGET('/?ajax=dbase&where=fcats');
createController();
$http.flush();
});
});
This is because you have the following watch statement in your controller trying to set a totalBeforeDiscounts property on $scope.pairs.
$scope.$watch('pairs', function(pairs) {
// ...
pairs.totalBeforeDiscounts = totalBeforeDiscounts;
}, true);
In your tests, when you create the controller and then call $http.flush(), that's actually triggering a $digest cycle. This kicks off all watchers.
createController();
$http.flush();
The watch handler above will execute and since it executes before $scope.pairs has any value, the pairs argument passed into the watch handler is undefined, resulting in your error.
As per the documentation:
After a watcher is registered with the scope, the listener fn is
called asynchronously (via $evalAsync) to initialize the watcher. In
rare cases, this is undesirable because the listener is called when
the result of watchExpression didn't change. To detect this scenario
within the listener fn, you can compare the newVal and oldVal. If
these two values are identical (===) then the listener was called due
to initialization.
https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch
Also, in the rest of your code you have $scope.pairs as an array, but in the watch you are trying to set a property like totalBeforeDiscounts on it. This doesn't look right.

AngularJS Factory $rootScope.$on Cleanup

How do I cleanup $rootScope.$on event subscriptions from inside a service?
I have a Factory that is being initialized in different controllers in my AngularJS application, where a $rootScope.$on event subscription is being declared. My problem is, when the controller is destroyed, the event is not cleaned up. I have done reading and found that with controllers and directives you can setup a $watch on $destroy for cleanup, but what about for services? How do you clean up services?
Here's a basic Plunker of my problem: http://plnkr.co/edit/dY3BVW. Click the 'create child' button a bunch of time to initialize a child controller that will initialize a factory with the $rootScope.$on. With the browser console open when you the click 'broadcast', you will see a bunch of event subscriptions being fired, event after the child controllers were destroyed.
Snippet from Plunker:
// setup rootScope on in Factory and create multiple instances
// to show that event subscriptions are not cleaned up
// TODO: how do you cleanup in factory?
app.factory('HelloWorldFactory', function($rootScope) {
// setup event subscription on initialization
function HelloWorldFactory() {
console.log('init helloWorldFactory');
var rootScopeOn = $rootScope.$on('test', function() {
console.log('test sub', Math.random());
});
}
return HelloWorldFactory;
});
// child ctrl will init a new factory instance
app.controller('ChildCtrl', function($scope, HelloWorldFactory) {
$scope.name = 'Child';
var helloWolrdFactory = new HelloWorldFactory();
});
You can kill the sentinel before creating the new event. The rootScopeOn variable will stay untouched after creating new controllers, so you can kill it every time you initialize it again.
app.factory('HelloWorldFactory', function($rootScope) {
var rootScopeOn;
// setup event subscription on initialization
function HelloWorldFactory() {
console.log('init helloWorldFactory');
if (typeof rootScopeOn === 'function') {
rootScopeOn();
}
rootScopeOn = $rootScope.$on('test', function() {
console.log('test sub', Math.random());
});
}
return HelloWorldFactory;
});
// child ctrl will init a new factory instance
app.controller('ChildCtrl', function($scope, HelloWorldFactory) {
$scope.name = 'Child';
var helloWolrdFactory = new HelloWorldFactory($scope);
});

Categories

Resources