I've got the following code where I'm trying to show/hide a text box based on if a key was pressed in the global window scope. However, every time a key is pressed, it does not seem to fire the watch service. Why is this?
Plnkr here http://plnkr.co/edit/qL9ShNKegqJfnyMvichk
app.controller('MainCtrl', function($scope, $window) {
$scope.name = '';
angular.element($window).on('keypress', function(e) {
//this changes the name variable
$scope.name = String.fromCharCode(e.which);
console.log($scope.name)
})
$scope.$watch('name', function() {
console.log('hey, name has changed!');
});
});
It is because you are handling the keypress event outside of the digest cycle. I would strongly encourage you to let angular do its thing with databinding or using ngKeypress
Otherwise, in your handler, call $scope.$digest().
angular.element($window).on('keypress', function(e) {
//this changes the name variable
$scope.name = String.fromCharCode(e.which);
console.log($scope.name);
$scope.$digest();
})
On a high level view, watching a value on a scope needs two parts:
First: the watcher - like you created one. Every watcher has two parts, the watch function (or like here the value) and the listener function. The watch function returns the watched object, the listener function is called when the object has changed.
Second: the $digest cycle. The $digest loops over all watchers on a scope, calls the watch function, compares the returned newValue with the oldValue and calls the corresponding listener function if these two do not match. This is called dirty-checking.
But someone has to kick the $digest. Angular does it inside its directives for you, so you don't care. Also all build-in services start the digest. But if you change the object outside of angular's control you have to call $digest yourself, or the preferred way, use $apply.
$scope.$apply(function(newName) {
$scope.name = newName;
});
$apply first evaluates the function and then starts the $digest.
In your special case, I would suggest to use ngKeypress to do it the angular way.
Related
I have three elements to this program. First, in the service, I have:
$scope.loaded = MySvc.loaded;
$scope.loaded = false;
Then, in the controller which imports this service, I have a series of introductory things. I need to trigger events in the directives once the controller is done with its asycnhronous work, so in the controller, when that trigger is ready, I do
MySvc.loaded = true;
And then in the directive, which also imports the service, I have,
$scope.loaded = MySvc.loaded;
$scope.$watch('loaded', function (newValue, oldValue) {
The directive triggers when loaded is initialized to 'false', but when I change the value to 'true', nothing happens. The watch simply does not trigger. Why not? How do I fix this?
This is a fine way to do things, and I view it as orthogonal to promises (which are indeed extremely useful). Your problem comes from breaking the linkage you've created with the assignment. Instead try:
$scope.data = MySvc.data;
And append to that, e.g. MySvc.data.loaded = false. This way the data variable is never reassigned, so your linkage between controller and service stays intact.
Then you can either watch data.loaded or watch data as a collection by passing true as the 3rd option to $watch.
As I said in comment, you may have problems with yours scopes overridding the loaded property.
Try using the angular events to solve your problem.
angular.module('test', [])
.controller 'MyController', ($scope, $timeout) ->
$timeout ->
$scope.$broadcast('READY')
, 2000
.directive 'myDirective', ->
scope: {}
template: '<span>{{ value }}</span>'
link: ($scope) ->
$scope.value = "I'm waiting to be ready..."
$scope.$on 'READY', ->
$scope.value = "I'm ready!!"
See this in action (CoffeeScript as it's faster for prototyping but should be clear enough).
You're making this complicated. Use promises instead!!! They're sweet. Promises keep state of their resolution.
.factory('hello_world', function ($q, $timeout) {
var promise = $q.defer;
$timeout(function () {
promise.resolve('hello world');
}, 1000)
return promise.promise;
})
.controller('ctrl', function ($scope, hello_world) {
hello_world.then(function (response) {
console.log("I will only be ran when the async event resolves!");
console.log(response);
});
});
As you can see promises are much better, no watches. No weird gates. Simpler, and sexy. ;)
Any thoughts on why this directive is triggering an infinite digest error?
http://jsfiddle.net/smithkl42/cwrgLd0L/13/
var App = angular.module('prettifyTest', []);
App.controller('myCtrl', function ($scope) {
$scope.message = 'Hello, world!';
})
App.directive('prettify', ['$compile', function ($compile) {
var template;
return {
restrict: 'E',
link: function (scope, element, attrs) {
if (!template) {
template = element.html();
}
scope.$watch(function () {
var compiled = $compile(template)(scope);
element.html('');
element.append(compiled);
var html = element.html();
var prettified = prettyPrintOne(html);
element.html(prettified);
});
}
};
}]);
It seems to be the very first line in the scope.$watch() function that's triggering the model update, as when I remove that line, it doesn't trigger the error.
var compiled = $compile(template)(scope);
I'm a little confused as to why that line is triggering another $digest - it doesn't seem to be updating anything directly in the scope.
Is there a better way to accomplish what I'm trying to do, e.g., some other way to check to see if the key values in the scope have actually changed so I can recompile the template? (And is there a better way of grabbing the template?)
When you use scope.$watch() with just a function and no watch expression, it registers a watcher that gets triggered on every digest cycle. Since you're calling $compile within that watcher, that's effectively triggering another digest cycle each time since it needs to process the watchers created by your template. This effectively creates your infinite digest cycle.
To use the same code, you should probably just be compiling once in your postLink function, but I don't think you even need to do that - you should be able just use the template property. Then your $watch() statement should include an expression targeting the property you want to watch for changes - in this case, just 'message', and update the HTML accordingly.
I am using a directive with a ngModelController like this
var directive = {
link: link,
restrict: 'E',
require: '?ngModel'
}
My link function does something like this (I omitted the long directive stuff)
function link(scope, element, attrs, ctrl) {
scope.updateModel = function(){
var newModel = {};
newModel.aValue = 'foo';
ctrl.$setViewValue(newModel);
}
}
I use the directive in my controller
View: this is correctly updated
<my-directive ng-model="aModel">
{{aModel.aValue}} // This is changed correctly when I update the value in the directive
</my-directive>
Controller: here is where I have problems
$scope.aModel = {};
$scope.aModel.aValue = 'bar';
$scope.$watch('aModel', function(newValue, oldValue){
console.log('aModel changed'); // This is never fired
});
$scope.$watch('aModel.aValue', function(newValue, oldValue){
console.log('aModel changed'); // This is neither fired
});
The documentation of $setViewValue says
"Note that calling this function does not trigger a $digest."
so I tried to fire it manually with $render, $digest or $apply, but every time run into trouble since the console says that the $digest is already in progress.
So what's the problem with this?
UPDATE
I just realized something really interesting.
Adding a $timeout to check the value in the controller, I realized that it is not actually changed and that's probably why the $watch function is not called.
But since the view is updated, I don't really get the point.
This is the situation:
Controller
$timeout(function(){
console.log('Check aModel value in the controller');
console.log($scope.aModel.aValue); // This is always 'bar' even if the view display 'foo'
},10000);
UPDATE 2
I think I found the problem: ngModel cannot be an object but it must be a value.
I leave the question open to see if anyone comes with a different solution, but I guess that's the point.
UPDATE 3
I was wrong, you can update the ngModel as object, but there is no way to fire the $watch method.
I create a plunker to show the problem.
I put a large timeout to avoid any $digest in progress problem.
Any hint would be really appreciated.
Add third parameter true to $watch function.
Use $timeout with no time parameter in order to call $apply and trigger the digest loop. This will cause it to execute a fraction of a second later and not run into the digest loop in a digest loop error. Clearly what your doing is not 'angular aware' and so you need to trigger the digest loop using $apply but your in a digest loop so you need to use the 'hack' mentioned above.
I was looking at one of the custom implementations of ng-blur (I know it's already available in the standard AngularJS now). The last line is what I don't understand.
.controller('formController', function($scope){
$scope.formData = {};
$scope.myFunc = function(){
alert('mew');
console.log(arguments.length);
}
})
.directive('mew', function($parse){
return function(scope, element, attr){
var fn = $parse(attr['mew']);
element.bind('blur', function(event){
scope.$apply(function(){
fn(scope);
});
});
}
});
In the view there's a simple mew="myFunc()" applied to inputs.
My question is why are we passing the scope to the function in the very last line of the directive. I tried to make it work without that but it doesn't. What's actually happening?
Also this too works scope.$apply(attr.mew). Same reason or something different?
$parse only does just that, it parses the string passed in, you need to call the resulting function with the current scope because otherwise how else would it know which function to call?
scope.$apply works in the following manner:
The expression is executed using the $eval() method.
Any exceptions from the execution of the expression are forwarded to the $exceptionHandler service.
The watch listeners are fired immediately after the expression was executed using the $digest() method.
The reason scope.$apply(attr.mew) is due to the fact that it's doing all of the above. It is parsing, and then applying the result of the parse to the scope.
Another option is to use an isolate scope to bind your directive to the mew attr.
return {
scope: {
mew: '&'
},
link: function (scope, element, attr) {
var fn = scope.mew;
element.bind('blur', function (event) {
scope.$apply(function () {
fn();
});
});
}
}
Example
For this specific example it will work, but as you said, the blur is out of the digest loop. In most of the use cases the function will change data on one scope or another, and the digest loop should run and catch those changes.
I need to scroll to a specific anchor tag on page reload. I tried using $anchorScroll but it evaluates $location.hash(), which is not what I needed.
I wrote a custom provider based on the source code of $anchorScrollProvider. In it, it adds a value to the rootScope's $watch list, and calls an $evalAsync on change.
Provider:
zlc.provider('scroll', function() {
this.$get = ['$window', '$rootScope', function($window, $rootScope) {
var document = $window.document;
var elm;
function scroll() {
elm = document.getElementById($rootScope.trendHistory.id);
if (elm) elm.scrollIntoView();
}
$rootScope.$watch(function scrollWatch() {return $rootScope.trendHistory.id;},
function scrollWatchAction() {
if ($rootScope.trendHistory.id) $rootScope.$eval(scroll);
});
return scroll;
}];
});
Now, when I try to call the scroll provider in my controller, I must force a digest with $scope.$apply() before the call to scroll():
Controller:
//inside function called on reload
$scope.apply();
scroll();
Why must I call $scope.$apply()? Why isn't the scroll function evaluating in the Angular context when called inside the current scope? Thank you for your help!
I'm not sure what your thinking is behind using $rootScope.$eval(scroll) - since the scroll() function is already executing in a context where it has direct access to the $rootScope.
If I understand correctly, you want to be able to scroll to a particular element as denoted by an id which is stored in $rootScope.trendHistory.id.
When that id is changed, you want to scroll to that element (if it exists on the page).
Assuming this is a correct interpretation of what you are trying to achieve, here is how I might go about implementing it:
app.service('scrollService', function($rootScope) {
$rootScope.trendHistory = {};
$rootScope.$watch('trendHistory.id', function(val) {
if (val) {
elm = document.getElementById($rootScope.trendHistory.id);
if (elm) elm.scrollIntoView();
}
});
this.scrollTo = function(linkId) {
$rootScope.trendHistory.id = linkId;
}
});
This is a service (like your provider, but using the simpler "service" approach) which will set up a $watch on the $rootScope, looking for changes to $rootScope.trendHistory.id. When a change is detected, it scrolls to the element indicated if it exists - that bit is taken directly from your code.
So to use this in a controller, you'd inject the scrollService and then call its scrollTo() method with the ID as an argument. Example:
app.controller('AppController', function($scope, scrollService) {
scrollService.scrollTo('some_id');
});
In your question, you mention this needing to occur on reload, so you'd just put the call into your reload handler. You could also just directly modify the value of $rootScope.trendHistory.id from anywhere in the app and it would also attempt to scroll.
Here is a demo illustrating the basic approach: http://plnkr.co/edit/cJpHoSemj2Z9muCQVKmj?p=preview
Hope that helps, and apologies if I misunderstood your requirements.