AngularJS use a directive to prevent other directives to execute - angularjs

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

Related

Unable to watch form $dirty (or $pristine) value in Angular 1.x

I have a scenario in an Angular 1.x project where I need to watch a controller form within a directive, to perform a form $dirty check. As soon as the form on a page is dirty, I need to set a flag in an injected service.
Here is the general directive code:
var directiveObject = {
restrict: 'A',
require: '^form',
link: linkerFn,
scope: {
ngConfirm: '&unsavedCallback'
}
};
return directiveObject;
function linkerFn(scope, element, attrs, formCtrl) {
...
scope.$watch('formCtrl.$dirty', function(oldVal, newVal) {
console.log('form property is being watched');
}, true);
...
}
The above only enters the watch during initialization so I've tried other approaches with the same result:
watching scope.$parent[formName].$dirty (in this case I pass formName in attrs and set it to a local var formName = attrs.formName)
watching element.controller()[formName] (same result as the above)
I've looked at other SO posts regarding the issue and tried the listed solutions. It seems like it should work but somehow the form reference (form property references) are out of scope within the directive and therefore not being watched.
Any advice would be appreciated.
Thank you.
I don't know why that watch isn't working, but as an alternative to passing in the entire form, you could simply pass the $dirty flag itself to the directive. That is:
.directive('formWatcher', function() {
restrict: 'A',
scope: {
ngConfirm: '&unsavedCallback', // <-- not sure what you're doing with this
isDirty: '='
},
link: function(scope, element, attrs) {
scope.watch('isDirty', function(newValue, oldValue) {
console.log('was: ', oldValue);
console.log('is: ', newValue);
});
}
})
Using the directive:
<form name="theForm" form-watcher is-dirty="theForm.$dirty">
[...]
</form>

Angularjs: how to update my model only once

I need to update my model after is has loaded its data. So i tried to write a directive which can do that. I didn't thought it would be this hard :(
I first tried a filter, which should be more simple, but got this error.
Error: [ngModel:nonassign] Expression 'editpage.url | addUrl' is non-assignable.
So now i try the directive way. This is my html code in the view:
<input ng-model="editpage.url" add-url type="text" class="light_txtbox" readonly>
And this is my directive:
app.directive('addUrl', [function() {
return {
restrict: 'A',
require: '?ngModel',
replace: true,
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return;
// what to do next?
}
};
}]);
In the "what to do next" part i tried a watch like this:
scope.$watch(attrs.ngModel, function(site) {
if (typeof site !== undefined) {
ngModel.$setViewValue('www.mysite.com/' + site);
ngModel.$render();
}
});
But of course now the model is updated and "hey, i am changed so update again!" and again and again...
I only need the update to take place once. I think i need another approach, but can not figure out what to do.
You can unregister a $watch
var unregister = scope.$watch(attrs.ngModel, function() {
if (shouldStopWatching) {
unregister();
}
});
where shouldStopWatching is whatever condition you need (i.e. stop on second call of callback etc)

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

how angular ui typeahead trigger when focus

I'm using angular-ui typeahead. How can I trigger the popup items when focus on the input box, not after typing.
I can attest to the issues associated with expanding the UX of the typeahead. While #ueq's solution works, it has trouble releasing focus/clicking. I suggest changing things, specifically how you trigger the open UX.
suggested changes
open on double click - this solves the issue of click-releasing in #ueq's answer
check for existing values so as not to overwrite the value - we don't want to accidentally overwrite existing data when we open, so check first then set to a non-valid value to trigger the open.
change the name of the directive.... go with something more descriptive - considering that ui.bootstrap has already changed their namespace moving from 13.x to 14.x it just makes sense to go with your own name. Since directives can represent both UI &/or UX, it makes sense to name your directive to something that other developers can later track down more easily.
why
When working with a typeahead, people have certain expectations of the UX. Clicking into an input and having something popup can be somewhat jarring and misdirecting. A single click or tab-focus into an input traditionally does nothing other than readying the input for keyboard interaction. A double click generally carries the expectation that something more will happen (e.g. double click a file & close from a select dialog vs. single click to select, then click "ok" to close).
In programming we often try to employ the separation of concerns paradigm to our code. But I think this could be applied also to this particular UX and UX in general. Let the single-click & tab-focusing do what they've done for years and utilize the double-click to expand the UX of the typeahead.
plunker - http://plnkr.co/edit/GGl6X4klzVLKLX62Itbh?p=preview
.directive('typeaheadClickOpen', function($parse, $timeout) {
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, elem, attrs) {
triggerFunc = function(evt) {
var ctrl = elem.controller('ngModel'),
prev = ctrl.$modelValue || '';
if (prev) {
ctrl.$setViewValue('');
$timeout(function() {
ctrl.$setViewValue(prev);
});
} else {
ctrl.$setViewValue(' ');
}
}
elem.bind('dblclick', triggerFunc);
}
}
})
Hi I had the same issue and with this github discussion I was able to figure it out: Setup a directive that calls $setViewValue like
.directive('typeahead', function () {
return {
require: 'ngModel',
link: function (scope, element, attr, ctrl) {
element.bind('click', function () {
ctrl.$setViewValue(' ' );
});
element.bind('blur', function () {
ctrl.$setViewValue('');
});
}
};
});
and add it to your input:
<input type="text" [...] typeahead>
Result (I created a plkr: http://plnkr.co/edit/Si6tFK2AammZy1HqEQzA):
Hope that helps :)
Use typeahead-min-length="0" if supported by your angular-ui version. Otherwise this will help you out:
directive('typeaheadOpenOnFocus', function ($timeout) {
return {
require: 'ngModel',
link: function (scope, element, attr, ctrl) {
element.bind('click', function () {
var vv = ctrl.$viewValue;
ctrl.$setViewValue(vv ? vv+' ': ' ' );
$timeout(function(){ctrl.$setViewValue(vv ? vv : '');},10)
});
}
};
})
and add typeahead-open-on-focus as attribute to your input element.
This will open the typeahead onfocus if it already has a value too.
And it automatically reverts the viewvalue.
Inspired by the answer of Boem
You can try this for avoiding the issue of view rendering
app.directive('typeahead', function () {
return {
restrict: "A",
require: 'ngModel',
link: function (scope, element, attr, ctrl) {
element.bind('click', function () {
ctrl.$setViewValue('');
ctrl.$render();
});
}
};});

How to move code from App.js to Directive

I have a small amount of js in the app.js file that I needed in order to manipulate the DOM in this Angular Grid:
http://plnkr.co/PXRgUA
You can see it in app.js.
$('.userRow ').live('click', function(e) {
$(this).find('span.userDetailRow.blueRow').show().animate({height:200},500);
});
$('.closeDetails').live('click', function(e) {
$(this).parent('span').animate({height:0}, 500).animate({height:0},500).hide();
e.stopPropagation();
});
How can I move this to a directive?
Does it have to be moved to a directive?
It does not seem right here.
Yes, you can (and should) move it to a directive. For the sake of clarity I'll include your old code here:
$('.userRow ').live('click', function(e) {
$(this).find('span.userDetailRow.blueRow').show().animate({height:200},500);
});
$('.closeDetails').live('click', function(e) {
$(this).parent('span').animate({height:0}, 500).animate({height:0},500).hide();
e.stopPropagation();
});
This (binding event listeners with jquery) is what people are chomping at the bit to describe as 'not the angular way.' Instead, you can use ng-click (which is just an inbuilt directive) to call javascript functions:
<tr row ng-click="expandRow()" ng-repeat="user in users" class="item-in-list el userRow" animate="fadeIn">
<span class="userDetailRow blueRow" style="display:none;"><span close ng-click="closeDetails(); $event.stopPropagation()">x</span>
You can see here there are two custom attributes defined on these elements. These link to the directives below. These directives have custom functions defined in their link function which you can then call with ng-click (though note that this is putting these functions on the global scope).
.directive('close', function() {
return {
restrict: 'A',
replace: false,
link: function($scope, element, attrs) {
$scope.closeDetails = function() {
$(element).parent('span').animate({height:0}, 500).animate({height:0},500).hide();
}
}
}
})
.directive('row', function() {
return {
restrict: 'A',
replace: false,
link: function($scope, element, attrs) {
$scope.expandRow = function() {
$(element).find('span.userDetailRow.blueRow').show().animate({height:200},500);
}
}
}
});
jQuery is still being used to here to locate and modify the elements for the sake of simplicity, so you can see where your old logic has gone. However you should ideally change this to use angular's inbuilt animation functionality. (more info on how animation works in the new angular version: http://www.yearofmoo.com/2013/08/remastered-animation-in-angularjs-1-2.html)
Plunker here:
http://plnkr.co/edit/UMvYnx?p=preview

Resources