How to write directive that hooks into ng-click handler - javascript

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>

Related

ng-click does not fire with a function

I am new with Angular and now I have encountered some problems...
So let's say I have a controller called ViewModelController and I use controlleras when I define the routes as following: .
And in my template I have just difened two div which seperate the container in two parts:
<div id='viewleft' class="divleft col-md-5"></div>
<div id='viewright' class="col-md-7 divright"></div>
And in ViewModelController, I have some code to render the template when the controller is loaded. The question is that the ng-click I put in all the elements just don't fire and I don't really know where is the problem.
I have tried thing like below but it just does not work.
var content1 = '<ul><li><button id ="b1" ng-click="vmCtrl.cprint($event.target)">123</button></li><ul>';
$("#viewleft").html(content1);
Can someone helps me on that? Thank you in advance, best wishes.
You have to compile this html so that angular code will work
$compile($("#viewleft").contents())(scope);
or better to use a directive that compile html when its value changes.
app.directive('compile', ['$compile', function ($compile) {
return function (scope, element, attrs) {
scope.$watch(
function (scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function (value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
}]);
in controller you can asssign html to scope variable
$scope.template= '<ul><li><button id ="b1" ng-click="vmCtrl.cprint($event.target)">123</button></li><ul>';
and on view you can add that
<div id='viewleft' class="divleft col-md-5" compile="template"></div>
You can refer this docs for $compile.

Modifying scope not working without $scope.$apply

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

Which parameters to pass into Angular JS custom directive controller?

I'm trying to create a custom directive which needs to use a separate controller because it needs to have functions which can be called by child directives.
Here is my code so far:
angular.module('myDirectives').controller('SlideInMenuController', function ($scope, $element, $attrs) {
$scope.isOpen = false;
// Toggle Function
this.toggle = function(){
$scope.$apply(function(){
$scope.isOpen = !$scope.isOpen;
});
};
// Watch $scope.isOpen and open the menu
$scope.$watch('isOpen', function() {
if($scope.isOpen == true){
$element.attr('is-open', true);
}
else{
$element.attr('is-open', false);
}
return false;
});
}
angular.module('myDirectives').directive('slideInMenu', function ($swipe) {
return {
restrict: 'EA',
scope: {},
controller:'SlideInMenuController'
};
});
angular.module('myDirectives').directive('slideInMenuToggle', function ($swipe) {
return {
restrict: 'EA',
require: '^slideInMenu',
link: function ($scope, $element, $attrs, SlideInMenuCtrl) {
$element.bind('click', function(){
SlideInMenuCtrl.toggle();
});
}
};
});
(Note: I'm using ng-annotate so I don't have to write all my dependencies twice)
I need to inject the $swipe service into the directive controller but a normal controller would't have $scope, $element, $attrs as the first three parameters. This has made me wonder if I should be putting those into the link function instead and doing DOM stuff there, but if I do that what goes in the controller and what goes in to the link function.
I've read numerous blogs and SO answers that say what order compile/link/controller are run in but still can't find a clear answer as to whatin my above example should go where.
Any help would really be appreciated.
There are two kind of functions for AngularJS. Neither of which is intended to be called directly.
1) Injectables: functions that receive parameters, whose names must (with a few exceptions) be registered with dependency injection subsystem. It's the reason for ng-annotate to exist. You can also use array notation for these.
angular.module('stackOverflow').service('answer', ['myService', function(myService) {
...
}]);
Some examples are the ones you pass to angular.module() functions, like service(), factory(), directive(), controller().
2) Plain functions. These have no special handling, it's vanilla JavaScript. They are passed to link and compile slots in directive definition objects.
You can omit rightmost parameters if you have no use for them, but not others. As the order of parameters is fixed, you cannot reorder them. But you can call them whatever you want.
That's it about functions.
About conventions using $: beware! AngularJS builtin services are prefixed with $, so you should name parameters this way for injectable functions. For all other cases, don't prefix with $: your own functions and positional parameters like you see in link() and compile(). Prefix with $ in those functions is misleading and bad guidance.
To better distinguish parameters for compile() and link, you can prefix with t for template and i for instance. Nowadays I prefer to use those unprefixed. It's better for moving them around.
compile: function (tElement, tAttrs) {
return function link(scope, iElement, iAttrs, ctrls) {
};
}

Directive at angularjs and custom method/html

I have this code:
<body ng-controller="testController">
<div test-directive transform="transform()">
</div>
<script type="text/ng-template" id="testDirective.html">
<div>
<p>
{{transform()}}
</p>
</div>
</script>
<script>
angular.module("Test", [])
.directive("testDirective", function() {
return {
templateUrl: "testDirective.html",
scope: {
transform: "&"
},
link: function(scope) {
}
};
})
.controller("testController", function($scope) {
$scope.transform = function() {
return "<a ng-click='somethingInController()'>Do Something</a>";
};
$scope.somethingInController = function() {
alert("Good!");
};
});
</script>
</body>
So basically what I want to accomplish is to create a directive with a method that will be called from the controller. And that method will do something with the values passed (in this example it does not receives nothing, but in the real code it does).
Up to that point is working. However, the next thing I want to do is create an element that will call a method in the controller. The directive does not knows what kind of element will be (can be anything) nor what method will be. Is there any way to do it?
Fiddle Example:
http://jsfiddle.net/abrahamsustaita/C57Ft/0/ - Version 0
http://jsfiddle.net/abrahamsustaita/C57Ft/1/ - Version 1
FIDDLE EXAMPLE WORKING
http://jsfiddle.net/abrahamsustaita/C57Ft/2/ - Version 2
The version 2 is now working (I'm not sure if this is the way to go, but it works...). However, I cannot execute the method in the parent controller.
Yes. However there is a few problems with your code. I will start by answering your question.
<test-directive transform='mycustommethod'></test-directive>
// transform in the directive scope will point to mycustommethod
angular.module('app').directive('testDirective', function() {
return {
restrict: 'E',
scope: {
transform: '&'
}
}
});
The problem is that printing the html will be escaped and you will get < instead of < (etc.). You can use ng-bind-html instead but the returned html will not be bound. You will need to inject the html manually (you can use jquery for this) in your link method and use var compiled = $compile(html)(scope) to bind the result. Then call ele.after(compiled) or ele.replace(compiled) to add it to your page.
I finally get to get it working.
The solution is combined. First of all, I needed to add another directive to parse the element I wanted:
.directive("tableAppendElement", function ($compile) {
return {
restrict: "E",
replace: true,
link: function(scope, element, attrs) {
var el = angular.element("<span />");
el.append(attrs.element);
$compile(el)(scope);
element.append(el);
}
}
})
This will receive the element/text that will be appended and then will registered it to the scope.
However, the problem still exists. How to access the scope of the controller? Since my directive will be used by a lot of controllers, and will depend on the model of the controller, then I just set scope: false. And with that, every method in the controller is now accessible from the directive :D
See the fiddle working here. This also helped me because now, there is no need to pass the transform method, so the controller can be the one handling that as well.

Call AngularJS function on document ready

Is there a way to call an Angular function from a JavaScript function?
function AngularCtrl($scope) {
$scope.setUserName = function(student){
$scope.user_name = 'John';
}
}
I need the following functionality in my HTML:
jQuery(document).ready(function(){
AngularCtrl.setUserName();
}
The problem here is my HTML code is present when page is loaded and hence the ng directives in the html are not compiled. So I would like to $compile(jQuery("PopupID")); when the DOM is loaded.
Is there a way to call a Angular function on document ready?
Angular has its own function to test on document ready. You could do a manual bootstrap and then set the username:
angular.element(document).ready(function () {
var $injector = angular.bootstrap(document, ['myApp']);
var $controller = $injector.get('$controller');
var AngularCtrl = $controller('AngularCtrl');
AngularCtrl.setUserName();
});
For this to work you need to remove the ng-app directive from the html.
The answer above although correct, is an anti-pattern. In most cases when you want to modify the DOM or wait for the DOM to load and then do stuff (document ready) you don't do it in the controller but in he link function.
angular.module('myModule').directive('someDirective', function() {
return {
restrict: 'E',
scope: {
something: '='
},
templateUrl: 'stuff.html',
controller: function($scope, MyService, OtherStuff) {
// stuff to be done before the DOM loads as in data computation, model initialisation...
},
link: function (scope, element, attributes)
// stuff that needs to be done when the DOM loads
// the parameter element of the link function is the directive's jqlite wraped element
// you can do stuff like element.addClass('myClass');
// WARNING: link function arguments are not dependency injections, they are just arguments and thus need to be given in a specific order: first scope, then element etc.
}
};
});
In all honesty, valid use of $document or angular.element is extremely rare (unable to use a directive instead of just a controller) and in most cases you're better of reviewing your design.
PS: I know this question is old but still had to point out some best practices. :)

Categories

Resources