How to stop ng-click from a custom directive in AngularJS? - angularjs

LIVE DEMO
Consider the following myButton directive:
angular.module("Demo", []).directive("myButton", function() {
return {
restrict : "E",
replace: true,
scope: {
disabled: "="
},
transclude: true,
template: "<div class='my-button' ng-class='{ \"my-button-disabled\": disabled }' ng-transclude></div>",
};
});
which can be used like this:
<my-button disabled="buttonIsDisabled"
ng-click="showSomething = !showSomething">
Toggle Something
</my-button>
How could I stop ng-click from executing when buttonIsDisabled is true?
PLAYGROUND HERE

You could use capture (addEventListener's optional third parameter) and stop the propagation of the event (using stopPropagation).
"Capture" allows you to catch the event before it reaches the "bubble" phase (when the triggering of "normal" event-listeners happens) and "stopPropagation" will...stop the propagation of the event (so it never reaches the bubbling phase).
element[0].addEventListener('click', function (evt) {
if (scope.disabled) {
console.log('Stopped ng-click here');
evt.preventDefault();
evt.stopPropagation();
}
}, true);
See, also, this short demo.

Why not use the actual button for your button. You could change your directive to:
angular.module("Demo", []).directive("myButton", function() {
return {
restrict : "E",
replace: true,
scope: {
disabled: "="
},
transclude: true,
template: "<button class='my-button' ng-class='{ \"my-button-disabled\": disabled }' ng-disabled='disabled' type='button' ng-transclude></button>"
};
});
Then style it to look like your div. See the Short Example I've made.

Try this in your link function:
link: function(scope, element, attrs) {
var clickHandlers = $._data(element[0]).events.click;
clickHandlers.reverse(); //reverse the click event handlers list
element.on('click', function(event) {
if (scope.disabled) {
event.stopImmediatePropagation(); //use stopImmediatePropagation() instead of stopPropagation()
}
});
clickHandlers.reverse(); //reverse the list again to make our function at the head of the list
}
DEMO
This solution uses jQuery to deal with cross browser problems. The idea here is to attach our event handler at the head of the click handlers list and use stopImmediatePropagation() to stop current handlers of the same event and bubbling event.
Also take a look at this: jquery: stopPropagation vs stopImmediatePropagation

<my-button disabled="buttonIsDisabled"
ng-click="showSomething = buttonIsDisabled ? showSomething : !showSomething">
or
<my-button disabled="buttonIsDisabled"
ng-click="showSomething = buttonIsDisabled ? function(){} : !showSomething">
Is this too simple?

Related

AngularJS: Should I observe optional attributes?

Assume I have a directive my-button with an optional attribute disabled. People could use this directive like:
<my-button>Button text</my-button>
or
<my-button disabled="variable">Button Text</my-button>
Should I be watching for a disabled attribute? Could these usages somehow transition from one to the other?
In response to JB Nizet's request for the code in question, here's a clean version of the directive function:
function ButtonDirective() {
var directive = {
link: link,
restrict: 'E',
scope: {
click: '&',
disabled: '=?',
},
template: '<a class="my-button" ' +
'data-ng-class="{\'my-button-disabled\': disabled}" ' +
'data-ng-click="disabled || click()" ng-transclude />',
transclude: true
};
function link(scope) {
if (typeof scope.disabled == 'undefined') scope.disabled = false;
}
return directive;
}
The directive creates an anchor tag styled as a button. It accepts two properties/parameters: click and disabled. The latter is optional. When disabled, the click event should fire when clicked, otherwise the the click event should fire when clicked.
To reiterate: Should I worry about someone somehow adding, removing, or modifying the disabled attribute after the fact? If so, how should I go about it?
After hashing things out with JB Nizet, he counseled me to not worry about the HTML attribute changing.

How to delegate ngFocus/ngBlur to directive's template <input> element?

I'm trying to create a custom component (directive) which is composed of an <input> box and a [-] and [+] buttons. Currently, the example below only implements the input box.
So, say I have the following HTML for my directive:
<my-input ng-blur="onBlur($event)" ng-focus="onFocus($event)"></my-input>
And for testing purposes, I use this code:
app.run(function ($rootScope) {
$rootScope.onBlur = function ($event) {
console.log('onBlur', $event);
};
$rootScope.onFocus = function ($event) {
console.log('onFocus', $event);
};
});
Now I want to create my custom <my-input> directive which has an <input> box on the template and I need the ng-blur and ng-focus set on <my-input> to respond to blur/focus events on the input box.
I have the following solution almost working: http://codepen.io/anon/pen/KpELmj
1) I have a feeling that this can be achieved in a much better way, I just can't seem to do it. Thoughts?
2) $event seems to be undefined and I can't understand why. Thoughts?
Ok figured it out. Doron's answer was a good starting point for research, but now I think I have what you are looking for. The key is you have to use & in the link section in order to get it to execute the expression.
.directive('myInput', function($timeout) {
return {
restrict: 'E',
scope: {
data: '=',
blur: '&myBlur' //this is the key line
},
template: '<input ng-blur="blur($event)" ng-model="data">'
}
})
This is how you use it:
<my-input my-blur="runBlurFunc()"></my-input>
If you really want to define the function on the root scope, you can use $scope.$root.onBlur() instead of runBlurFunc()
Hope I got your question right, did you try to use the link function?
app.directive('myInput', function () {
return {
restrict: 'E',
scope: {
ngBlur: '&',
ngFocus: '&'
},
bindToController: true,
controller: controllerFn,
controllerAs: 'ctrl',
link:function(scope){
scope.onBlur = function(ev){
console.log(ev);
}
scope.onFocus = function(ev){
console.log(ev);
}
},
template: '[-]<input ng-blur="onBlur($event)" ng-focus="onFocus($event)"></input>[+]'
}
});

Angular jquery plugin wrapper against data from ajax call

I am trying to write a basic jquery plugin wrapper directive but the problem I keep facing is that angular has not rendered the bound data when the plugin is called within the link function.
from html:
<my-syntax ng-bind="snippet.code"></my-syntax>
the directive:
angular.module('myDemo').directive('mySyntax', function($timeout) {
return {
restrict: 'E',
replace: true,
template: '<pre><code></code></pre>',
link: function(scope, element) {
// this timeout seems brittle, need better solution
$timeout(function() {
element.each(function(i, e) { hljs.highlightBlock(e) });
}, 50);
}
});
the highlightjs plugin relies on the content of the element, but since that is coming from
my "snippet.code" scope binding, and that value is coming from an ajax call, the jquery plugin is executing against something that hasn't rendered yet. I have "solved" this by wrapping the jqueryPlugin call in a $timeout with 50ms but that seems very brittle. I have also tried using isolated scope and wrapping the jqueryPlugin call in a watch on the scope variable but in this case nothing renders at all (and no js errors are occurring). I would think this is a very common type of directive but I have yet to find a solution to this problem.
Attempt:
<my-syntax code="snippet.code"></my-syntax>
Directive:
angular.module('myDemo').directive('mySyntax', function($timeout) {
return {
restrict: 'E',
replace: true,
template: '<pre><code>{{code}}</code></pre>',
scope: {
code: '='
},
link: function(scope, element) {
scope.$watch('code', function() {
element.each(function(i, e) { hljs.highlightBlock(e) });
}, 50);
}
});
Your second attempt is almost correct.
The problem is that the first time the listener function for the watcher is executed both the new and old value will be undefined. This means the highlightBlock function will run twice, which it cannot handle:
You can use highlightBlock to highlight blocks dynamically inserted
into the page. Just make sure you don't do it twice for already
highlighted blocks.
Example:
return {
restrict: 'E',
replace: true,
template: '<pre><code>{{code}}</code></pre>',
scope: {
code: '='
},
link: function(scope, element) {
var watchExpression = function() {
return scope.code;
};
var listener = function(newValue, oldValue) {
if (newValue === oldValue) return;
element.each(function(i, e) {
hljs.highlightBlock(e)
});
unregister();
};
var unregister = scope.$watch(watchExpression, listener);
}
}
Since it cannot be run twice, I'm letting the listener function unregister the watcher when it's done.
Also, the third parameter in $watch is a boolean that sets if to deep watch or not. In your example you are passing 50, but I suspect it's a copy and paste error from the $timeout attempt.
Demo: http://plnkr.co/edit/6j0BMxjNX88GchXCIlJq?p=preview

AngularJS use a directive to prevent other directives to execute

Some actions in my Angular app require the user to be registered. If the user is not registered we want to show a "Register modal" and prevent the original action.
Those actions can be triggered via ng-click or any other "click binding" directive (for example the 'modal-toggle' one).
So I found this solution: https://stackoverflow.com/a/16211108/2719044
This is pretty cool but only works with ng-click.
I first wanted to make the "terminal" property of the directive dynamic but couldn't manage to do it.
So the idea was to set "terminal" to true and manually prevent default click action in the directive.
Here is my DOM
<!-- This can work with terminal:true and scope.$eval(attrs.ngClick) (see example above) -->
<div user-needed ng-click="myAction()">Do it !</div>
<!-- This doesn't work. I can't manage to prevent the modal-toggle to be executed -->
<div user-needed modal-toggle="my-modal-id-yey">Show yourself modal !</div>
And my directive(s) (which don't work...)
// First try (with terminal:true)
app.directive('userNeeded', function() {
return {
priority: -100,
terminal: true,
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('click', function(e) {
if(isRegistered()) {
// Here we do the action like scope.$eval or something
}
});
}
};
});
// Second try (with stopPropagation)
app.directive('userNeeded', function() {
return {
priority: -100
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('click', function(e) {
if(!isRegistered()) {
e.stopPropagation();
}
});
}
};
});
...And that's why I'm here. Any idea ?
Thanks a lot.
You were extremely close. Instead of stopPropagation you needed stopImmediatePropagation. The difference between the two is summarized in this StackOverflow answer by #Dave:
stopPropagation will prevent any parent handlers from being
executed while stopImmediatePropagation will do the same but
also prevent other handlers from executing.
So to fix the code, all we have to do is swap out that method and VoilĂ :
app.directive('userNeeded', function() {
return {
priority: -100
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('click', function(e) {
if(!isRegistered()) {
e.stopImmediatePropagation();
}
});
}
};
});
Here is an example Plunker of the working code. In the example I modified the directive slightly to allow specific events to be specified (such as user-needed="submit") by passing the value directly to the element.bind function; however, it defaults to 'click'.

How to overwrite ng-click functionality

I'm trying to take a form button that says Save to change to Saving... when it's busy. It'd be awesome if this could detect that there's an ng-click directive here and only trigger that directive if busy is false. Would I need to create a new directive for this, or is there a way to just tap into ng-click's functionality?
HTML:
<button-large color="green" ng-click="createWorkstation()" busy="disableSave()" busyLabel="Saving...">Save</button-large>
JS:
directive('buttonLarge', function () {
return {
scope: {
busy: '&'
},
replace: true,
restrict: 'E',
transclude: true,
template: '<button type="checkbox" class="buttonL" ng-transclude/>',
link: function (scope, element, attrs) {
var config = {
color: "Default"
};
angular.extend(config, attrs);
element.addClass("b"+capitalize(config.color));
//when the button is busy, disable the button
scope.$watch(attrs.busy, function () {
console.log('changed', scope.busy);
});
//capitalize first letter of string
function capitalize(s) {
return s[0].toUpperCase() + s.slice(1);
}
}
}
})
I'd just do it like this:
<button ng-click="createWorkstation()">{{isBusy && "Saving" || "Save"}} </button>
Where isBusy can just be a boolean that you are changing in your scope (or a function I guess) while you are processing/etc. This doesn't require a directive and keeps the wording in the markup. You could probably extend this to having a service or constant for the strings/etc but that just depends on how far you want to take it.
** update **
If you want to bind html per one of the comments you'd use the ng-bind-html-unsafe directive:
<button ng-click="go()" ng-bind-html-unsafe="isBusy && '<span class=icol-refresh></span><span>Saving...</span>' || 'Save'">Process</button>

Resources