preface
I've seen many questions regarding using ng-attr and directives however I have yet to see this specific case being implemented.
code & plunkr
http://bit.ly/1s6gWkD
use case
I'm trying to dynamically add a loading overlay into target DOM elements via an attribute directive. The idea is that by virtue of the target DOM element possessing the attribute directive, the DOM will have the overlay appended to its children.
I've approached this from various angles with no luck. Because this is going to be used in many places where we might want to block certain UIs but not fully block the app with a modal, I am hoping to keep our templates clean and attach this dynamically.
questions
is this possible (assuming there IS a directive life-cycle event to tackle this), BTW this is what I would call the dynamic approach
if not possible, I did try a few less-than-ideal 'static' approaches using this such as with no luck
ng-class="{loadOverlay: hasOverlay}"
ng-attr-load-overlay="hasOverlay"
observations
I do realize that there may be an issue with this approach as once the attribute is removed, there may not be a life-cycle event in the directive to know that it is ordered to remove itself. I don't know enough about directives to know if this is the case.
ideally what I'm looking for
target DOM element w/o overlay
target DOM element w/ overlay
settling for the static approach
After giving this some thought I think having a more versatile directive combined with the 'static' approach is the best.
plunkr solution
http://bit.ly/1toMCV9
snips
.directive('loadOverlay', function() {
return {
restrict: 'EA',
scope: true,
link: function(scope, element, attrs) {
var id = 'nx-load-overlay-' + parseInt(Math.random() * 1000);
function toggleOverlay(show) {
if (show === true) {
var d = '<div id="' + id + '" class="nx-load-overlay"><div class="nx-load-overlay-spinner"><span class="fa fa-cog fa-spin fa-3x"></span><br/><span style="font-weight:bold; font-size:larger;">loading</span></div></div>';
element.append(d)
} else {
$('#' + id).remove()
}
}
if (attrs.loadOverlay)
scope.$watch(attrs.loadOverlay, toggleOverlay);
else
toggleOverlay(true)
}
}
})
Related
When building angular directives, I've found there is more than one way to pass a value from the parent scope to the child scope. Some ways I'm aware of are:
Don't use an isolate scope at all, and the child will simply have
access to the parent scope (you can mount a pretty good argument that this is bad).
Use the attributes parameter of a link function.
Use an isolate scope and bind to the attribute (e.g. param: '=')
The codepen here: https://codepen.io/ariscol/pen/WEKzMe shows two similar directives, one done with link and one done with 2-way binding in an isolate scope. Furthermore, it shows how they differ as far as 1-time binding compared to 2-way binding. For reference, here are the two directives:
app.directive("contactWidgetWithScope", function() {
return {
restrict: 'E',
template: '<div ng-bind="contact.name"></div>'
+ '<div ng-bind="contact.title"></div>'
+ '<div ng-bind="contact.phone"></div>',
scope: {
contact: '='
}
};
});
app.directive("contactWidgetWithLink", function() {
return {
restrict: 'E',
template: '<div ng-bind="name"></div>'
+ '<div ng-bind="title"></div>'
+ '<div ng-bind="phone"></div>',
scope: {},
link: function(scope, elem, attrs) {
scope.name = attrs.contactname;
scope.title = attrs.contacttitle;
scope.phone = attrs.contactphone;
}
};
});
Now, if I were trying to decide which way was "better", I might consider how I was going to use this directive. If I was going to have a thousand contacts, and I wanted to use this directive to list all one thousand contacts on a page, in an ng-repeat, for example, I imagine that I would have significantly better performance with link, as it won't add any watchers. On the other hand, if I wanted this directive to be incorporated into a page header, and I wanted the contact details to be updated as you clicked on any given contact in a list, I would want 2-way binding, so that any change to some "selectedContact" property in a parent scope would be automatically reflected in this directive. Are those the proper considerations? Are there others?
To add to my confusion, it is simple to add an observer to a linked attribute and achieve a 1-way binding such that a change in the value of the attribute will be reflected in the child. Would doing this have more or less of a performance impact? Conversely, I imagine you could do a 1-time binding on the value of the scope version and thereby eliminate the performance impact, e.g.: <contact-widget-with-scope contact="::vm.contact">. That should work, right? Seems like that option gives you a lot of flexibility, because it means the person who invokes the directive can decide if they want to pay the performance price to get the benefit of 2-way binding or not. Are these considerations accurate? Are there other things I ought to consider when deciding how to make values available to my directives?
I have a small angular app that has a mobile and a desktop view. The client wants the control for each of those views positioned differently in a way that I can't really accomplish with clever use of HTML, as below:
Normal:
Mobile:
The controls are absolutely identical other than the position, so it feels like a waste to repeat the code. I was going to just plop them in a separate template, but first I wanted to see if I missed some clever directive I can use to accomplish this more easily.
I would likely use the flexible box model (display: flex) for this. You can find a good overview for that on CSS Tricks. Pay particular attention to the order property for rearranging your items.
If you're looking for an Angular-specific way of doing this, if you made each of your controls a directive, you could wrap them in a parent directive that would compile the template when it loads.
.directive('myTemplate', ['$compile', function($compile) {
return {
link: function(scope, element, attributes) {
var directiveOne = angular.element('<div/>')
.attr('directiveOneName', '')
.attr('otherAttribute', attributeValue),
directiveTwo = angular.element('<div/>')
.attr('directiveTwoName', '');
$compile(directiveOne)(scope);
$compile(directiveTwo)(scope);
if (/* check window width here */) {
element.append(directiveTwo);
element.append(directiveOne);
} else {
element.append(directiveOne);
element.append(directiveTwo);
}
},
restrict: 'AE'
};
}]);
Performance on that approach might not be fantastic, though.
I'm just getting started on Angular and am trying to wrap my head around proper directive use. I'm writing a custom directive that takes an object array and parses it into a variable number of vertical divs. It's basically a grid system where the elements are arranged into stacked vertical columns rather than in rows. The number of divs dynamically varies with the width of the screen, requiring dynamic changes in the div class as well as reconstructing the ordering of the array elements in the div columns as the page resizes.
When I use the contents of the template as plain, static HTML, everything loads just fine. The filters dynamically change the dataset when you use the input fields, etc.
When I use my directive, the initial page-load looks fine. However, dynamic filtering is broken - it is no longer bound to the input fields. More importantly, on a page resize, the HTML fails to compile at all, leaving a blank screen and uncompiled directive tags in the DOM.
I don't know Angular well enough to troubleshoot this. If I had to guess, it sounds like something is not being bound properly on the page $compile due to a problem with scope.
Note: I know doing string concat for the template is poor practice but I just want to get things working before I start messing around with nesting directives.
Edit: here's a link to the Github repo for my front-end code: https://github.com/danheidel/education-video.net/tree/master/site
HTML
<body ng-controller="channelListController">
Creator: <input ng-model="query.creators">
Tags: <input ng-model="query.tags">
query: {{query}}
<div id="channel-view">
<channel-drawers channels="channels"></channel-drawers>
</div>
</body>
JS
.controller('channelListController', function ($scope, $http){
$http.get('api/v1/channels').success(function(data){
$scope.channels = data;
});
})
.directive('channelDrawers', function($window, $compile){
return{
restrict: 'E',
replace: true,
scope: {
channels: '='
},
controller: 'channelListController',
//templateUrl: 'drawer.html',
link: function(scope, element, attr){
scope.breakpoints = [
{width: 0, columns: 1},
{width: 510, columns: 2},
{width: 850, columns: 3},
{width: 1190, columns: 4},
{width: 1530, columns: 5}
];
angular.element($window).bind('resize', setWindowSize);
setWindowSize(); //call on init
function setWindowSize(){
scope.windowSize = $window.innerWidth;
console.log(scope.windowSize);
_.forEach(scope.breakpoints, function(point){
if(point.width <= scope.windowSize){
scope.columns = point.columns;
}
});
var tempHtml = '';
for(var rep=0;rep<scope.columns;rep++){
tempHtml +=
'<div class="cabinet' + scope.columns + '">' +
'<div class="drawer" ng-class="{' + ((rep%2 === 0)?'even: $even, odd: $odd':'even: $odd, odd:$even') + '}" ng-repeat="channel in channels | looseCreatorComparator : query.creators | looseTagComparator : query.tags | modulusFilter : ' + scope.columns + ' : ' + rep + '">' +
'<ng-include src="\'drawer.html\'"></ng-include>' +
'</div>' +
'</div>';
}
console.log(tempHtml);
element.html(tempHtml);
$compile(element.contents())(scope);
}
}
};
})
The $compile function should be implemented when you want to manipulate your template. The link function should be implemented when you want to bind your template to your scope and/or setup any watchers. If you have dynamic HTML that you're inserting into your DOM, then ask your self these questions:
Are you modifying the template? If so, then create an element template (angular.element(...)) and append it to your element parameter.
Have you modified the template (step 1) and your template contains binding expressions, interpolation expressions, and/or attributes that should bind from other templates? If so, you need to compile and link your element you created from step 1.
Here is an example:
.directive('myDirective',function($compile) {
restrict: 'E',
scope: '=',
compile: function(element, attr) {
// manipulating template?
var e = angular.element('<div ng-model="person">{{person.name}}</div>');
element.append(e);
// the following is your linking function
return function(scope, element, attr) {
// template contains binding expressions? Yes
$compile(e)(scope);
};
}
});
To fix your code, try moving the template manipulation to your compile function, and in your linking function, call $compile(e)(scope).
First, thanks to pixelbits and pfooti for their input. It put me on the right track. However, I wanted the answer to be a clean slate since our discussions got into technical matters that ended up being tangential to the actual answer.
Basically, this question is poorly framed. After doing more reading, it became clear that I was using the Angular elements in ways they aren't really intended for.
In this case, I'm doing a bunch of model manipulation in my directive and it really should occur in the controller instead. I ended up doing that and also moving the window resize handler code to another component.
Now, I don't even need a directive to properly format my data. A couple of nested ng-repeats with a dusting of ng-class and ng-style do the job just fine in 2 lines of HTML.
<div ng-repeat="modColumn in splitChannels"
ng-class="{'col-even' : !$even, 'col-odd' : !$odd}"
ng-style="{ width: 99 / windowAttr.columns + '%' }"
class="cabinet">
<div ng-repeat="channel in modColumn"
ng-class="{'row-even' : !$even, 'row-odd' : !$odd}"
ng-cloak
class="panel roundnborder">
<ng-include src="'panel.html'"></ng-include>
</div>
</div>
If I could give a bit of advice from one Angular beginner to another, it would be this: if your code is getting complex or you're digging into the internals of things, step back and rethink how you're using the Angular components. You're probably doing an action that should be done in another component class. Proper Angular code tends to be very terse, modular and simple.
Is there a reason you're doing it with templates?
Can't you bind the number of columns to a variable on the scope? I've done similar stuff with just fiddling around with either ng-if directives to hide stuff that's not important now, or to have general layout stuff attached to the current scope (I generally stuff it all into properties on $scope.view)
There's also plenty of this kind of stuff that already works in css3's media selectors as well, without needing mess with the DOM at all. Without a clearer picture of what you're trying to accomplish I'm not sure if this is super-necessary. More than one ways to skin a cat, etc etc.
Otherwise, #pixelbits is right - if you are fiddling with the DOM tree directly, that needs to happen in compile - values going into the DOM goes into link.
I wanted to write a directive that only applied to IMG tags throughout my whole page, and initially I thought I would have to decorate each tag with a custom directive name, such as:
<img my-img />.
But, while I was putting together some sample code for this question, I decided to see if the directive would match on the element IMG itself. And it worked!
Here's what I did: http://plnkr.co/edit/z4n4a3MN89nRNYyXKCih?p=preview
app.directive('img', function () {
return {
restrict: 'E',
link: function (scope, element) {
element.bind('load', function () {
element.addClass('fadeIn');
});
element.bind('error', function () {
element.removeClass('fadeIn');
});
}
};
});
As you can see, I wanted all images on a page to fade in when they loaded. I wanted to do this in an angular fashion without using jQuery, so I thought this was a good approach, but is it good practice? In my case, I really do want this logic to apply to all the images on my page (and there may be hundreds), so I thought this would be a clean way of doing it, but for the life of me I haven't found anywhere where anyone else does this (i.e., matching a directive to an IMG tag or any standard tag for that matter).
I think I would avoid the img directive. Take note that Angular has already added their own directives which match html element names (e.g. - form, input, select, script), so it seems conceivable that there could potentially be a conflict if they (or any library you use) utilize the same directive name. And do you really want to fade in all images? What if you use an image as a decoration on the page?
It seems like it would be best to instead add the attribute. It's very intuitive with nominal effort. If you don't care about the built in attributes, you could also create your own element (e.g. ).
I'm evaluating whether or not to use AngularJS for a web project, and I'm worried about the performance for a feature I need to implement. I would like to know if there's a better way to implement the functionality I'm trying to in AngularJS.
Essentially, it seems to me the time it takes AngularJS to react to an event is dependent on the number of DOM elements in the page, even when the DOM elements aren't being actively changed, etc. I'm guessing this is because the $digest function is traversing the entire DOM.. at least from my experiments, that seems to be the case.
Here's the play scenario (this isn't exactly what I'm really trying to do, but close enough for testing purposes).
I would like to have angularJS highlight a word as I hover over it. However, as the number of words in the page increases, there's a larger delay between when you hover over the word and when it is actually highlighted.
The jsfiddle that shows this: http://jsfiddle.net/czerwin/5qFzg/4/
(Credit: this code is based on a post from Peter Bacon Darwin on the AngularJS forum).
Here's the HTML:
<div ng-app="myApp">
<div ng-controller="ControllerA">
<div >
<span ng-repeat="i in list" id="{{i}}" ng-mouseover='onMouseover(i)'>
{{i}},
</span>
<span ng-repeat="i in listB">
{{i}},
</span>
</div>
</div>
</div>
Here's the javascript:
angular.module('myApp', [])
.controller('ControllerA', function($scope) {
var i;
$scope.list = [];
for (i = 0; i < 500; i++) {
$scope.list.push(i);
}
$scope.listB = [];
for (i = 500; i < 10000; i++) {
$scope.listB.push(i);
}
$scope.highlightedItem = 0;
$scope.onMouseover = function(i) {
$scope.highlightedItem = i;
};
$scope.$watch('highlightedItem', function(n, o) {
$("#" + o).removeClass("highlight");
$("#" + n).addClass("highlight");
});
});
Things to note:
- Yes, I'm using jquery to do the DOM manipulation. I went this route because it was a way to register one watcher. If I do it purely in angularJS, I would have to register a mouseover handler for each span, and that seemed to make the page slow as well.
- I implemented this approach in pure jquery as well, and the performance was fine. I don't believe it's the jquery calls that are slowing me down here.
- I only made the first 500 words to have id's and classes to verify that it's really just having more DOM elements that seems to slow them down (instead of DOM elements that could be affected by the operation).
Although an accepted answer exists allready, I think its important to understand why angularJS reacts so slow at the code you provided. Actually angularJS isnt slow with lots of DOM elements, in this case it's slow because of the ng-mouseover directive you register on each item in your list. The ng-mouseover directive register an onmouseover event listener, and every time the listener function gets fired, an ng.$apply() gets executed which runs the $diggest dirty comparision check and walks over all watches and bindings.
In short words: each time an element gets hovered, you might consume e.g. 1-6 ms for the internal
angular dirty comparision check (depending on the count of bindings, you have established). Not good :)
Thats the related angularJS implementation:
var ngEventDirectives = {};
forEach('click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
function(name) {
var directiveName = directiveNormalize('ng-' + name);
ngEventDirectives[directiveName] = ['$parse', function($parse) {
return {
compile: function($element, attr) {
var fn = $parse(attr[directiveName]);
return function(scope, element, attr) {
element.on(lowercase(name), function(event) {
scope.$apply(function() {
fn(scope, {$event:event});
});
});
};
}
};
}];
}
);
In fact, for highlighting a hovered text, you probably would use CSS merely:
.list-item:hover {
background-color: yellow;
}
It is likely that with newer Angular Versions, your code as is, will run significantly faster. For angular version 1.3 there is the bind-once operator :: which will exclude once-binded variables from the digest loop. Having thousands of items, exluded will reduce the digest load significantly.
As with ECMAScript 6, angular can use the Observe class, which will make the dirty comparisiion check totaly obsolete. So a single mouseover would result internally in a single event callback, no more apply or diggesting. All with the original code. When Angular will apply this, I dont know. I guess in 2.0.
I think that the best way to solve performance issues is to avoid using high level abstractions (AngularJS ng-repeat with all corresponding background magic) in such situations. AngularJS is not a silver bullet and it's perfectly working with low level libraries. If you like such functionality in a text block, you can create a directive, which will be container for text and incapsulate all low level logic. Example with custom directive, which uses letteringjs jquery plugin:
angular.module('myApp', [])
.directive('highlightZone', function () {
return {
restrict: 'C',
transclude: true,
template: '<div ng-transclude></div>',
link: function (scope, element) {
$(element).lettering('words')
}
}
})
http://jsfiddle.net/j6DkW/1/
This is an old question now, but I think it's worth adding to the mix that Angular (since v1.3) now supports one time binding which helps slim down the digest loop. I have worked on a few applications where adding one time binding reduced the number of watches dramatically which led to improved performance.
ng-repeat is often responsible for adding a lot of watches, so you could potentially consider adding one time binding to the ng-repeat.
ng-repeat="i in ::list"
Here is a summary of a few techniques that can be used to avoid adding unnecessary watches
http://www.syntaxsuccess.com/viewarticle/547a8ba2c26c307c614c715e
Always profile first to find the real bottleneck. Sometimes it might be not something you initially suspect. I would suspect your own code first, then Angular (high number of watchers being the main feature leading to sluggish performance). I described how to profile and solve different performance problems in an Angular app in detailed blog post https://glebbahmutov.com/blog/improving-angular-web-app-performance-example/
Get zone.js from btford and run all functions in a zone to check their times, then create zones to handle ajax code (crud) and other zones for static code (apis).
Alternatively, limiting ng-repeat and/or disabling two-way binding on objects goes a long way atm.. a problem that web components already covers by using shadow dom leaving the top crispy to touch. still zone.js - watch the video through the link on plausibilities.
Well, I can see you are using $watch. Angular recomends $watch whenever it is very much needed. SCenarios like updating a variable through ng-model
http://jsfiddle.net/5qFzg/10/
Suraj