

AngularJS Directive Design Made Easy - embwbam
http://seanhess.github.io/2013/10/14/angularjs-directive-design.html

======
ianstormtaylor
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.

~~~
mortalapeman
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.

~~~
lucisferre
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.

~~~
mortalapeman
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.

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

~~~
embwbam
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.

~~~
cjcenizal
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.

~~~
mortalapeman
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.

~~~
embwbam
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.

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

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

------
vladgur
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/](http://jsfiddle.net/codef0rmer/2ZrX3/)

~~~
mortalapeman
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.

------
ryankshaw
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](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>).

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

~~~
ganarajpr
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..

