In this post I’m going to outline my approach on writing Directives for Angular 1.x releases. There’s a lot of confusion around how and why and where to do things with Directives, but they are actually very simple once you grasp the concepts and separation ideas. This post isn’t going to cover nesting Directives/data flow into them from parent scopes etc, but will cover my ideal way of creating/structuring and separating all concerns in the Directive, and how to use
link properties correctly.
We’re going to cover a basic Directive, the structure, and how things should be structured to make the best use of Angular. Let’s create a pseudo “file upload” Directive to demonstrate how I’d approach.
Note, this code will not actually work, nor do I intend it to as this is purely for demonstration purposes on structuring Directives properly.
Following my AngularJS style-guide, naturally, we’ll create a basic Directive definition and pass it into Angular’s
Now the definition is in place, let’s add some basic properties to the file upload component:
This essentially completes my “Directive boilerplate”, the basics I need to get something up and running.
Controller (presentational layer)
Instead of binding the Controller (I do this with
link as well) to the Object (
controller: fn), I move the
controller property’s binding “out” into the main
Let’s have a look at adding some comments and moving the functions up top.
Sexy? Of course it is. By now you should’ve spotted
controllerAs: 'vm' which I alias the Controller under
vm (standing for ViewModel). This treats a Controller as a ViewModel for the “Presentation Model” design pattern. Read up on my ControllerAs article if you’re not familiar. Essentially instead of injecting
$scope, the Controller binds itself to the
$scope under our
vm alias, essentially creating
$scope.vm. I treat this as a Controller “Class” and we can dive into using the
this keyword instead of
this is great and far better than
$scope in my eyes, we only use
$scope for things like
$on events or
$watch, treating it a little differently than our Controller Class - the “ViewModel”.
We’ve got our function setup now, however I prefer using an “exports” style and bind all my functions and variables that way, adding any appropriate comments. Let’s see what that looks like altogether:
The next phase would be setting up some kind of file upload element with an
<input type=file> with a model bound to it. Let’s add that to our Directive with
ng-model attributes and values, and also an “upload” button with
ng-change (yes we could make this a form with
ng-submit but let’s keep it simple).
Let’s add some comments to our pseudo Controller to see how we’d handle the upload. Note I’ve added
UploadService (great name) to the
fileUploadCtrl arguments to be dependency injected.
Wait, not a lot has changed, why not? Let’s look at why.
Services (business logic layer)
Anything that communicates with an API, such as posting files off to a backend should never be done in a Controller, I repeat NEVER! Why? Separation of concerns. Of course we could do it, but that makes our lives harder as Controllers are to be treated as ViewModels, not ViewModelBusinessLogicThings.
I’m not going to write some pseudo code for our Service, but understanding why it’s passed into our Controller to abstract business logic is highly important to getting your Directive structures and dependencies manageable and scalable from the beginning.
All a Service should do is provide us with necessary Model data for our Controller (ViewModel) to make a copy of, and present to the user how we see fit.
Link function (DOM layer)
Directives are fantastic as they offer us a door into our Directives that shouldn’t be handled in the presentational logic layer (Controller) or a business logic layer (Service) - that thing we call the DOM (Document Object Model). We need the DOM sometimes, and Angular gives it to us on a plate.
Our file upload Directive wouldn’t be complete without some drag and drop funk, so we’ll use the DOM events provided to us,
dragover, drop etc. First let’s add a
<div class="drop-zone"> to our Directive, which will serve as our “drag and drop” area.
So, now we need to tie into our Directive. Our
link function comes in handy here, I’ve also injected
$scope, $element, $attrs (I like dollar-prefixing, sorry,
iAttrs just makes me cry).
We’ll need to bind our special event listeners to our
.drop-zone element. Remember, we want to keep our
link functions as light as possible, I rarely augment
$scope in them and neither should you.
Adding in some event listeners on our element:
Again, I’d comment and abstract these into a better, cleaner view, I don’t need to provide
dragover support for this demo, so we’ll drop those:
So what now, we’ve got our event listener setup, we can grab our files from
e.dataTransfer.files and pass them off to our upload API, but we want to use the same function as in our Controller, the
We can pass our Controller into the Directive itself, I use a
$ctrl alias just to keep it short and sweet, but this gives us access to our functions.
Smashing! Code reuse, using our Controller’s
uploadFiles method to pass it back into our API, this will then make any presentational logic changes alongside it, as previously mentioned we might show the uploaded file(s) to the user, so this would all be reused and handled in the Controller.
But wait, it won’t work just yet… We forgot the magic line
$digest cycle will then be kicked off so Angular can run our code and update our application with any data that’s changed. We need to do this as the
drop event listener is outside of Angular’s ecosystem and it never knew the event took place, so we have to tell it something’s happened.
All together now:
Recapping and MVVM (Model-View-ViewModel)
This approach allows the Controller to be used as a ViewModel, and to use the
link function to properly deal with DOM manipulation whilst making easy work of communicating back to our Controller. The approach also promotes better separation of concerns, as well as separation of the code itself, such as breaking the functions out and assigning them, rather than nesting under another layer of code (such as inside an Object).
Any suggestions/improvements feel free to make changes/create issues on GitHub. Enjoy!