AngularJS directive that binds to multiple events - angularjs

I have an AngularJS directive that binds to the scroll event:
angular.element($window).bind('scroll',function()
{
});
It's used to update the class of a fixed element when it reaches the bottom of the page.
The problem I have is that when a function fires new elements are drawn and the page grows longer. When scrolling again this is detected properly and the element updates it's class correctly, but until the scroll event fires it looks wrong.
Is there a way to bind the directive to also fire when new elements are drawn to the DOM as well as to scroll?

How are new elements added to the dom? There's multiple ways to attack this depending on how. Let's say their being rendered via an ng-repeat. For each element attach a new directive (via class, attribute, etc....) and then in the directives controller fire the event you require. Something like:
<div data-ng-repeat="item in items" class="myDirectiveName">...</div>
Then in your directive:
.directive("myDirectiveName", function(){
return {
restrict: 'C',
link: function(scope){
//fire event
scope.$emit("newElementAdded", element);
}
};
})
edit
If you only want it to fire on the last one, you can do a check
link: function(scope){
if(scope.$last){
...
}
}
Ng-repeats come with a few things attached to their scopes that you can use, $last, $first etc... check it out here: https://docs.angularjs.org/api/ng/directive/ngRepeat

Directly listening to events with bind(or on) will not cause a digest cycle in angular. You will need to call $apply on the scope to make sure all watchers are called.
Also, depending on if you add the elements manually to the DOM you will need to use the $compile service to have angular parse and execute all directives in the elements.

Related

How to $compile a AngularJS directive for html that is in a Leaflet map control?

I am trying to add a modal button to a leaflet map legend, with a AngularJS framework. Check out the map and modal buttons in this demo (modal buttons are the round info buttons): http://skyplan.ch/maps/dev/gwm6/
The modals are inserted using an Angular directive that looks like <div infomodal="views/modals/popup.html">.
I need to place such a button in the map legend. The problem is that since the legend is created dynamically, I need to $compile the modal directive. However, I only have access to the the content of the map control via the onAdd function of the control iControl.
I have tried calling $compile on the div that is generated in the onAdd function, but that hasn't worked. Has anybody tried to do something like this before?
I got it using the following steps:
Got the scope of your main controller in a variable.
var scope = angular.element('[ng-controller]:first').scope();
Created a custom button to test change of the language
var $div = $('<div><button ng-click="lang=\'de\';changeLanguage();">Change Language to DE</button></div>');
Appended it to your leaf legend control
$('.leaflet-bottom.leaflet-right:last .leaflet-control').append($div);
Force the angular to compile the appended the HTML control specifying the scope context of your main controller (1st step)
angular.injector(['ng']).invoke(function($compile) {
$compile($div)(scope);
});
Click the button and the "magic" happens. :-)
You may need to append (step 3) and recompile (step 4) after each MapBox update.
Since I was outside your project, that was the steps necessary to achieve the solution, but once your have the directive logic (not ugglified) you have full access to the leafpanel thru the $element and the $compile using directive service injection.

Dynamic layout using angularjs (the angularjs way)

I just recently started tinkering with AngularJs, so my question is fairly basic. For concreteness's sake, let me start with the minimal setting: a page with banner, body and footer. The goal is to make banner and footer stay at the top and bottem of the page (css position: fixed) and the body will fill in the rest.
If I were to use jQuery, I could set position: fixed for body, and listen to window's resize event to determine where I should put my body.
Presumably, I could do the same in AngularJs. But I've read many places that one shouldn't try to manipulate the layout from the code, since it's not in the philosophy of AngularJs. So, what's the best way to achieve this in the AngularJs way? Thanks.
Dynamic layout usually refers to a layout that changes depending upon the current route. Routing is how AngularJS associates a URL with a Controller and data. It becomes an important part of an app once you go beyond just 1 page.
Multiple Views
If you use something like ui-router (http://angular-ui.github.io/ui-router/) it allows you to define views that can have child views.
In your example, the header, body and footer would all be child views of a parent view. Each view would have it's own template, controller and data. When routing changes you can have just one or more of those view change, while the other views remain the same.
Binding Events
You can do all the same event bindings in AngularJS as you can in jQuery. The difference is the life-cycle of those bindings.
If you bind to the click event for a directive using the directive's element, then you don't have to worry about that binding when the directive is destroyed. AngularJS will remove the binding automatically.
When you bind to something like the window.resize event, then you have to be more careful.
link: function($scope, $el, $attrs) {
// this doesn't have to be unbind
$el.bind('click',function(e) {
// do stuff
});
// this has to be unbind on destroy
angular.element($window).bind('resize.mybinding',function(e) {
// do stuff
});
$scope.$on('$destroy',function(){
angular.element($window).unbind('resize.mybinding');
});
});
I've seen a lot of AngularJS source code bind to window.resize when it wasn't necessary. Only you know when it's needed, but sometimes you can get away with just using a watcher.
link: function($scope, $el, $attrs) {
$scope.$watchGroup(function(){
return [$window.innerWidth,$window,innerHeight];
},function(values){
// do stuff
});
});
The above does the same thing as binding. It executes a closure function when the size of the window changes, but the difference is that it's triggered only during digest and not by the window resize event. This can sometimes be more efficient for performance. As the resize is relative to only when the directive is digested.
A lot of AngularJS applications use JavaScript code to manage the layout of an application. How much JavaScript code you use, and why you do it that way is up to you, but keeping layout separated from the JavaScript side has a lot of advantages. It's better to let CSS handle it.
Bootstrap provides a nice plug-in called Affix that does just that.
<div id="banner" data-spy="affix">Fixed Banner</div>
<div>Body</div>
<div id="footer" data-spy="affix">Fixed Footer</div>
More on affix here for example:
http://www.tutorialrepublic.com/twitter-bootstrap-tutorial/bootstrap-affix.php

Problems binding Angular custom directive to model

I am trying to get two multiselect widgets to stay in sync, but am only able to get the behaviour going in one direction.
One widget is on the main page, and the other one is in an angular ui-bootstrap modal. Both multiselect widgets are created using a custom directive. Each instance of the directive is bound to a different controller (MainCtrl and ModalCtrl). The "selected" attribute of each instance of the directive is bound to a scoped variable in their respective controller, which is in turn bound to the same getter method in the Model (getSelectedFilters).
If I change the selection on the widget on the main page, the change is reflected in the widget in the modal panel. However, if I change the selection in the modal panel, the change isn't reflected in the widget on the main page even if the Model has been properly updated (the button "Show currently selected" will show the correct selection even though the widget doesn't). I don't understand the difference at all.
Here is a stripped down plnkr illustrating my problem.
http://plnkr.co/edit/nC5bkGFE4LkYZsaxdjL7?p=preview
I am entirely new to Angular, and would appreciate any input as to why my directive on the main page isn't being updated properly when the data in the model changes.
Version 7 of the plnkr was what I had initially posted.
Basically, calling $apply on $scope.selected whenever the value was changed was not enough to notify the other directive of the change. I also had to setup a $watch:
$scope.$watch('selected', function(newVal, oldVal) {
select2.val(newVal).select2(options);
});
I am now completely unclear as to why the select2 directive in the modal was updating at all without a $watch when I was modifying the select2 directive on the main page. I would love if someone could explain what was going on there! To anyone who might know, go back to version 7 to see what I mean.

When does controller come into play in AngularJS?

I am trying to understand the $scope and how controller and view are clued together. When Angular first runs through the DOM elements, when it finds ng-controller what does it do? I know when it finds the binding variables, it creates either watch or keydown events and also for the events it injects itself and watch for the other related events. It is done by creating a scope for that given DOM element. so when an item changes in view or model it can push the value to proper places. My question is when does controller is instantiated and $scope get injected into it and how $scope calls associated methods when a event happens?
Thanks
You would have to go through the documentation on their site for clarity. From what I understand when the framework encounters the ng-controller attribute on the view, it will attach and instantiate the controller. Any code directly within the controller function will run right there. If you want code to run only on certain events like a click event then you put ng-click='myFunction()' on the element and myFunction as a $scope property. If you want to run code inside a controller on some other event then you need to use $scope.$on within the controller and $scope.$broadcast to trigger the event outside. Note that controller should only have business logic. Any code to directly manipulate DOM goes within a Directive. Use scope property in the directive to bind variables and functions between the controller and the directive.
Again, as I said, it will help to go through documentation and videos on youtube to get a better understanding on the foundations of AngularJS.

Is their a better way for a controller to 'call' a directive

I have a directive that creates and manages a bootstrap modal dialog.
Currently I have the directive watch a boolean held on the controller. The controller can then set this to true to have the modal dialog display.
This seems kinda messy. Is there a better way?
The directive in action:
<modal trigger="shouldDisplayModal" title="{{modalTitle}}"
message="{{modalMessage}}" positiveclick="okClicked()"
negativeclick="closed()"
positivelabel="Ok" negativelabel="Cancel"/>
The watch in the controller of the directive:
// watch the trigger value. expected to be boolean
$scope.$watch('trigger',function(newValue, oldValue){
if (newValue)
{
// enable any disabled buttons
modalElem.find('button').removeClass('disabled');
// show the dialog
modalElem.modal('show');
}
else
{
// hide the dialog
modalElem.modal('hide');
}
});
Here is a working example: http://jsfiddle.net/rabidgremlin/Ya96z/31/
UPDATE: Here is a fixed up example that corrects some issues with multiple directives on a page: http://jsfiddle.net/rabidgremlin/sjbCJ/1/
I was going to suggest using ng-show inside your directive's template (this what the dialog component on the directive page does, along with a visible attribute that is just like your trigger attribute), but then I saw that you also need to enable some buttons before modifying the visibility.
So, I think what you have is fine, and I don't see it as messy. Either your directive has to $watch for something, or you could create the dialog when an event happens -- this seems to be what the $dialog service does that #pkozlowski mentioned in the comments. The latter would not need a trigger attribute.
I blogged about working with angular and bootstrap modals just a couple weeks ago.
My solution involves a service, all of the hide/show magic for the modal is handled by bootstrap's javascript, and angular just worries about the data.
http://willvincent.com/blog/angularjs-and-twitter-bootstrap-playing-nicely

Resources