AngularJS : service $broadcast and $watch not triggering on receiving controller - javascript

AngularJS noob here, on my path to the Angular Enlightenment :)
Here's the situation:
I have implemented a service 'AudioPlayer' inside my module 'app' and registered like so:
app.service('AudioPlayer', function($rootScope) {
// ...
this.next = function () {
// loads the next track in the playlist
this.loadTrack(playlist[++playIndex]);
};
this.loadTrack = function(track) {
// ... loads the track and plays it
// broadcast 'trackLoaded' event when done
$rootScope.$broadcast('trackLoaded', track);
};
}
and here's the 'receiver' controller (mostly for UI / presentation logic)
app.controller('PlayerCtrl', function PlayerCtrl($scope, AudioPlayer) {
// AudioPlayer broadcasts the event when the track is loaded
$scope.$on('trackLoaded', function(event, track) {
// assign the loaded track as the 'current'
$scope.current = track;
});
$scope.next = function() {
AudioPlayer.next();
};
}
in my views I show the current track info like so:
<div ng-controller="PlayerCtrl">
<button ng-click="next()"></button>
// ...
<p id="info">{{current.title}} by {{current.author}}</p>
</div>
the next() method is defined in the PlayerCtrl, and it simply invokes the same method on the AudioPlayer service.
The problem
This works fine when there is a manual interaction (ie when I click on the next() button) - the flow is the following:
PlayerCtrl intercepts the click and fires its own next() method
which in turn fires the AudioPlayer.next() method
which seeks the next track in the playlist and calls the loadTrack() method
loadTrack() $broadcasts the 'trackLoaded' event (sending out the track itself with it)
the PlayerCtrl listens the broadcast event and assigns the track to the current object
the view updates correctly, showing the current.title and current.author info
However, when the next() method is called from within the AudioService in the 'background' (ie, when the track is over), all the steps from 1 to 5 do happen, but the view doesn't get notified of the change in the PlayerCtrl's 'current' object.
I can see clearly the new track object being assigned in the PlayerCtrl, but it's as if the view doesn't get notified of the change. I'm a noob, and I'm not sure if this is of any help, but what I've tried is adding a $watch expression in the PlayerCtrl
$scope.$watch('current', function(newVal, oldVal) {
console.log('Current changed');
})
which gets printed out only during the 'manual' interactions...
Again, like I said, if I add a console.log(current) in the $on listener like so:
$scope.$on('trackLoaded', function(event, track) {
$scope.current = track;
console.log($scope.current);
});
this gets printed correctly at all times.
What am I doing wrong?
(ps I'm using AudioJS for the HTML5 audio player but I don't think this is the one to blame here...)

When you have a click event the $scope is updated, without the event you'll need to use $apply
$scope.$apply(function () {
$scope.current = track;
});

As it's not safe to peek into the the digest internals, the easiest way is to use $timeout:
$timeout(function () {
$scope.current = track;
}, 0);
The callback is executed always in the good environment.
EDIT: In fact, the function that should be wrapped in the apply phase is
this.loadTrack = function(track) {
// ... loads the track and plays it
// broadcast 'trackLoaded' event when done
$timeout(function() { $rootScope.$broadcast('trackLoaded', track); });
};
Otherwise the broadcast will get missed.
~~~~~~
Actually, an alternative might be better (at least from a semantic point of view) and it will work equally inside or outside a digest cycle:
$scope.$evalAsync(function (scope) {
scope.current = track;
});
Advantage with respect to $scope.$apply: you don't have to know whether you are in a digest cycle.
Advantage with respect to $timeout: you are not really wanting a timeout, and you get the simpler syntax without the extra 0 parameter.

// apply changes
$scope.current = track;
try {
if (!$scope.$$phase) {
$scope.$apply($scope.current);
}
} catch (err) {
console.log(err);
}

Tried everything, it worked for me with $rootScope.$applyAsync(function() {});

Related

Angular $scope confusion with $watch

I'm still in the beginning stages of my Angular 1.0 journey. I'm learning to like it, but I'm still scratching my head in a few places.
Lately, I've run into something while using $watch that's left me confounded. Take a look:
$scope.$watch('cookies', function() {
if ($cookies.getAll().redditSession) {
$scope.$emit('cookiesChanged')
// $scope.userWelcome = cookieService.decodeCookie($cookies.get('redditSession'))
}
})
$scope.$on('cookiesChanged', function() {
$scope.userWelcome = cookieService.decodeCookie($cookies.get('redditSession'))
})
This code works. If my cookies change, I emit an event thereby triggering the event listener, which changes the value of $scope.userWelcome to some value stored in the cookie. I see this change if I navigate to another route in my app.
However, I'm wondering why I had to use an event emitter here? Notice the line I commented out. I tried this first, but it doesn't change value of $scope.userWelcome, even if I move to another page in my app. I have to reload the page in order to see that I'm logged in.
What's going on here?
Try watching the cookie directly:
$scope.$watch(
function () {
return $cookies.get('redditSession');
},
function (newValue) {
if (newValue) {
$scope.userWelcome = cookieService.decodeCookie(newValue);
};
}
);
Your mistake is that you try to get the new value with the standard method. The way you can actually get the new value is adding it to the parameters of the function. Here it goes:
$scope.$watch('cookies', function(newValue, oldValue) {
if (newValue.getAll().redditSession) {
$scope.userWelcome = cookieService.decodeCookie(newValue.get('redditSession'))
}
// also try this
console.log(oldvalue === $cookies);
});
Cheers!

Using WebSpeech API through AngularJS

I have been trying to use the WebSpeech Api through angularjs. Everything seems to work but the model doesn't get updated at once.
If I start the recognition again, the model updates. Seems like some inner loop/other construct is holding angular to see the changes.
Here is the codepen that I made.
Steps to reproduce:
1. Click start, and speak
2. After recognition detects end of speech hit start once again to start another recognition.
3. As soon as the second recognition is started, the model is updated with previous transcript.
Note: If a do console.log as below then it shows correct transcript, means the recognition part is working fine.
if(event.results[i].isFinal) {
self.final = self.final.concat(event.results[i][0].transcript);
console.log(event.results[i][0].transcript);
}
Everything seems perfect, except you forgot to call $scope.$apply(); when you modified values to get it effect on view. So it should be like this,
angular.module('speech',[]);
angular.module('speech').controller('speechController', function($scope) {
this.rec = new webkitSpeechRecognition();
this.interim = [];
this.final = '';
var self = this;
this.rec.continuous = false;
this.rec.lang = 'en-US';
this.rec.interimResults = true;
this.rec.onerror = function(event) {
console.log('error!');
};
this.start = function() {
self.rec.start();
};
this.rec.onresult = function(event) {
for(var i = event.resultIndex; i < event.results.length; i++) {
if(event.results[i].isFinal) {
self.final = self.final.concat(event.results[i][0].transcript);
console.log(event.results[i][0].transcript);
$scope.$apply();
} else {
self.interim.push(event.results[i][0].transcript);
}
}
};
});
I have updated your codepen with working solution.
AngularJs creates a "watch" internally for the all data-bindings created in view and call $scope.$digest() which inturns iterate thorugh all watches and checks if any of the watched variables have changed. When you call $scope.$apply() it internally calls $scope.$digest() so data-binding gets refreshed.
Listener directives, such as ng-click, register a listener with the DOM. When the DOM listener fires, the directive executes the associated expression and updates the view using the $apply() method.
When an external event (such as a user action, timer or XHR) is received, the associated expression must be applied to the scope through the $apply() method so that all listeners are updated correctly (ref).
So in your case view gets update when you click next start button again (ng-click) and not when recording event occurs.
Also would be usefult to read this

How to suppress a watch statement or digest cycle in Angular

Currently I have a text input attached to a model with a $scope.watch statement observing the model. This is all used to achieve a type of auto complete / typeahead feature.
<!-- HTML -->
<input type="text" ng-model="search.mySearchText">
// JS
var deregister = $scope.$watch('search.mySearchText', doSearch);
function doSearch() {
mySearchService.executeSearch(search.mySearchText)
.then(function(res) {
// do something with the data
});
}
This works fine. However, occasionally in my .then function I want to make changes to search.mySearchText. Obviously this would cause the watcher to be fired again, but I don't want this.
What I'm hoping to do is find a way to suppress the $watch from firing that next time. Maybe by somehow telling Angular that that particular watched model property is no longer dirty?
I tried removing the $watch by de/re- registering the watch at appropriate times, but that didn't work either.
function doSearch() {
mySearchService.executeSearch(search.mySearchText)
.then(function(res) {
deregister(); // shut off the watch
search.mySearchText = 'some new string'; // manipulate the model property that I don't want to cause a search
deregister = $scope.$watch('search.mySearchText', doSearch);
});
}
However, this didn't prevent the event firing like I expected, which is why I'm now looking for a way to suppress the event.
You could have a variable that determines whether doSearch exits early, like so:
var searchActive = true;
function doSearch() {
if (!searchActive) return;
mySearchService.executeSearch(search.mySearchText)
.then(function(res) {
searchActive = false;
// do manipulation of search.mySearchText
searchActive = true;
});
}

AngularJS - triggering $watch/$observe listeners

I would like to fire all $watch/$observe listeners even if watched/observed value didn't change. This way I could provide a "testing only" feature to refresh current page/view without user interaction. I've tried to call $apply/$digest but that didn't worked:
$timeout(function(){
$scope.$apply();
});
$timeout(function(){
$scope.$digest();
});
Is there any other way to do it?
Best Regards,
Executing $scope.$apply() will trigger digest cycle as it internally calls $digest, below is example of manual change.
number variable won't get bound as timeout brings it out of angulars scope.
setTimeout(function () {
$scope.number = Math.random();
});
however you can "force" it to show up by manually applying scope changes:
setInterval(function () {
$scope.$apply();
}, 100);
Demos:
No change / Change with manual updates
This will not trigger watchers though. From $digest implementation, it checks if value has changed since the last watch evaluation and will run callback only if it did.
if ((value = watch.get(current)) !== (last = watch.last) ... [rootScope.js]
Therefore you will need somehow change value of the last execution and it's possible to do via $$watchers object on the scope:
$scope.digest = function () {
setTimeout(function () {
angular.forEach($scope.$$watchers, function (w) {
w.last.value = Math.random();
});
$scope.$apply();
});
}
DEMO
How about to use the $emit function then capture that event with $on function?
Within an $emit() event function call, the event bubbles up from the child scope to the parent scope. All of the scopes above the scope that fires the event will receive notification about the event.
We use $emit() when we want to communicate changes of state from within our app to the rest of the application.
_.bind($scope.$emit, $scope, 'myCustomEvent');
then on the capture phase:
$scope.$on('myCustomEvent', function() {
// do something
});

How to run function in AngularJS controller on document ready?

I have a function within my angular controller, I'd like this function to be run on document ready but I noticed that angular runs it as the dom is created.
function myController($scope)
{
$scope.init = function()
{
// I'd like to run this on document ready
}
$scope.init(); // doesn't work, loads my init before the page has completely loaded
}
Anyone know how I can go about this?
We can use the angular.element(document).ready() method to attach callbacks for when the document is ready. We can simply attach the callback in the controller like so:
angular.module('MyApp', [])
.controller('MyCtrl', [function() {
angular.element(document).ready(function () {
document.getElementById('msg').innerHTML = 'Hello';
});
}]);
http://jsfiddle.net/jgentes/stwyvq38/1/
See this post How to execute angular controller function on page load?
For fast lookup:
// register controller in html
<div data-ng-controller="myCtrl" data-ng-init="init()"></div>
// in controller
$scope.init = function () {
// check if there is query in url
// and fire search in case its value is not empty
};
This way, You don't have to wait till document is ready.
Angular has several timepoints to start executing functions. If you seek for something like jQuery's
$(document).ready();
You may find this analog in angular to be very useful:
$scope.$watch('$viewContentLoaded', function(){
//do something
});
This one is helpful when you want to manipulate the DOM elements. It will start executing only after all te elements are loaded.
UPD: What is said above works when you want to change css properties. However, sometimes it doesn't work when you want to measure the element properties, such as width, height, etc. In this case you may want to try this:
$scope.$watch('$viewContentLoaded',
function() {
$timeout(function() {
//do something
},0);
});
Angular initializes automatically upon DOMContentLoaded event or when
the angular.js script is evaluated if at that time document.readyState
is set to 'complete'. At this point Angular looks for the ng-app
directive which designates your application root.
https://docs.angularjs.org/guide/bootstrap
This means that the controller code will run after the DOM is ready.
Thus it's just $scope.init().
The answer
$scope.$watch('$viewContentLoaded',
function() {
$timeout(function() {
//do something
},0);
});
is the only one that works in most scenarios I tested. In a sample page with 4 components all of which build HTML from a template, the order of events was
$document ready
$onInit
$postLink
(and these 3 were repeated 3 more times in the same order for the other 3 components)
$viewContentLoaded (repeated 3 more times)
$timeout execution (repeated 3 more times)
So a $document.ready() is useless in most cases since the DOM being constructed in angular may be nowhere near ready.
But more interesting, even after $viewContentLoaded fired, the element of interest still could not be found.
Only after the $timeout executed was it found. Note that even though the $timeout was a value of 0, nearly 200 milliseconds elapsed before it executed, indicating that this thread was held off for quite a while, presumably while the DOM had angular templates added on a main thread. The total time from the first $document.ready() to the last $timeout execution was nearly 500 milliseconds.
In one extraordinary case where the value of a component was set and then the text() value was changed later in the $timeout, the $timeout value had to be increased until it worked (even though the element could be found during the $timeout). Something async within the 3rd party component caused a value to take precedence over the text until sufficient time passed. Another possibility is $scope.$evalAsync, but was not tried.
I am still looking for that one event that tells me the DOM has completely settled down and can be manipulated so that all cases work. So far an arbitrary timeout value is necessary, meaning at best this is a kludge that may not work on a slow browser. I have not tried JQuery options like liveQuery and publish/subscribe which may work, but certainly aren't pure angular.
I had a similar situation where I needed to execute a controller function after the view was loaded and also after a particular 3rd-party component within the view was loaded, initialized, and had placed a reference to itself on $scope. What ended up working for me was to setup a watch on this scope property and firing my function only after it was initialized.
// $scope.myGrid property will be created by the grid itself
// The grid will have a loadedRows property once initialized
$scope.$watch('myGrid', function(newValue, oldValue) {
if (newValue && newValue.loadedRows && !oldValue) {
initializeAllTheGridThings();
}
});
The watcher is called a couple of times with undefined values. Then when the grid is created and has the expected property, the initialization function may be safely called. The first time the watcher is called with a non-undefined newValue, oldValue will still be undefined.
Here's my attempt inside of an outer controller using coffeescript. It works rather well. Please note that settings.screen.xs|sm|md|lg are static values defined in a non-uglified file I include with the app. The values are per the Bootstrap 3 official breakpoints for the eponymous media query sizes:
xs = settings.screen.xs // 480
sm = settings.screen.sm // 768
md = settings.screen.md // 992
lg = settings.screen.lg // 1200
doMediaQuery = () ->
w = angular.element($window).width()
$scope.xs = w < sm
$scope.sm = w >= sm and w < md
$scope.md = w >= md and w < lg
$scope.lg = w >= lg
$scope.media = if $scope.xs
"xs"
else if $scope.sm
"sm"
else if $scope.md
"md"
else
"lg"
$document.ready () -> doMediaQuery()
angular.element($window).bind 'resize', () -> doMediaQuery()
If you're getting something like getElementById call returns null, it's probably because the function is running, but the ID hasn't had time to load in the DOM.
Try using Will's answer (towards the top) with a delay. Example:
angular.module('MyApp', [])
.controller('MyCtrl', [function() {
$scope.sleep = (time) => {
return new Promise((resolve) => setTimeout(resolve, time));
};
angular.element(document).ready(function () {
$scope.sleep(500).then(() => {
//code to run here after the delay
});
});
}]);
Why not try with what angular docs mention https://docs.angularjs.org/api/ng/function/angular.element.
angular.element(callback)
I've used this inside my $onInit(){...} function.
var self = this;
angular.element(function () {
var target = document.getElementsByClassName('unitSortingModule');
target[0].addEventListener("touchstart", self.touchHandler, false);
...
});
This worked for me.
$scope.$on('$ViewData', function(event) {
//Your code.
});

Categories

Resources