Modifying scope not working without $scope.$apply - javascript

I have a directive for a button, which when clicked displays a loading screen.
angular.module('randomButton', ['Data'])
.directive('randomButton', function(Data) {
return {
scope: '=',
restrict: 'A',
templateUrl: 'templates/components/randomButton.html',
link: function(scope, element, attrs){
element.click(function(){
scope._loading();
});
}
}
});
It does this by calling another function on the scope contained within a different directive:
angular.module('quoteContainer', [])
.directive('quoteContainer', function(Data) {
function quoteController($scope){
$scope._loading = function(){
console.log('loading');
$scope.loadMessage = Data.getRandomText();
$scope.loading = true;
};
}
return {
scope: '=',
restrict: 'A',
templateUrl: 'templates/components/quoteContainer.html',
controller: ['$scope', quoteController],
link: function(scope,element,attrs){
}
}
});
My problem is, for this change to occur, I'm having to call $scope.$apply within the ._loading() function. For example:
$scope._loading = function(){
console.log('loading');
$scope.$apply(function(){
$scope.loadMessage = Data.getRandomText();
$scope.loading = true;
});
};
I understand this is bad practice, and should only be used when interfacing with other frameworks/ajax calls etc. So why does my code refuse to work without $scope.$apply, and how can I make it work without it?

Basically you need to let angular know some way of the change happening, because since it's in an asynchronous event handler, angular's usual mechanisms for noticing changes are not applying to it.
Hence the need to wrap it in $apply, which triggers a digest cycle after your code has run, giving angular a chance to make changes according to the new data. However, the preferred way of doing this is using angular's built-in $timeout service, which effectively does the same (i.e. wrapping your code in an $apply block), but doesn't have problems with the possibility that an other digest cycle might be ongoing when it's triggered.
You can use it the same way as you're currently using $apply:
$timeout(function(){
$scope.loadMessage = Data.getRandomText();
$scope.loading = true;
});
(It can take a second parameter if you actually want to delay the application of the values, but it's not necessary in your case.)

Related

Angularjs watch is not triggering

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

$animate.removeClass not working without $evalAsync inside directive?

I have created a directive that fade out and fade in the view on model changes.
app.controller('myCtrl', function($scope, $interval) {
$scope.number = 0;
$interval(function() {
$scope.number++;
}, 1000);
});
app.directive('fadeOnChange', function($timeout, $animate) {
return {
restrict: 'E',
//We output the value which will be set between fades
template: '{{theFadedValue.nv}}',
link: function(scope, elem, attr) {
//The scope variable we will watch
var vtw = attr.valueToWatch;
//We add the anim class to set transitions
$animate.addClass(elem, 'anim');
//We watch the value
scope.$watch(vtw, function(nv) {
//we fade out
var promise = $animate.addClass(elem, 'fade-it-out');
//when fade out is done, we set the new value
promise.then(function() {
scope.$evalAsync(function() {
scope.theFadedValue = {"nv": nv};
//we fade it back in
$animate.removeClass(elem, 'fade-it-out');
});
});
})
}
};
});
And here's the view
<div ng-controller="myCtrl">
<h1><fade-on-change value-to-watch="number"></fade-on-change></h1>
</div>
It works perfectly but I'd like to understand why I need to use $apply, $digest, $timeout or $evalAsync to wrap my call to $animate.removeClass for it to work? If I don't, the class simply does not get removed (which caused me a lot of headaches this afternoon).
I red about those four methods and understand how they are different, but the need to use one of those in this case mystifies me.
plunker
Basically async methods are don't run $digest cycle directly.(exceptional case for $http because it internally wraps inside $scope.$apply())
In your case you are waiting for complete promise which async. That's why you could use $timeout(function(){ }) or $evalAsync, This angular service wrap inside $scope.$apply() & $scope.$apply() internally run the digest cycle and all scope variables gets updated.
$evalAsync run after the DOM has been manipulated by Angular, but
before the browser renders
Link here for more information on when to use $scope.$apply()
Hope this will clear your doubts. Thanks.

Using $compile in a directive triggers AngularJS infinite digest error

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.

Can anyone explain this small piece of code?

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.

How to write directive that hooks into ng-click handler

I'm facing a situation in Angular where I sense I'm doing something wrong but I can't find the Angular way to solve it.
I'm developing a mobile app. I would like to create a directive let's call it cc-tap-highlight that would be used in conjunction with ng-click. Meaning that I could use it like this:
<a ng-click="doSomething()" cc-tap-highlight>Click me</a>
What this would do is to add a class to the clicked element and remove it after some seconds.
Now one could say, let's just manually bind to the elements click event in the directive. That would work for a desktop app. However, on mobile devices Angular does a lot of magic for us to have fast tap/clicks:
https://github.com/angular/angular.js/blob/master/src/ngMobile/directive/ngClick.js
For sure, I don't want to reimplement all of it's magic!
So, currently, instead of having my cc-tap-highlight directive I use this rather hackish approach:
In the view
<a ng-click="doSomething($event)" cc-tap-highlight>Click me</a>
In the controller:
$scope.doSomething = function($event){
//do your things with $event.currentTarget
}
There are two major problems with this approach:
the controller should not manipulate the DOM
We need to repeat the patter over and over through our entire code base violating DRY
However, I can't for the life of me, figure out how to write a directive that hooks into the ng-click handler and does it's things.
You can try to make your directive generate a ng-click directive with wrapper function.
Here's a quick example. It's by far not thoroughly tested but I think the principle is sound. What you want is your custom code to run before/after the click event regardless of how that's triggered(tap,click, whatever).
This does have the drawback that it creates a new scope so interaction with other directives that may need isolate scope was not tested.
DIRECTIVE
app.directive('myClick', ['$parse','$compile', function($parse, $compile) {
return {
restrict: 'A',
compile : function(tElement, tAttrs, transclude) {
//you can call the wrapper function whatever you want.
//_myClick might be more appropriate to indicate it's not really public
tElement.attr('ng-click', 'myClick($event)');
tElement.removeAttr('my-click');
var fn = $parse(tAttrs['myClick']);
return {
pre : function(scope, iElement, iAttrs, controller) {
console.log(scope, controller);
scope.myClick = function(event) {
console.log('myClick.before');
fn(scope, {$event:event});
console.log('myClick.after');
};
$compile(iElement)(scope);
},
post : function postLink(scope, iElement, iAttrs, controller) {
}
};
},
scope : true
};
}]);
CONTROLLER
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.test = function($event) {
console.log('test', $event);
};
//this is to show that even if you have a function with the same name,
//the wrapper function is still the one bound thanks to the new scope
$scope.myClick = function() {
console.log('dummy my click');
};
});
HTML
<button ng-click="test($event)">NG-CLICK</button>
<button my-click="test($event)">MY-CLICK</button>
<button ng-click="myClick($event)">MY-CLICK-DUPLICATE-FN</button>

Categories

Resources