angular weirdness with element.click and $location.search - javascript

Here is the problem
<div id="my-id" ng-click="moveOn(10)">Click me</div>
In the directive I have
scope.moveOn = function (val) {
$location.search('id', val);
}
And finally in the parent of this directive I listen for a change of this id
$scope.$watch(function () {
return $routeParams.id;
}, function (newId, oldId) {
...
});
This setup works great, after the $location.search is called the $watcher is triggered immediately. But now I also have a directive which does it slightly different, as follows:
element.find('#my-id').click(function (val) {
$location.search('id', val);
});
In the template there is no ng-click!
In this situation I can also see that the call to $location.search is made, but now it takes a very long time (a couple of seconds) before the watcher goes off.
So for some reason there must be a difference between ngClick and binding to a click event. Any suggestions what might be going on here ?

You are updating angular within an event that is outside of angular.
Try using $apply to notify angular of the change so it can run a digest
element.find('#my-id').click(function (val) {
scope.$apply(function(){
$location.search('id', val);
});
});

Related

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

Debouncing ng-keypress

Does anyone have any pointers for me to debounce the keypress event in angular? I can't get it to debounce. And I know for sure because I'm using $log.debug to print out the keys pressed, and the amount of times it fires off is not at the debounce rate.
I have set it up like this:
<div ng-keypress="doSomething"></div>
and in my controller (not that I have included underscore.js to utilize its debounce method in this instance):
...
$scope.doSomething = function(event, keyEvent) {
var keypressed = String.fromCharCode(keyEvent.which).toUpperCase();
_.debounce(handleKeyPress(keypressed), 500);
}
function handleKeyPress(keypressed) {
//do something with the keypress
}
Thanks for your help in advance.
Try the following code:
$scope.doSomething = _.debounce(function(event, keyEvent) {
$scope.$apply(function() {
// Do something here
});
}, 500);
Working Plunker
As #Enzey said, _.debounce() returns a "debounced" function that needs to be called somewhere to have any effect. And you need to call $apply() in order to trigger a digest cycle. Otherwise any changes on the model made within the debounced function won't update the view.
Update
It turned out that what the OP really wanted was a throttled function. Below is another code snippet using _.throttle():
$scope.doSomething = _.throttle(function($event) {
if ($scope.$$phase) return; // Prevents 'digest already in progress' errors
$scope.$apply(function() {
// Do something here
});
}, 100);

In AngularJS, how do I add a $watch on the URL hash?

Using Angular, how can I add a watch that is fired when the hash is updated, either by my application, or when the browser updates the URL from either the URL bar or the back/forward buttons?
$scope.$watch accepts function as the first argument, so you can do this:
$scope.$watch(function () {
return location.hash
}, function (value) {
// do stuff
});
But I would recommend using events, such as $routeChangeSuccess for default router:
$scope.$on("$routeChangeSuccess", function () {})
or $stateChangeSuccess for ui-router
$scope.$on("$stateChangeSuccess", function () {})
$locationChangeSuccess could be better.
$scope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl){
// TODO What you want on the event.
});
This works too if you're okay with not using the angular $watch.
Basically, you watch 'hashchange' windows event. whatever angularJS does is a wrapper around this. For example,
$($window).bind('hashchange', function () {
// Do what you need to do here like... getting imageId from #
var currentImageId = $location.search().imageId;
});
location.hash can be updated internally by angular or externally by the user or the browser (click link in bookmarks). If it is updated internally angular runs a $scope.$apply(). If an external event updates location.hash $watch does only fire UNTIL AFTER the next $scope.$apply(), e.g. a user pushes a button within your app.
If you wish to use a $watch, add an additional event listener to "hashchange" to call $apply, or add all functionality to the native DOM listener AND do not forget to call $apply(), as this is an external call.
window.addEventListener("hashchange", function(){ $scope.$apply(); }, false);
OR ...
window.addEventListener("hashchange", function(){ me._locationHashChanged(); $scope.$apply(); }, false);

Updating attrs value inside directive - how to do it in AngularJS

In a nutshell:
I try to do something like this inside my directive - namely change the value of model that is liked to 'trigger' attribute:
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
and it does not work. Why? Should I do it other way around?
And here is full story:
I have a dialog that is triggered when showRemoveDialog flag is set. This flag is set when user clicks Remove button.
Here is a dialog's opening tag:
<div remove-dialog trigger="{{showRemoveDialog}}" class="modal fade" id="myModal">
Then I have a directive removeDialog:
myApp.directive("removeDialog", function () {
return {
restrict: 'A',
link: function (scope, element, attrs, controller) {
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
attrs.$observe('trigger', function (newValue) {
if (newValue) {
angular.element(element).modal('show');
} else {
angular.element(element).modal('hide');
}
});
},
controller: 'DeleteController'
};
});
As you can see, I observe trigger attribute and when it changes to true (user clicks Remove button), I show the dialog:
$scope.remove = function () {
$scope.showRemoveDialog = true;
};
And it works.
But if the value of trigger changes to false/null I want to close it - for instance Cancel button was clicked, or X icon was clicked. And if one of these two actions occur, I need to set back trigger value to false/null, so that the next time when user click on Remove button value would change from false -> true, and my dialog appears once again.
The problem is that this piece of code does not work:
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
I mean it does not set the value of {{showRemoveDialog}} in scope to null. I already tried $apply function, but still in wain.
I guess I'm doing something really wrong in angular. Please help.
Yes, the idea you have come up with is kind of confusing, changing the attribute will not actually change the scope variable, so to fix this you would have to change the scope variable, in this case you know what the variables name is so it would work, but for other elements you might not know what the variable is. To fix this specific issue you would have to do.
scope.showRemoveDialog = null;
scope.$apply();
This is not very dynamic though. Here is what I would do (not tested).
Pass the variable name in as a string
trigger="showRemoveDialog"
Then in your directive get some help from $parse
myApp.directive("removeDialog", function ( $parse ) { ....
The link function...
link: function (scope, element, attrs, controller) {
var variableName = attrs.trigger;
angular.element(element).on('hidden.bs.modal', function () {
$parse(variableName + ' = null')(scope);
scope.$apply(); // Might not be needed.
});
scope.$watch(variableName, function (newValue) {
if (newValue) {
angular.element(element).modal('show');
} else {
angular.element(element).modal('hide');
}
}, true); // true might not be needed.
},
Also you don't need to do angular.element(element) as the element passed to the link function should already be wrapped.
The first argument to the jQuery on() method is the event you're listening for. I've never seen it used with custom events before, only standard Javascript ones like "keydown". So my first question would be have you tested that the event hook is ever called? If not put a console.log("event called"); before you try to set your element's trigger attribute.
Another thing I would mention is that setting an attribute to null like that wont work. Have a look at the AngularJS source code . Instead I would set the attribute to false.
Lastly I would recommend just using the Angular UI Bootstrap library that includes a nice modal feature - or something else, I don't mind but reinventing the wheel here seems unnecessary.

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