Accidentally Defining A Directive Twice In AngularJS
Over the last two months, I've tried to demonstrate how powerful it is that AngularJS allows you to bind a single directive to multiple compilation and linking functions. Not only does this allow you to bind to different priorities on the same element, it also allows you to share elements, like the Script tag. The flip-side to this coin, however, is that if you accidentally define a directive twice, your application may start to break in a way that seems nonsensical.
Having nothing to do with the features that I mentioned above, we recently had a strage merge-conflict that broke our staging environment. One of the QA (Quality Assurance) engineers reported that nothing was loading. And, when I pulled it up in my browser, indeed, the entire AngularJS application was breaking.
Multiple directives [loading, loading] asking for new/isolated scope.
Two months ago, I would have looked at this and been completely baffled! But, thankfully, I just happened to be digging into AngularJS' ability to define a directive multiple times. As such, I had a hunch that "loading" was being defined twice and, therefore, being linked twice on the same element.
NOTE: While not the point of this post, this is a good reminder that if you believe that you should "isolate all the scopes," you're doing it wrong.
To help others debug, I wanted to put together a small example of this. Notice that in this code, the "bnThing" directive is "accidentally" being included twice:
And, here is the bnThing directive definition:
Multiple Directive Resource Contention
Multiple directives [bnThing, bnThing] asking for new/isolated scope on:
The problem is that bnThing is being linked twice on the same element. When I remove the accidentally-duplicated Script tag, the error goes away and the page functions as expected. Hopefully this will help you debug the problem if you ever find yourself with this seemingly insane error message.
Want to use code from this post? Check out the license.
Nice tips. Thanks for sharing.
Something I've taken up is the idea that every file must have a module definition, and every module should be used in only one file. Basically this means a unique module per file. (This excludes a shared templates module.)
This has several benefits:
1. The error above would have been spotted even quicker (by defining the same module twice, AngularJS would have thrown an error even earlier).
2. You can blindly concatenate your files without worrying about the order the modules are defined.
3. It's easier to see and manage dependencies: every file's dependencies *must* be declared in the same file, sort of like node's `require` or Java (etc) `import` statements.
4. It forces you to think about how correlated two objects are. If two objects share a module, they also must share a file, which means depending on one is always including the other.
The main drawback is your modules become sort of verbose, so there's some unnecessary bytes wasted declaring module names. This, in theory, could be mitigated by using some sort of custom AngularJS-specific "uglifier" to replace module names with shorter strings, much like the mangle option for UglifyJS, but I haven't worried about it yet.
I think my [production] code is probably way too ugly for that. Some of our Controllers are HUGE (thousands of lines). If we tried to put more than one thing in a file, things would get crazy. That said, I know our current file situation is not good either :D But, hey, this was the first AngularJS app any of us every built.
That said, I am not sure what you mean here:
>>> The error above would have been spotted even quicker (by defining the same module twice, AngularJS would have thrown an error even earlier).
Are you referring to something like:
angular.module( "myModule", [ "ng" ] )
The error you are talking about would then be trying to define "myModule" more than once? I am not sure which error you mean.
So, I was thinking about it more: having one module per file _won't_ fix the issue above, because you are allowed to completely overwrite a module in Angular (useful for testing, kinda bad in code, though). So it would actually have continued to hide your duplicate file (but you would not have had the error above, the new file would have just overridden the old one.)
As far as my technique, I still try to avoid having multiple objects per file. I only put two things in the same module if they are closely related (for example, a value or constant and the service that consumes that item, or a controller for a UI Router state).
What this means is your module naming scheme needs to be very strict, but very verbose. We are using something like:
foo.components.directive1 (reusable component-style directives)
foo.routes.people (routes are grouped to sort-of reflect the state hierarchy)
foo.helpers.* (shared helper objects)
foo.services.* (shared services)
foo.models.* (shared angular-data models)
If a controller or directive is unique to a specific route (or other module), it's grouped under that with a unique module, then the parent module depends on it.
So you definitely have a lot of modules, with a lot of names, but it's working so far.
Another technique, sort of related to you mentioning huge controllers, is I view large controllers as code smell. Generally speaking, I try to break data and flow control logic out into services (factories). I generally only want to see controllers gluing logic to the views (also, via controllerAs whenever possible).
We're no where near as far along as you guys (I assume this is in reference to InVisionApp), but we're making progress on getting our alpha project out the door. Hopefully these early decisions don't bite me later!
Oh, and I should have mentioned, dependencies are mostly wired in closest to where they are used, so there's a `foo.routes` which has all the top-level routes, then `foo.routes.people` might depend on `foo.routes.people.person`, etc.
@Ben: First of all, THANK YOU! I was banging my head against a wall for two days trying to solve this issue. You certainly saved me some time.
As for @Phil's comments, his second pass assumptions are correct; defining one module per file does *not* solve the problem, nor did make finding the problem any easier. The project I am working on uses AMD-style module loading so that we can lazy load components. The directive being defined was completely custom, and it was verified that it was only being used in my markup in one place. The "one place" it was being used was within a modal template being passed to `$mdDialog` (we use angular-material). The error was only getting thrown once the dialog got popped, so it cannot *always* be assumed that angular will pick up on these types of errors on page load.
The weird thing is that there are other directives in the same block, all declaring an isolate scope, that were causing no problems at all. In order to solve my problem, I had to remove the references to the problematic directive in the `define()` block of the parent directive definition and the module dependency block in `app.module('foo', [...])` within that file. Given that the directives were loaded once globally already, the duplicate dependency definition was causing the problematic directive file to load twice.
I wish I knew why the other directives weren't having the same issue, but I'm just fine with getting the problematic directive to work. Thanks again! =]