AngularJS Passing function with arguments to event directive - angularjs

To start off, I know there are loads of similar questions but none that I found which supports execution of arbitrary methods with event listeners.
I have this directive which executes functions passed when the window resizes.
app.directive('onResize', function() {
var directive = {
'link': function(scope, element, attrs) {
var onResizeHandler = scope.$eval(attrs.onResize);
angular.element(window).on('resize', onResizeHandler);
angular.element(window).on('$destory', function() {element.off();});
}
};
return directive;
});
I can trigger the above directive with
<div data-on-resize="stickyHeader">...</div>
Which runs my method, inside my controller.
app.controller('myController', [$scope, function($scope) {
$scope.stickyHeader = function() {
console.log('event triggered')
};
}]);
All the above code works fine, but I need to pass some arguments to stickyHeader as in data-on-resize="stickyHeader(arg1, arg2)" When I try that, I get Cannot read property 'call' of undefined at ng (angular.js:3795) in the console. Not sure what I can do to make my directive support arbitrary methods with arguments.

The directive needs to evaluate the AngularJS expression defined by the on-resize attribute every time the events occurs:
app.directive('onResize', function($window) {
var directive = {
link: function(scope, elem, attrs) {
angular.element($window).on('resize', onResizeHandler);
scope.$on('$destroy', function() {
angular.element($window).off('resize', onResizeHandler);
});
function onResizeHandler(event) {
scope.$eval(attrs.onResize, {$event: event});
scope.$apply();
}
}
};
return directive;
});
Also since the resize event comes from outside the AngularJS framework, the event needs to be brought into the AngularJS execution context with $apply().
Further, to avoid memory leaks, the event handler needs to be unbound when the scope is destroyed.
Usage:
<div data-on-resize="stickyHeader($event)">...</div>
For more information, see AngularJS Developer Guide - $event.

Related

Why won't parent model get updated on Angular Directive function call?

http://jsfiddle.net/2dgzt18a/
I'm expecting the model on the parent to get updated when Enter is pressed in the input. But it does not. Output from console log looks promising, like it should do it. Do I need to use a $watch ? Doubt it, but thought I'd ask.
HTML
<div data-ng-app="testApp">
<div data-ng-controller="testCtrl">
<strong>{{pkey}}</strong>
<span data-test-directive
data-parent-item="pkey"
data-parent-update="update(pkey)"></span>
</div>
</div>
DIRECTIVE
var testApp = angular.module('testApp', []);
testApp.directive('testDirective', function ($timeout) {
return {
scope: {
key: '=parentItem',
parentUpdate: '&'
},
replace: true,
template: '<div><input type="text"></input></div>',
link: function(scope, elem, attrs) {
elem.bind('keydown keypress', function(event) {
if (event.which === 13) {
scope.parentUpdate({ pkey: 'D+' + scope.key});
event.preventDefault();
}
})
}
};
});
CONTROLLER
testApp.controller('testCtrl', function ($scope) {
$scope.pkey = 'golden';
$scope.update = function (k) {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.pkey, k);
$scope.pkey = 'C+' + k;
console.log('CTRL:', $scope.pkey);
};
});
I believe I have seen this work using a controller in a directive, but since I'm interested in a Keypress event, is why I need to use link.
elem.bind just binds js function to event, nothing else.
Add scope.$apply().
P.S. i.e. ng-click does nearly the same: binds event and call apply after callback.
P.S.1. If you can use more modern angular version - there are ng-keypress and similar directives.
scope.$apply is not preferred to use. It is better to use $timeout
The $timeout does not generate error like „$digest already in
progress“ because $timeout tells Angular that after the current cycle,
there is a timeout waiting and this way it ensures that there will not
any collisions between digest cycles and thus output of $timeout will
execute on a new $digest cycle

Angular class change directive doesn't work

I want to add a directive that will do something when an element class is changed.
My directive is:
(function (angular) {
angular.module('myApp')
.directive('myClassWatch', myClassWatch);
function myClassWatch() {
return {
restrict: 'A',
link: function (scope, element, attrs, controller) {
scope.$watch(function () {
return element.attr('class');
}, function (newValue, oldValue) {
debugger;
if(oldValue !== newValue)
console.log('class changed from ' + oldValue + ' to ' + newValue);
});
}
}
}
})(angular);
The html is:
<div class="top-icons-item popup-container popup-link-container" my-class-watch>
</div>
I do some actions elsewhere that toggles the class "open" in my div element - it is visible in the html - yet the debugger is never called (also no console logs of course). I can see that the link function is called on page load and the debugger also stops, but thats only on page load and not afterwards when I actually do actions that adds another class.
I have read several issued here before including Directive : $observe, class attribute change caught only once but I can't understand why I don't get the same result. What can I do to try check why this occures?
Update: The class change is made using jQuery not in a controller but in an old jquery watches code. May this be cause? could angular be unaware of class change when its not done from an angular code?
Wrap your jQuery code into $apply.
It similar to you are making changes to $scope out of angular context(jquery ajax, setTimeout, etc). Use $apply to make angular know about the changes done.
angular.element(document.getElementById('app')).injector().invoke(['$compile', '$rootScope', function($compile, $rootScope) {
$rootScope.$apply(function() {
//your jquery code goes here...
var a = document.getElementById('abc');
angular.element(a).addClass('hello');
});
}]);

Angularjs controller function vs directive function

Lately I've been building some modules and in some of them I only used controllers (controller is set within an existing directive I already need to use to load template) to have this comunnication between services and the view, for example:
$scope.callFunction = function(data) {
factRequest = saveData(data);
};
I also noticed I could do this from within a directive, like this:
link:function(scope) {
scope.callFunction = function(data) {
factRequest.saveData(data);
}
}
//or..
link:function(scope, element, attr) {
attrValue = attr.myValue;
element.bind('click', function(attrValue) {
factRequest.saveData(attrValue);
});
}
//or even..
link:function(scope, element, attr) {
attrValue = attr.myValue;
element.bind('click', function(attrValue) {
factRequest.saveData(attrValue);
});
var elButton = element.fin('span'); //for example
elButton.bind('click', function(attrValue) {
factRequest.saveData(attrValue);
});
}
Considering a scenario where this a reusable object, for example, a product where it display on multiple pages and have a commom function, such as addFavorite, addCart, addWishList, etc.. And also considering performance.
What is the difference between those call methods? And what is the best option to use as a call Function?
To restate, you are calling a service method on a click event and want to know where the best place to put that logic is.
Let's look at each of your examples:
Controller
angular.module('myApp').controller('MyController', function($scope, factRequest) {
$scope.callFunction = function(data) {
factRequest.saveData(data);
};
});
First of all, whenever I find myself injecting $scope into a controller I question my approach. This is because adding variables to the current scope creates hidden dependencies if you are relying using those variables in a child controller -- and is unnecessary if you are not.
Instead, you should be using the controllerAs syntax and adding the function to the controller itself. Something like this:
angular.module('myApp').controller('MyController', function(factRequest) {
var vm = this;
vm.callFunction = function(data) {
factRequest.saveData(data);
};
});
...and you would access it in your template like this:
<div ng-controller="MyController as vm">
<input ng-model="vm.data">
<button ng-click="vm.callFunction(vm.data)">
Click Me!
</button>
</div>
This is a perfectly good approach utilizing native Angular directives.
Directive w/ Link Function
angular.module('myApp').directive('myDirective', function(factRequest) {
return {
link: function(scope) {
scope.callFunction = function(data) {
factRequest.saveData(data);
}
}
};
});
Again, I don't like this because you are adding the function to scope. If you have a directive and want to expose some functionality to the template, you should use a controller. For example:
angular.module('myApp').directive('myDirective', function() {
return {
controller: 'MyDirectiveController',
controllerAs: 'myDir',
template: '<input ng-model="myDir.data">' +
'<button ng-click="myDir.callFunction(myDir.data)">' +
'Click Me!' +
'</button>'
};
}).controller('MyDirectiveController', function(factRequest) {
var myDir = this;
myDir.callFunction = function(data) {
factRequest.saveData(data);
}
});
This is essentially the same as the first example, except that it is now a reusable component.
Directive w/ Click Event Handler
angular.module('myApp').directive('myDirective', function(factRequest) {
return {
link: function(scope, element, attr) {
element.on('click', function() {
factRequest.saveData(scope.$eval(attr.myValue));
});
}
};
});
Notice I took a few liberties here. For one thing, an event handler function gets the event object as its first argument, so trying to pass attr.myValue wouldn't work. Also, I call scope.$eval(), which is a best practice that enables the use of Angular expressions in the myValue attribute.
I like this approach best, because it doesn't rely on the use of other directives like ng-click. In other words, this directive is more self-contained.
One thing I should add is that Angular will not remove this event listener when the element is removed from the DOM. It is a best practice to clean up after your directive, like this:
angular.module('myApp').directive('myDirective', function(factRequest) {
return {
link: function(scope, element, attr) {
function onClick() {
factRequest.saveData(scope.$eval(attr.myValue));
}
element.on('click', onClick);
scope.$on('$destroy', function() {
element.off('click', onClick);
});
}
};
});
Conclusion
From a performance perspective, all of these approaches are roughly equivalent. The first two don't add any watchers themselves, but ng-click and ng-model do so its six of one, half a dozen of the other.

AngularJS directive doesn't update scope value even with apply

I'm usin a directive to show a div on the screen only when the screen size is smaller than 600px. The problem is, the scope value isn't being updated, even using $apply() inside the directive.
This is the code:
function showBlock($window,$timeout) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.isBlock = false;
checkScreen();
function checkScreen() {
var wid = $window.innerWidth;
if (wid <= 600) {
if(!scope.isBlock) {
$timeout(function() {
scope.isBlock = true;
scope.$apply();
}, 100);
};
} else if (wid > 600) {
if(scope.isBlock) {
$timeout(function() {
scope.isBlock = false;
scope.$apply();
}, 100);
};
};
};
angular.element($window).bind('resize', function(){
checkScreen();
});
}
};
}
html:
<div ng-if="isBlock" show-block>
//..conent to show
</div>
<div ng-if="!isBlock" show-block>
//..other conent to show
</div>
Note: If I don't use $timeout I'll get the error
$digest already in progress
I used console logs inside to check if it's updating the value, and inside the directive everything works fine. But the changes doesn't go to the view. The block doesn't show.
You should use do rule in such cases to get the advantage of Prototypal Inheritance of AngularJS.
Basically you need to create a object, that will will have various property. Like in your case you could have $scope.model = {} and then place isBlock property inside it. So that when you are inside your directive, you will get access to parent scope. The reason behind it is, you are having scope: true, which says that the which has been created in directive is prototypically inherited from parent scope. That means all the reference type objects are available in your child scope.
Markup
<div ng-if="model.isBlock" show-block>
//..conent to show
</div>
<div ng-if="!model.isBlock" show-block>
//..other conent to show
</div>
Controller
app.controller('myCtrl', function($scope){
//your controller code here
//here you can have object defined here so that it can have properties in it
//and child scope will get access to it.
$scope.model = {}; //this is must to use dot rule,
//instead of toggle property here you could do it from directive too
$scope.isBlock = false; //just for demonstration purpose
});
and then inside your directive you should use scope.model.isBlock instead of scope.isBlock
Update
As you are using controllerAs pattern inside your code, you need to use scope.ag.model.isBlock. which will provide you an access to get that scope variable value inside your directive.
Basically you can get the parent controller value(used controllerAs pattern) make available controller value inside the child one. You can find object with your controller alias inside the $scope. Like here you have created ag as controller alias, so you need to do scope.ag.model to get the model value inside directive link function.
NOTE
You don't need to use $apply with $timeout, which may throw an error $apply in progress, so $timeout will run digest for you, you don't need to worry about to run digest.
Demo Here
I suspect it has something to do with the fact that the show-block directive wouldn't be fired if ng-if="isBlock" is never true, so it would never register the resize event.
In my experience linear code never works well with dynamic DOM properties such as window sizing. With code that is looking for screens size you need to put that in some sort of event / DOM observer e.g. in angular I'd use a $watch to observe the the dimensions. So to fix this you need to place you code in a $watch e.g below. I have not tested this code, just directional. You can watch $window.innerWidth or you can watch $element e.g. body depending on your objective. I say this as screens will be all over the place but if you control a DOM element, such as, body you have better control. also I've not use $timeout for brevity sake.
// watch window width
showBlock.$inject = ['$window'];
function bodyOverflow($window) {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($window.innerWidth, function (newWidth, oldWidth) {
if (newWidth !== oldWidth) {
return isBlock = newWidth <= 600;
}
})
}
};
}
// OR watch element width
showBlock.$inject = [];
function bodyOverflow() {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($element, function (new, old) {
if (newWidth) {
return isBlock = newWidth[0].offsetWidth <= 600;
}
})
}
};
}

JQuery UI Spinner is not updating ng-model in angular

Angular's ng-model is not updating when using jquery-ui spinner.
Here is the jsfiddle http://jsfiddle.net/gCzg7/1/
<div ng-app>
<div ng-controller="SpinnerCtrl">
<input type="text" id="spinner" ng-model="spinner"/><br/>
Value: {{spinner}}
</div>
</div>
<script>
$('#spinner').spinner({});
</script>
If you update the text box by typing it works fine (you can see the text change). But if you use the up or down arrows the model does not change.
Late answer, but... there's a very simple and clean "Angular way" to make sure that the spinner's spin events handle the update against ngModel without resorting to $apply (and especially without resorting to $parse or an emulation thereof).
All you need to do is define a very small directive with two traits:
The directive is placed as an attribute on the input element you want to turn into a spinner; and
The directive configures the spinner such that the spin event listener calls the ngModel controller's $setViewValue method with the spin event value.
Here's the directive in all its clear, tiny glory:
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
Note that $setViewValue is intended for exactly this situation:
This method should be called when an input directive wants to change
the view value; typically, this is done from within a DOM event
handler.
Here's a link to a working demo.
If the demo link provided above dies for some reason, here's the full example script:
(function () {
'use strict';
angular.module('ExampleApp', [])
.controller('ExampleController', ExampleController)
.directive('jqSpinner', jqSpinner);
function ExampleController() {
var c = this;
c.exampleValue = 123;
};
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
})();
And the minimal example template:
<div ng-app="ExampleApp" ng-controller="ExampleController as c">
<input jq-spinner ng-model="c.exampleValue" />
<p>{{c.exampleValue}}</p>
</div>
Your fiddle is showing something else.
Besides this: Angular can not know about any changes that occur from outside its scope without being aknowledged.
If you change a variable of the angular-scope from OUTSIDE angular, you need to call the apply()-Method to make Angular recognize those changes. Despite that implementing a spinner can be easily achieved with angular itself, in your case you must:
1. Move the spinner inside the SpinnerCtrl
2. Add the following to the SpinnerCtrl:
$('#spinner').spinner({
change: function( event, ui ) {
$scope.apply();
}
}
If you really need or want the jQuery-Plugin, then its probably best to not even have it in the controller itself, but put it inside a directive, since all DOM-Manipulation is ment to happen within directives in angular. But this is something that the AngularJS-Tutorials will also tell you.
Charminbear is right about needing $scope.$apply(). Their were several problems with this approach however. The 'change' event only fires when the spinner's focus is removed. So you have to click the spinner then click somewhere else. The 'spin' event is fired on each click. In addition, the model needs to be updated before $scope.$apply() is called.
Here is a working jsfiddle http://jsfiddle.net/3PVdE/
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
var mdlAttr = $(this).attr('ng-model').split(".");
if (mdlAttr.length > 1) {
var objAttr = mdlAttr[mdlAttr.length - 1];
var s = $scope[mdlAttr[0]];
for (var i = 0; i < mdlAttr.length - 2; i++) {
s = s[mdlAttr[i]];
}
s[objAttr] = ui.value;
} else {
$scope[mdlAttr[0]] = ui.value;
}
$scope.$apply();
}
}, 0);
});
Here's a similar question and approach https://stackoverflow.com/a/12167566/584761
as #Charminbear said angular is not aware of the change.
However the problem is not angular is not aware of a change to the model rather that it is not aware to the change of the input.
here is a directive that fixes that:
directives.directive('numeric', function() {
return function(scope, element, attrs) {
$(element).spinner({
change: function(event, ui) {
$(element).change();
}
});
};
});
by running $(element).change() you inform angular that the input has changed and then angular updates the model and rebinds.
note change runs on blur of the input this might not be what you want.
I know I'm late to the party, but I do it by updating the model with the ui.value in the spin event. Here's the updated fiddle.
function SpinnerCtrl($scope, $timeout) {
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
$scope.spinner = ui.value;
$scope.$apply();
}
}, 0);
});
}
If this method is "wrong", any suggestions would be appreciated.
Here is a solution that updates the model like coder’s solution, but it uses $parse instead of parsing the ng-model parameter itself.
app.directive('spinner', function($parse) {
return function(scope, element, attrs) {
$(element).spinner({
spin: function(event, ui) {
setTimeout(function() {
scope.$apply(function() {
scope._spinnerVal = = element.val();
$parse(attrs.ngModel + "=_spinnerVal")(scope);
delete scope._spinnerVal;
});
}, 0);
}
});
};
});

Resources