Hacker News new | comments | show | ask | jobs | submit login
AngularJS Directive Design Made Easy (seanhess.github.io)
85 points by embwbam 1315 days ago | hide | past | web | 24 comments | favorite

Every time I see someone explaining the Angular API I cringe because it seems like an API that has no respect for the user actually forced to learn it. Everything seems so hostile. For example:

  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.

Directives are a DSL for creating new behavior using HTML. Once you learn the language, the symbols make sense and directives are easy to read. I think you are confusing "intuitive" with "familiar". Since you are not familiar with directives, you do not find them intuitive.

I think there is still a fair point. I'm quite familiar with AngularJS and writing quite advanced directives and I would agree some of the APIs are not as user-friendly as they should ideally be. Couple that with limited documentation and guidance and you have quite a steep learning experience.

Bottom line, things could be better.

I partially agree, directives are not easy to learn, and there is not enough good documentation or literature on how to write them well. However, I have found the api to be very expressive , concise, and flexible once mastered. I can't say the same for most real languages I work with everyday.

  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.

This tutorial assumes you already familiar with the basics of Angular's directives and focuses on the actual design of directives you wish to write.

I think you should assume that people may have a conceptual knowledge but not a working knowledge. If they had a working knowledge then your tutorial would be obsolete IMO. Other than that confusion I found it enjoyable.

FYI some of your examples are returning 404's

Thanks, fixed!

One nit-pick, isolate scopes (that is where you do `scope: { 'property': '=' }) are not the only (or even the best) way to bind data to a directive. In fact if you look at the AngularJS internals it's pretty much never used and instead they just use the current scope (whatever scope that directive is declared in).

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.

Interesting. I started with $scope.$watch(attrs.myBinding) first, and switched it to isolate scopes after doing more research.

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.

I really enjoyed your post. I agree that it's a good "directive smell" if your directive is reusable and application-agnostic.

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.

A single element cannot have more than once isolated scope. It only makes sense to use an isolated scope when a directive has a template associated with it. Otherwise, one should rely on the $parse + the "attrs" object + $scope.$watch to correctly handle expressions.

That seems strange. What happens if two attribute directives ask for an isolate scope? Does it give them the same one, or just fail?

I used isolate scopes in the article because I thought they were easier to understand, but I'm thinking of updating it.

After looking into it, angular seems to throw an error if two directives ask for an isolate scope.

The problem with isolate scope is that you need to pass in all the data through attributes. This is great if you want something reusable that won't collide. My rule of thumb is that if it's got its own template, it should be isolated.

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.

I thought you could do this since the scope of the transcluded part is a sibling to the isolate scope.

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


Ooh, I didn't consider transclusion. For the example I used (an attribute directive), would I make its template literally

  <div ng-transclude></div>

I had the same issue learning directives. In particular, I had a hard time getting "container" directives working right with isolated scope. I've come up with a rule of thumb: if this directive is a small, self-contained widget, opt for isolated scope. If not, keep it simple and either not create any scope (for really basic wrapper directives), or create a new scope using the scope prototype chain.

But I still have a ton to learn, so I'm not sure that's the best approach.

A few notes from my own experience:

- 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)

great article! a couple orthogonal notes:

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

Im curious if watching a function is conventional as in

   scope.$watch(function() {
        scope.bulb = controller.getState();
as opposed to watching a data-bound variable and what how that(watching a func) affects performance

Note: from this example: http://jsfiddle.net/codef0rmer/2ZrX3/

The only reason one would use scope.$watch in this fashion is to have a function executed when a digest occurs. The original purpose of the $watch function is to take 2 functions, one that returns the watched value and another to be executed when that value changes. The fiddle provided is not a very good example of "reusable" directives. The fiddle could be rewritten to use a directive that adds or removes a given class based on a boolean, and the rest of the state could be handled by built in directives.

"A good directive is not application specific" is a very generalist statement. You should encapsulate DOM manipulation in a directive, so even if it's not as reusable as ng-repeat, if you have to manipulate the DOM it's the way to go as well.

The point is - "All DOM manipulation belongs to a directive" but still it can be made application agnostic. What he is mentioning there is making it application agnostic - like for example not assuming in the directive that you have $scope.users on the $scope etc..

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | DMCA | Apply to YC | Contact