Angular $scope confusion with $watch - javascript

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!

Related

Unable to trigger change after changing value in select box

I'm trying to change a value in a dropdown box and then trigger the event that happens when the value changes. The code is quite simple and looks like this:
zScalerEndPointList.on("change", function(){
// Change the value
$("select#phase1_type").val("string:ddns");
// Trigger the change
$("select#phase1_type").trigger("change");
})
The value is successfully changed, but the event is not triggered. If logging the object with jQuery I can see that the attached change event is null. However, if running the following command manually in the Javascript console window it works:
$("select#phase1_type").trigger("change");
Any ideas on what could be wrong? I do not own the application, this is a Javascript executed via TamperMonkey.
Grateful for any input.
/Patrik
I am not sureI got you but you can use watch for the dorpdown list or anything similar to this
$scope.$watch(function () { return self.filter; }, function (newValue, oldValue) {
if (newFilter != oldFilter) {
// call trigger
}
}, true);
Turns out this is a duplicate question. That I've asked before. A bit awkward. :)
Tampermonkey: Trigger event does not work for element
Solved it with the following function:
function triggerEvent(e, s){
"use strict";
var event = document.createEvent('HTMLEvents');
event.initEvent(e, true, true);
try {
document.querySelector(s).dispatchEvent(event);
} catch(err){
console.log("Unable to fire event " + e + " on selector " + s);
console.log(err);
}
}
Looks like both web applications is using some transpiled framework which makes life hard for me by removing and re-adding elements to the DOM. Hope this helps someone else too.
/Patrik

Keep element in focus after state changed

In my AngularJS/Ui-Router application I have a series of input box like this:
<input ng-model="vm.filter" ng-keyup="vm.onKeyUp($event)" type="text" class="form-control" id="filter">
Then I have a controller, in which I have put the onKeyUp function, that looks like this:
var vm = this;
vm.onKeyUp = function (e) {
var val = e.currentTarget.value;
$state.go('aState', { 'filter': val });
}
This will fire the state change (really just state parameter filter is changed) that will call a resolve behind the scene and at the end of the flow, my data set is filtered and the subset is showed. Amazing.
But there is a little problem: on the state changed the HTML input lost its focus, and I can't figure out how can set it on focus at the end of the flow.
This is necessary since the user start to write in the input box and then will be surprised when his focus get "misteriously" lost. Bad UI experience.
Ok for what can i read you just call the same state with different parameters on the url (:filter), what happens even if the state is the same is that the stateChange force a reload of the view and therefore the lost of the focused element.
And that is the behaviour unless you make a event.preventDefault() on $stateChangeStart but im pretty sure your data will not be updated.
So possible solutions are imho:
Rethink your flow so it doenst depend on state change. (No need to code here :P)
Find a way to save your active element and set it on reload, like.
//sessionStorage to prevent data stored after window is closed;
$scope.$on('$stateChangeStart', function(){
$window.sessionStorage.setItem('focusedId', '#' + document.activeElement.id);
});
$scope.$on('$viewContentLoaded', function(){
if((var id = $window.sessionStorage.getItem('focusedId'))) {
$DOM.focus(id);
//Use a service for DOM manipulation, you know manipulation in controllers is bad :(
}
});
`
the service could be like this:
.factory('$DOM', ['$window', '$timeout', function ($window, $timeout) {
return {
__constructor: function (selector, event) {
$timeout(function () {
var element = (typeof selector == 'object') ?
selector :
$window.document.querySelector(selector);
(!!element && !!element[event]) ? element[event]() : null;
});
},
focus: function (selector) {
this.__constructor(selector, 'focus');
},
blur: function (selector) {
this.__constructor(selector, 'blur');
}, //and so....
};
}])
comment if you find a better way, maybe you can refresh the data preventing the refresh of the view and my understanding of ui.router is wrong :)

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;
});
}

$rootScope.$watch triggered twice

I want to trigger a new Chat once the variable newChat is being changed like this:
$rootScope.newChat = {
roomId: roomId,
friendId: friendId
};
in my ChatController I $watch the variable like this:
$rootScope.$watch('newChat', function (data) { /*do stuff*/ }
this works on my page after the first reload of the page without any problems. But for the first load this $watch gets triggered twice which causes issues on some other parts of the chat.
I checked the value of newChat. Both times the value is exactly the same. No other parts of my application use the $rootScope.newChat Variable
Why is that and how can I fix this?
Every watch will get triggered when a $digest cycle runs. What you need to do is check the new value vs. the old value.
$rootScope.$watch('newChat', function (newValue, oldValue) {
if(newValue !== oldValue){
/*do stuff*/
}
});
I fixed this problem like this
$rootScope.$watch('someValue', function (){
/* rewrite into a new scope */
$scope.someValue = $rootScope.someValue;
});
$scope.$watch('someValue', function (){/*To do*/});

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

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() {});

Categories

Resources