Angular.js closing with click anywhere but on the element - javascript

It's pretty common thing, like if you click on inbox here here on stackoverflow. I'll be calling that dialog or whatever gets opened a thing.
Now there are two ways I know of to deal with this,
you create an invisible overlay where only the element you opened
has bigger zindex, and you close your thing by clicking on the
overlay
click event on the document, and you check upon clicking whether you clicked on your thing or outside it, in which case you close your thing.
In either case I'd ideally like to use ng-class to add/remove class that would show/hide the thing.
Now how would I do this with angular, so it could be used on any component(thing) I might add. It's not like I have single modal, I might have quite a few different components, with different html structure, positioning and stuff.
Which approach would be better, document event, overlay or something completely else?
Since angular doesn't really have any reference to dom, document approach could be a problem, right, since I can't quite check whether I'm clicking on the thing element? Unless I'd give every thing the same class..
Overlay approach on the other hand doesn't require any reference to dom elements.
Both approaches would need a unique var at rootScope for that ng-class to work.. which bring the question whether to use ng-class or something custom..
Anyway just throwing my ideas out there, maybe I'm thinking about it wrong from the beginning, has anyone dealt with this before?

The way I've tackled things like this before is using inheritedData to communicate to the click handler whether it's in or out of the thing:
In the custom directive for the thing, add a data variable to the element, using jqLite data, say element.data('myThing',true) . If you want to distinguish between multiple instances of the the thing, you might need to use some uniquely generated key.
In the same custom directive, in a click event handler on document.body, you can check angular.element(event.target).inheritedData('myThing')
An example directive that uses this technique is below
app.directive('thing', function($document,$window) {
return {
restrict: 'E',
template: '<div><span>Inner thing</span></div>',
replace: true,
link: function(scope,element) {
element.data('thing',true);
angular.element($document[0].body).on('click',function(e) {
var inThing = angular.element(e.target).inheritedData('thing');
if (inThing) {
$window.alert('in');
} else {
$window.alert('out');
}
})
}
}
});
and can be seen in this Plunker http://plnkr.co/edit/bRDLcLoesM7Z0BIxKxYu?p=preview

Related

Is adding / removing from classlist "ok" in Angular 2?

I have a directive that enables drag and drop for elements. While I am dragging I am giving the dragged element and the elements I am dragging over some classes. Right now I am doing something like this
onDragStart(event: DragEvent) {
event.dataTransfer.setData("text/plain", this.article.id.toString());
this.el.classList.add(this.draggedItemClass);
}
this.el is the native element that I am dragging. I.e. I am writing a css class from my Component using JavaScript. I found similar code in some ng library but it feels like I am doing something wrong and should not write css-classes with JavaScript and rather should do this via my template. So my question is is adding / removing from classlist "ok" in Angular 2 or should I go via the template in some way (probably using my data as a basis)?
From a directive, you can always use #HostAttribute binding. And that is the best practice for sure.
Example:
#HostBinding('class.someClass') someField: boolean = false;
so in that onDragStart, you will just do someField = true
This is cleanest and best soulution.
P.S. Your solution is not that bad because you let Angular to give you access to the element which is good, and you are not selecting it manually from the document, but anyway, the above mentioned solution is way better

Get Elements with AngularJS without referencing the document

I have been developing an advanced web-based GUI in AngularJS. Recently, I decided to use the call document.getElementsByClassName() (I hate using element collecting methods, but here I had to use one) and my boss flipped his lid for accessing the document element. He says that I "need to use only Angular calls for everything", even for element collection! Is there an "Angular way" to collect elements by class name? If so, which way is better to use within the Angular framework? Please provide reasons why. Thanks!
UPDATE: Why I need to use an element collector...
So, I really wish I didn't have to do this, but I do...
I am using a third-party directive that I found online called the Bootstrap DateTimePicker. Its pretty cool and very nice looking, yet it might have a bug...
First, I make a directive bound to an attribute, stating that the element I pass in is meant to be a "DateTimePicker". I then pass that element to the DateTimePicker function.
When invoked, this function creates a new div with absolute positioning and appends it to the body of the page.
Now, I open a dialog in my GUI that has a table in it. On each row of the table, I have two DateTimePickers: one for end-date and one for start-date.
My problem is that, once I leave my screen and the elements which the DateTimePickers were bound to are destroyed, the DateTimePickers still remain! If I open the dialog box again, it creates a ton more of these divs as well!
Until I could determine a true solution to this issue, I decided to use the element collector as a temporary quick-fix. I grab all of the elements with the datetimepicker class and perform a:
elem[i].parentNode.removeChild(elem[i]);
Not having your exact use case but knowing that you are attempting to aggregate elements by class name in your controller makes me agree with you boss. Think of the controller as an object which exposes data and and services to your declarative html page. The data is bound into the markup for presentation and possible modification. THe services are usually wrapped in functions on your controller which are then tied to event handling directives like ng-click or ng-change. These services should operate exclusively on your data and never touch the DOM. If you need to modify a DOM element in your declarative markup then that should be done through directives like ng-class etc.
In any case, It would be useful to know what you are trying to accomplish so as to give you a better idea of the "angular way" to approach the problem.
Well, I have my answer. This does not solve the question "Grab all elements with a certain class name without touching the document element" yet it does solve my problem and eliminates my need to use document.getElementsByClassName.
First of all, it turns out that every element using the DateTimePicker directive have an element.datetimepicker("remove") function.
I use a directive for each DateTimePicker:
components.directive('DateTimePicker', function() {
// Requires bootstrap-datetimestamp.js
return {
restrict: 'E',
replace: true,
scope: {
dateTimeField: '='
},
template:
'<div>' +
'<input type="text" readonly data-date-format="yyyy-mm-ddThh:ii:ssZ" data-date-time required/>'+
'</div>',
link: function(scope, element, attrs, ngModel)
{
var input = element.find('input');
input.
datetimepicker(
{
//stuff
})
.on('changeDate', function(ev)
{
//more stuff
});
...
Directive drastically shortened for the sake of your eyeballs...
I then need to remove the DateTimePicker and the input it is bound to from the DOM on destruction of the dialog box that the input is a child of. To do so, I added this to my directive:
scope.$on("$destroy",function handleDestroyEvent()
{
input.datetimepicker("remove");
input = null;
});
And it works! The DateTimePicker gets removed, the DateTimePicker's handles to the input are cleaned up, and I've marked my input for the GC! WooHoo! Thanks everybody!
If you include jQuery in your project before AngularJS, Angular will use jQuery instead of jqLite for the angular.element function. This means you should be able to use jQuery's selectors for finding / referencing DOM elements.

How to make sure that onclick event in jQuery applies to the newly updated elements of the class?

I have a set of links with a class app-context-link on my page – just some <a> menu elements.
They are generated when the page is loaded from some data that the page receives from a JSON.
I then apply the following on click jQuery function, to make sure that when a user clicks on one of those elements, a certain procedure is performed:
$(".app-context-link").on('click', function(e) {
e.preventDefault();
// do something
}
The problem is that when the user clicks on one of the items, I want to highlight the item they clicked, perform some action on the JSON, and then return a new menu to the user that has other elements of the same class app-context-link – in this case the "old" onclick function does not apply to the newly added items, right? And then I can't get them to behave in the same way because of that.
Does anyone have an idea how I could resolve it?
I know I probably have to rewrite everything very carefully with callbacks, etc. but maybe there's an easier solution already inside jQuery and I'm just missing something?
And as a bonus track – if I decided to leave the menu as it is and just to highlight the element clicked (through appending a class), how would I do that? Sorry if that last question sounds stupid, but I'm a novice and feel a bit confused...
Thank you!
You need to delegate event, e.g:
$(document).on('click', ".app-context-link", function(e) {
e.preventDefault();
// do something
});
Now selector will be filtered on each click.
document is an example, usually, you'd prefer to bind it to closest static container
function bindLink() {
$(".app-context-link").click(function() {
// do something
});
}
Now, everytime you change your data, you can call your bind function (bindLink()).

Getting an Element in AngularJS

It seems that getting an element in AngularJS is a bad idea, i.e. doing something like:
$('.myElement')
in say, a controller is not an angular way of doing things.
Now my question is, how should I get something in angular?
Right now, what I'm doing (and is an accepted way of doing it) is by watching a variable, and my directive does something based on it.
scope.$watch('varToWatch', function (varToWatch) {
if(attrs.id == varToWatch)
{
//Run my Directive specific code
}
});
However, while this particular design works for most cases, watch is an expensive operation, and having lots of directives watching can really slow down your application.
TL:DR - What is an angular way of getting a directive based on a variable on the directive? (like the one above)?
If you want to get/set values you don't need to fetch the element using jQuery. Angular data binding is the way to do it.
directives is the way to go if you want to do animations or any kind of element attributes and DOM manipulation.
Your code is basically right; the directive should watch something in the $scope and perform it's logic when that thing changes. Yes, watch statements are expensive, and that is a problem once your number of watches start to approach ~2000.
Looking at your code though, I see one problem:
The variable $scope.varToWatch references an id in the template.
When this variable changes, you want something to happen to the element which has this id.
The problem here is in the first point: The controller should know nothing about the DOM, including the id of any element. You should find another way to handle this, for example:
<div my-directive="one"> ... </div>
<div my-directive="two"> ... </div>
<div my-directive="three"> ... </div>
...etc
And in your directive:
scope.$watch('varToWatch', function (varToWatch) {
if(attrs.myDirective == varToWatch)
{
// Run my Directive specific code
}
});
You are very vague as to what you're trying to achieve, but I'll try to answer in context of your last comment.
I have a lot of the same directives (therefore the code will run on all of them), but I need to get only one directive from the lot.
You talk a lot about getting the right element. The directive element is passed to the link function in the directive. If you are not using this element (or children of it) directly, but rather trying to search for the element you want somehow, you are most likely approaching the problem the wrong way.
There are several ways to solve this, I'm sure. If you're thinking about animations, there is already support for that in Angular, so please don't try reinvent the wheel yourself. For other logic, here are two suggestions:
Secondary directive
If the logic you want to apply to this directive is generic, i.e. it could be applied to other directives in your application, you could create a new directive which works together with directives. You can set prioritization in directive in order to control which directive is executed first.
<main-directive ... helper-directive="{{condition_for_applying_logic}}"></main-directive>
jsFiddle example
Expanding main directive
If the logic is tightly coupled to this directive, you can just create a new attribute, either dynamic or static, and bind to it in the directive. Instead of checking 'attrs.id == varToWatch', you check if $scope.apply-logic === 'true' and apply the logic then.
<main-directive ...></main-directive> <!-- Not applied here -->
<main-directive apply-logic="true" ...></main-directive> <!-- Applied here -->
<main-directive apply-logic="{{some.varOnScope}}"...></main-directive> <!-- Conditional -->
Please comment if something is unclear.

Genuinely stop a element from binding - unbind an element - AngularJS

I'm trying to find out how I can stop a DOM element from binding data from the scope in angular.
I know that you could do this with if statements and all, but is there a genuine & permanent way to stop binding a element in angular but keep the content that was added?
So say i have this
<div ng-bind=​"content" class=​"ng-binding">​Welcome​</div>​
And i change the model so that the div changes to this.
<div ng-bind=​"content" class=​"ng-binding">​Welcome​ World</div>​
Then I click the button that will unbind it, so if I change the model to 'Welcome Universe', I wan't the <div> to be the same as before. This
<div ng-bind=​"content" class=​"ng-binding">​Welcome​ World</div>​
I know there are many other ways to do this, but i don't know any way to genuinely unbind the element, without cloning it and replacing the old one looping through the attributes and text..ect
Demo thing: http://jsfiddle.net/a9tZY/
So, by doing this, it shouldn't affect the model or other elements that are binding to that model.
Long story short, Tell Angular to leave the element alone forever.
UPDATE
The way to do this is to create a new scope on the element with a directive like so.
yourModule.directive('unbindable', function(){
return { scope: true };
});
And apply it to your element like so
<div unbindable id="yourId"></div>
Then to unbind this element from any updates you do this.
angular.element( document.getElementById('yourId') ).scope().$destroy();
Done, here's a demo.
Demo: http://jsfiddle.net/KQD6H/
So this creates a new scope on the element and only works because all scopes inherit all data from their parent scopes. so the scope is basically the same as the parent scope, but allows you to destroy the scope without affecting the parent scope. Because this element was given it's own scope, when you destroy it it doesn't get the parent scope back like all of the other elements, if that makes sense 0.o
Everything below this line was my original answer,I'll leave it here incase someone prefers this way
I have managed to achieve this genuinely with a unbindable directive.
When you have the unbinable directive set up on the element all that is required to unbind the element is this.
yourElement.attr('unbind', 'true'); // Ref 1
$scope.$broadcast('unbind'); // Ref 2
Here is the directive.
app.directive('unbindable', function(){
return {
scope: true, // This is what lets us do the magic.
controller: function( $scope, $element){
$scope.$on('unbind', function(){ // Ref 3
if($element.attr('unbind') === 'true'){ // Ref 4
window.setTimeout(function(){ $scope.$destroy() }, 0);//Ref 5
}
});
}
}
});
and you set your element up like this.
<h1 unbindable></h1>
So whenever you add the unbind="true" attribute to the h1 and broadcast unbind the element will be unbind-ed
REF-1: Add the unbind true attribute to the element so that the directive knows what element you are unbinding.
REF-2: Broadcast the unbind event across the scopes so that the directive knows that you want to unbind a element - Make sure you add the attribute first. --- Depending on your app layout, you might need to use $rootScope.$broadcast
REF-3: When the unbind event is broadcasted
REF-4: If the element associated with the directive has a true unbind attribute
REF-5: Then destroy the scope made by the directive. We have to use setTimeout because I think angular tries to do something after the $on event and we get a error, so using setTimeout will prevent that error. Although it fires instantly.
This works on multiple elements, here is a nice demo.
Demo: http://jsfiddle.net/wzAXu/2/
This one got me curious, so I did some poking around. At first I tried the "unbind()" method suggested in the other answer, but that only worked with removing event handlers from the element when what you're actually trying to do is remove the angular scope from the element. There may be some neater hidden function in Angular to do this, but this works just fine too:
angular.element(document.getElementById('txtElem')).scope().$destroy();
This retains the model (and updates anything else still bound to it), but removes the binding from the element. Also, in your example above, there is no binding to remove because you aren't binding to any element, just displaying the model expression inline. My example shows this in action: http://jsfiddle.net/3jQMx/1/
You can call the unbind method that stops listening to the element where the ng-model attribute is present. See fiddle: http://jsfiddle.net/jexgF/
angular.element(document.getElementById('txtElem')).unbind()
unbind removes all event listeners, so whenever any changes are made, it wont listen for those and hence not go through the angular loop. I have also assumed that you are not using jQuery, but if you are, you can use a better selector than document.getElementById

Categories

Resources