How to suppress a watch statement or digest cycle in Angular - javascript

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

Related

Trigger Click from AngularJS Controller

I'm using a request header with a key and a value to read from the AngularJS controller. If that header, in this case, is AppHeaders is valid and has a custom value, I need to trigger a click from that controller. The code that I have, if something like this:
$scope.$applyAsync(function() {
if (appHeaders != null && appHeaders['header-name'] != null) {
if (appHeaders['header-name'] == "custom-value") {
$('.class-name').click();
}
}
});
What's wrong? I make a deep debug into this and the conditional works fine. I guess that the problem is because the element on the DOM doesn't exist when the click is fired.
Thanks for your help guys! The final solution results on apply the $broadcast event on the functions declared and use a counter to validate the last cycle of the calls to trigger the click on the element.
// Here we declare an empty array to store each request
var requests = [];
// Our first function
$scope.firstFunction = function() {
// Execute broadcast event with the name of the function
$scope.$broadcast('requestEnded',requests.push('firstFunction'));
};
// Our last function
$scope.secondFunction = function() {
// Execute broadcast event with the name of the function
$scope.$broadcast('requestEnded', requests.push('secondFunction'));
};
// This listener is executed each time that requestEnded is fired
$scope.$on('requestEnded', function(event, countRequests) {
// Here we validate that the count of requests is the desire
if (countRequests == 2) {
// Trigger Click
$('.class-selector').click();
}
});
I write a post with the entire research explaining this:
https://jbrizio.github.io/2017/10/20/Trigger-click-when-determinate-requests-finish-using-AngularJS.html

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!

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 :)

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

Binding multiple events of the same type?

Firstly, is it possible? Been struggling with this one for hours; I think the reason my events aren't firing is because one event is unbinding/overwriting the other. I want to bind two change events to the same element. How can I do that?
As per request, here's the function I'm struggling with:
(function($) {
$.fn.cascade = function(name, trigger, url) {
var cache = {};
var queue = {};
this.each(function() {
var $input = $(this);
var $trigger = $input.closest('tr').prev('tr').find(trigger);
//$input.hide();
var addOptions = function($select, options) {
$select.append('<option value="">- Select -</option>');
for(var i in options) {
$select.append('<option value="{0}">{1}</option>'.format(options[i][0], options[i][1]));
}
$select.val($input.val()).trigger('change');
}
var $select = $('<select>')
// copy classes
.attr('class', $input.attr('class'))
// update hidden input
.bind('change', function() {
$input.val($(this).val());
})
// save data for chaining
.data('name', name)
.data('trigger', $trigger);
$input.after($select);
$trigger.bind('change', function() {
var value = $(this).val();
$select.empty();
if(value == '' || value == null) {
$select.trigger('change');
return;
}
// TODO: cache should be a jagged multi-dimensional array for nested triggers
if(value in cache) {
addOptions($select, cache[value]);
} else if(value in queue) {
$select.addClass('loading');
queue[value].push($select);
} else {
var getDict = {}
getDict[name] = value;
// TODO: use recursion to chain up more than one level of triggers
if($(this).data('trigger')) {
getDict[$(this).data('name')] = $(this).data('trigger').val();
}
$select.addClass('loading');
queue[value] = [$select];
$.getJSON(url, getDict, function(options) {
cache[value] = options;
while(queue[value].length > 0) {
var $select = queue[value].pop();
$select.removeClass('loading');
addOptions($select, options);
}
});
}
}).trigger('change');
});
return this;
}
})(jQuery);
The relevant chunk of HTML is even longer... but essentially it's a select box with a bunch of years, and then an <input> that gets (visibly) replaced with a <select> showing the vehicle makes for that year, and then another <input> that gets replaced with the models for that make/year.
Actually, it seems to be running pretty well now except for on page load. The initial values are getting wiped.
Solved the issue by pulling out that $select.bind() bit and making it live:
$('select.province').live('change', function() {
$(this).siblings('input.province').val($(this).val());
});
$('select.make').live('change', function() {
$(this).siblings('input.make').val($(this).val());
});
$('select.model').live('change', function() {
$(this).siblings('input.model').val($(this).val());
});
Sucks that it's hard-coded in there for my individual cases though. Ideally, I'd like to encapsulate all the logic in that function. So that I can just have
$('input.province').cascade('country', 'select.country', '/get-provinces.json');
$('input.make').cascade('year', 'select.year', '/get-makes.json');
$('input.model').cascade('make', 'select.make', '/get-models.json');
Yes that is possible.
$(…).change(function () { /* fn1 */ })
.change(function () { /* fn2 */ });
jQuery event binding is additive, calling .change a second time does not remove the original event handler.
Ryan is correct in jQuery being additive, although if you find there are problems because you are chaining the same event, beautiful jQuery allows another approach, and that is calling the second function within the first after completion of the first as shown below.
$('input:checkbox').change(function() {
// Do thing #1.; <-- don't forget your semi-colon here
(function() {
// Do thing #2.
});
});
I use this technique frequently with form validation, one function for checking and replacing disallowed characters input, and the second for running a regex on the results of the parent function.
Update to Post:
OK... You all are quick to beat on me with your negative scores, without understanding the difference in how we each view Mark's request. I will proceed to explain by example why my approach is the better one, as it allows for the greatest flexibility and control. I have thrown up a quick example at the link below. A picture's worth a 1000 words.
Nested Functions on One Event Trigger
This example shows how you can tie in three functions to just one change event, and also how the second and third functions can be controlled independently, even though they are still triggered by the parent change event. This also shows how programmatically the second and third functions can BOTH be tied into the same parent function trigger, yet respond either with or independently (see this by UNCHECKING the checkbox) of the parent function it is nested within.
$('#thecheckbox').change(function() {
$("#doOne").fadeIn();
if ($('#thecheckbox').attr('checked')) { doFunc2() }
else { doFunc3() };
function doFunc2() { $("#doTwo").fadeIn(); return true; }
function doFunc3() { $("#doTwo").fadeOut(); return true; }
$("#doThree").fadeIn();
});
I've included the third 'Do thing #3 in the example, to show how yet another event can follow the two nested functions as described earlier.
Forgive the earlier bad pseudocode originally posted first, as I always use ID's with my jQuery because of their ability to give everything an individual status to address with jQuery. I never use the 'input:checkbox' method in my own coding, as this relies on the 'type' attribute of an input statement, and therefore would require extra processing to isolate any desired checkbox if there is more than one checkbox in the document. Hopefully, the example will succeed at articulating what my comments here have not.
I am actually not sure exactly if you can bind two different change events. But, why not use logic to complete both events? For example...
$('input:checkbox').change(function() {
// Do thing #1.
// Do thing #2.
});
That way, you get the same benefit. Now, if there are two different things you need to do, you may need to use logic so that only one or the other thing happens, but I think you would have to do that anyway, even if you can bind two change events to the same element.

Categories

Resources