Can anyone explain this small piece of code? - javascript

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.

Related

Angular directive parent scope

I'm building a directive that decorates a table header item and sort the data upon the click event. I cannot apply the changes on the data model to the parent scope using the directive scope.
vm.data is an array on the parent scope that contains the data I want to sort in the directive.
After the click the data object in the directive has changed but the parent is still in the same order.
I dont want to access the parent scope using $parent, What I'm missing ??
<th sortable="browser" data="vm.data">Browser</th>
directive code:
angular
.module("app")
.directive("sortable", ['lodash', sortableDirective]);
function sortableDirective(lodash) {
return {
restrict: "A",
scope:{
data:"="
},
controller:function($scope){
},
link: function (scope, element, attributes) {
var sorted = undefined;
var col = attributes['sortable'];
var oldClass = 'sorting'
attributes.$$element.addClass(oldClass);
$(element).on("click", sort);
function changeClass(){
if(sorted=='asc'){
attributes.$$element.removeClass(oldClass);
attributes.$$element.addClass('sorting_asc');
oldClass = 'sorting_asc';
}
else if(sorted=='desc'){
attributes.$$element.removeClass(oldClass);
attributes.$$element.addClass('sorting_desc');
oldClass='sorting_desc';
}
}
function sort() {
if (sorted == 'asc') {
sorted = 'desc';
}
else {
sorted = 'asc';
}
scope.data = lodash.sortBy(scope.data, function (o) {
return o[col];
});
if (sorted == 'desc') {
lodash.reverse(scope.data);
}
changeClass();
}
}
};
}
This is because you are using jQuery to listen to change on the element. So just change this line:
$(element).on("click", sort);
to
element.on("click", sort);
The 2nd attribute i.e. element is already an instance of jQlite if jQuery is not available and will be an instance of jQuery if jQuery is available.
In any case, there is a method available .on which will be executed on the value change. Since you again wrapped it to $(), the Angular was not getting notified of the change in the data.
Edit:
On the 2nd walk through of your code, I see the actual problem. You are reassigning the complete scope.data in the sort() method which is breaking the pass by reference behavior of Javascript (or in any OOPS programming).
The pass by reference will only work if you continue to modify your SAME reference variable. Noticed the word, SAME?? By writing scope.data = lodash.sortBy(scope.data, function (o) {}) you removed the reference of the actual data passed to the directive. Hence the values are not updated.
So to fix this problem, you have a few options:
Change your sorting code to not reassign the complete scope.data variable RECOMMENDED (use inbuilt sort method)
Pass the modified data to the parent scope using scope.$emit()
Or use the $parent property which you don't want to use
The bidirectional binding will update the parent on each digest cycle but the click handler needs to invoke that digest cycle with $apply:
link: function (scope, element, attributes) {
var sorted = undefined;
var col = attributes['sortable'];
var oldClass = 'sorting'
element.addClass(oldClass);
//REPLACE this
//$(element).on("click", sort);
//
//WITH this
element.on("click", function (e) {
sort();
scope.$apply();
});
function changeClass(){
if(sorted=='asc'){
element.removeClass(oldClass);
element.addClass('sorting_asc');
oldClass = 'sorting_asc';
}
else if(sorted=='desc'){
element.removeClass(oldClass);
element.addClass('sorting_desc');
oldClass='sorting_desc';
}
}
The ng-click directive automatically invokes $apply but when the click event is handled by AngularJS jqLite, the code needs to notify the AngularJS framework.
From the Docs:
$apply([exp]);
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
-- AngularJS $rootScope.scope API Reference -- $apply

When to use worker and factory function in angularjs

i am learning angularjs from Pro angularjs by adam freeman.
This is an example he defined an directive in angularjs
myApp.directive("highlight", function () {
return function (scope, element, attrs) {
if (scope.day == attrs["highlight"]) {
element.css("color", "red");
}
};
});
the first function the writer calls is factory function and it returns a worker function. I' m not able to understand why he returns another function.
When i write the code below, the code returns an error.
I don't know what i'm doing wrong.
myApp.directive("highlight", function (scope, element, attrs) {
if (scope.day == attrs["highlight"]) {
return element.css("color","red")
}
});
If you take a look at the Angular docs about directives, you'll see that there are two ways you can define a directive. One is to use the directive definition object and the other is to just return a postLink function.
The example in your book shows the latter form. The reason your function doesn't work is because Angular calls your function and expects it to return a directive definition object or a postLink function. Instead the function you defined returns nothing or the result of element.css("color", "red") depending on your if statement.

Angular scope watch doesn't work in my app

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.

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.

When does ngClass evaluate functions / How to mimic this in custom directive

I'm hoping someone can offer some Angular insight that I am sorely lacking right now.
ngClass allows you to add/remove a class depending on some function. For example, if I write:
<div ng-class="{'some-class' : myFunction()}"> </div>
then .some-class will be added/removed if myFunction() evaluates to truthy or falsey. What is especially good is that I don't need to worry about this function getting called anywhere else in my code for the expression to evaluate. ngClass just takes care of keeping the class up to date with the current return value of myFunction().
So my first question is how does ngClass determine when to check if the return value of myFunction() has changed? Does it check myFunction() on every digest loop? Something presumably has had a watch set on it, but what is being watched? I tried reading the code for ngClass but I didn't have enough background knowledge to get a handle on how it's implemented.
The reason I want to know this is that I want to mimic this behaviour in a custom directive. So in my app I have something like the above ngClass. myFunction() is sitting on the main controller for my page, but it does a bunch of direct DOM manipulation so it shouldn't be there. I'd like to take myFunction() out of the controller and move it into a custom directive, but if I do that then I need some way to repeatedly check against this function in order to set classes. I would like to be able to do something like this:
app.directive('myDirective', function () {
return {
restrict: 'A,
link: function (scope, elem, attrs) {
function myFunction() {//...}
// I want this to be checked at the
// same frequency that ngClass is checked
// Is that each digest cycle?
if (myFunction()) {
elem.addClass('some-class');
} else {
elem.removeClass('some-class');
}
}
}
Obviously this isn't going to work. It's only going to run when the directive gets linked. So how can I watch myFunction() for changes?
I would use .$watch(), which evaluates the initial argument during every $digest cycle, and invokes a listener (the second argument) once during initial load and subsequently, every time the evaluation of the initial argument returns a different value than the previous.
See https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch.
Base on the psuedo-code you provided (actual code would have been nicer for testing purposes :-) here's an example of what you should do:
app.directive('myDirective', function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
scope.$watch(function() {
// .. this is your "myFunction()"
return result;
}, function(newResult) {
if (newResult) {
elem.addClass('some-class');
} else {
elem.removeClass('some-class');
}
});
}
}
}
Also, make sure you have the closing quote on the 'A' value.

Categories

Resources