AngularJS Directive Design Made Easy
seanhess.github.io
seanhess.github.io
restrict: 'E'
Good luck knowing what that does at first glance. Instead why couldn't it have been simply called: type: 'element'
Or another one, surely the default for creating new custom elements would be to always replace the element, so we shouldn't have to manually set: replace: true
But we do because not all directives default to replacement, so maybe they thought it would get confusing if some defaulted to it. Instead they probably should have realized that the problem wasn't with the default for replacement, it was that everything was shoe-horned into a "directive". Why not have an easy shortcut for create elements vs. creating events or attributes... app.element('button', ...
app.attribute('toggle', ...
app.event('scroll', ...
And then you can have sensible defaults for each one without the user getting extremely confused. I would absolutely expect elements to replace by default.I'm sure this is an oversimplification, but surely there have to be less implementor-hostile ways to write these features. For example, even after reading the article I still have no idea what setting this on an attribute does:
scope: {
toggle: '='
}
That's ridiculous. I don't think there are enough designers on the Angular team.Bottom line, things could be better.
scope: {
toggle: '='
}
That's a joke, with minification these days what's the point in being so concise and confusing.Bad from an API design point of view and bad that the tutorial felt it didn't need some explination.
FYI some of your examples are returning 404's
This can be done by calling `$scope.$watch(attrs.myBinding)` directly with expression values passed in through element attributes. If necessary `scope: true` can be used create a child scope that inherits from the current scope. This is a good idea if your directive uses it's own controller. However this isn't necessary for most simple directives.
An isolated scope does exactly that, creates a scope for the directive completely isolated from the prototype chain of the containing scope (and thus the application's scope). This is used for complex directives like ng-repeat that need to create and expose their own scopes with the templates that you put inside the repeat blocks.
I know I initially fell into the trap of using isolate scopes to solve this problem. After looking through Angular's own directives for examples I learned other approaches.
I'm not sure exactly what the considered best practices are here (because there is almost no detail on issues like this in the documentation) but at least basing it off how the Angular core team builds their own directives I would suggest avoiding isolate scopes unless scope isolation is specifically necessary.
I'd be interested to hear if someone who knows for sure could chime in as to what the best practice is and why. I can update the article to match.
In that vein, I think you're right that an isolate scope is generally a better default because it makes it clear that the directive expects no dependencies on the inherited scope, and it prevents the directive from overwriting any scope properties set by an ancestor.
I think using an inherited scope makes sense if you're building very app-specific directives that are tightly-coupled to their ancestor.
I used isolate scopes in the article because I thought they were easier to understand, but I'm thinking of updating it.
But I still have a ton to learn, so I'm not sure that's the best approach.
However, this can really bite you in the butt. If you're doing something like
<div my-directive>{{foo}}</div>
where my-directive uses isolate scope, {{foo}} is broken. So I'd never use isolate scope for an attribute directive."The advantage of transclusion is that the linking function receives a transclusion function which is pre-bound to the correct scope. In a typical setup the widget creates an isolate scope, but the transclusion is a sibling (rather than a child) of the isolate scope. This makes it possible for the widget to have private state, and the transclusion to be bound to the parent (pre-isolate) scope."
<div ng-transclude></div>- think very hard about whether you want to use isolated scopes or not, because their presence affects the semantics of scope inheritance, and changing that decision after the fact can cause hard-to-debug issues in completely unrelated parts of your app. This is especially true if you're using transclusions
- exposing events is tricky if you're exposing model values because in those cases you need to call $digest before calling $apply. Another catch is that attaching the event trigger to a DOM event handler will often cause it to fire on programmatic model changes, which may or may not be what you want (it usually isn't desirable to fire a change event when set the initial value of a field from the controller, e.g. to populate an edit page, in an app that does save-on-blur)
scope.$watch(function() {
scope.bulb = controller.getState();
});
as opposed to watching a data-bound variable and what how that(watching a func) affects performanceNote: from this example: http://jsfiddle.net/codef0rmer/2ZrX3/
1. if you were going to actually do something like the 'scroll' example, make sure to debounce the scroll handler. it's a good practice for any 'scroll' or 'resize' event handler, but especially when you have to dirty check every time, it could get slow: for example, using http://underscorejs.org/#debounce
element.scroll(_.debounce(function(){...}, 15)
2. Sean mentions "A good directive only does one job" which is so true! and in the same spirit, note that it is better to not make things be element based directives (the "restrict: 'E'" part) but allow the end-user to use it as an attribute-based selector (eg: "restrict: 'A'") because you can have an element "mix-in" functionality of 2 directives by giving them both attributes, but an element can only be 1 type (a <modal> cant also be a <photo>).