Objectives |
---|
Students will be able to: |
Explain why custom directives are valuable |
Create a custom directive |
Before this lesson, students should already be able to:
- Describe Angular directives
As you've seen by now, directives make up a huge amount of the code you work with in Angular. Angular was designed to be an extension of HTML - a way to have custom-defined interactive tags of your own making.
While we've been leveling up at using the directives that come with Angular, it's time to start making some up ourselves.
One of the most obvious uses of this is when you've got repetitive code to render some information or data. If you're using a bunch of the same component all over the place, you would want to use the DRY principle – Don't Repeat Yourself. Instead of writing that component in several different views, you can extract it to a custom directive! We can just reference that directive whenever we need to use it and not worry about repeating the code to render it.
Examples:
- A "card" with info on it (see more below)
- Loading indicator or progress bar
- Stars rating mechanism
- Date picker
- Color picker
As an example, we're going to mess around with duplicating something that's becoming a common pattern in interface design – the concept of a card. Applications like Twitter, Pinterest, Facebook, and others are moving towards this design pattern.
Everyone's favorite CSS framework, Bootstrap, is even on board, where in version 4+ you're able to create a card with just some CSS classes:
Let's see if we can make something similar, wrapped up in our own custom HTML element. We want to take something like this:
<div class='card'>
<h4 class="card-title">{{card.question}}</h4>
<h6>Cards Against Assembly</h6>
</div>
and end up with a reusable <wdi-card></wdi-card>
component, maybe something like:
<wdi-card question="{{card.question}}"></wdi-card>
We want it to look like:
Take five minutes and inspect our starter code. You'll see a pretty normal Angular app, and since we're repeating using those cards, there are a few consistent tags we're repeating every time we render a card. So we're going to make those cards a custom-defined directive to cut out the repetition.
Since we're going to see how Angular handles templates, we'll need to start a simple static webserver. We could build a quick express app, but we can just use:
python -m SimpleHTTPServer
cd starter-code
and then type python -m SimpleHTTPServer
. This will start a simple webserver that
only serves up static files from the current directory
This will let us serve up our HTML and JS files without running into Cross Origin issues.
Why we do this
We need to do this since by default Chrome doesn't let us request another local file as an HTTP request. This prevents someone from fishing for a specifc file on your computer when you're visiting a site.Rather than just throw this wherever, let's make a file dedicated just to that function. Clean code, yo.
I called it:
cardDirective.js
Don't forget to include this JS file in your index.html
file.
Just like controllers and routing configurations, the first line is a simple extension of angular
:
angular.module('CardsAgainstAssembly')
.directive('wdiCard', wdiCard);
An important thing to point out: The first argument is the name of the directive and how you'll use it in your HTML. Angular converts camelCase
to snake-case
for us, so if you want to use <secret-garden></secret-garden>
in your HTML, name your directive .directive('secretGarden', myFunctionIHaventMadeYet)
.
Remember, in the official Angular docs it's called ngClass
or ngRepeat
, but in your HTML you use ng-class
and ng-repeat
.
Now, we obviously need a function named wdiCard
!
function wdiCard(){
var directive = {};
return directive;
}
Nothing fancy yet - we're just constructing an object and then returning it. We'll put some specifics in there now, but that's simple so far.
You've got a couple interesting options when making your own directives. We'll go through them all, quickly, and you can play with them on your own in a bit.
restrict
replace
template/templateUrl
scope
While the name isn't obvious, the restrict
option lets us decide what kind of directive we want to make. It looks like this:
restrict: 'EACM',
E
is element. An HTML element, like<wdi-card></wdi-card>
A
is attribute. Like<div wdi-card="something"></div>
C
is class. Like<div class="wdi-card"></div>
M
is comment. Like<!-- directive: wdi-card -->
You can choose to have just one, all of the above, or any combination you like. You should steer towards elements & attributes as much as possible, though – classes can get messy with other CSS classes, and comments could just end up weird if there isn't a good reason for it.
For ours, let's play with just an element.
function wdiCard(){
var directive = {
restrict: 'E'
};
return directive;
}
Replace is pretty straightforward. Should this directive replace the HTML? Do you want it to get rid of what's in the template & swap it out with the template we're going to make? Or add to it, and not remove the original. For example, replacing would mean:
<div ng-repeat="card in cardsController.questionsList" >
<wdi-card></wdi-card>
</div>
Would actually render as:
<div ng-repeat="card in cardsController.questionsList" >
<div class='card'>
<h4 class="card-title">{{question}}</h4>
<h6>Cards Against Assembly</h6>
</div>
</div>
See, it's replaced. Let's say we like that for our example:
function wdiCard(){
var directive = {
restrict: 'E',
replace: true
};
return directive;
}
This is where our partial view comes in. Now, if it's a pretty tiny, self-contained directive, you can use template: <p> "Some javascript " + string + " concatenation"</p>
But that easily starts getting ugly, so it's often better (even for small directives like this) to make a quick little partial HTML file and reference it with templateUrl
instead.
Let's extract our existing card tags, and throw them in a partial. Cut out:
<div class='card'>
<h4 class="card-title">{{card.question}}</h4>
<h6>Cards Against Assembly</h6>
</div>
Quickly touch templates/cardDirective.html
or some similarly-named template, and paste it back in.
<!-- templates/cardDirective.html -->
<div class='card'>
<h4 class="card-title">{{card.question}}</h4>
<h6>Cards Against Assembly</h6>
</div>
In scripts/cardDirective.js
, we can add our option:
function wdiCard(){
var directive = {
//'A' == attribute, 'E' == element, 'C' == class
restrict: 'E',
replace: true,
templateUrl: "templates/cardDirective.html"
};
return directive;
}
And lastly, in our index.html
, let's finally use our custom directive. So exciting. This is it. Here we go.
<!-- index.html -->
<div class='col-sm-6 col-md-6 col-lg-4' ng-repeat="card in cardsController.questionsList" >
<wdi-card></wdi-card>
</div>
TRY IT! So awesome! We've now got this much more readable index.html
, with a very semantic HTML tag describing exactly what we want rendered.
This is awesome. This is a great, reusable component. Except for one thing.
If you notice, our template uses {{card.question}}
inside it. This obviously works perfectly - we're geniuses. But what if we wanted to render a card somewhere outside of that ng-repeat
, where card in cardsCtrl.questionsList
isn't a thing. What if we want to render a one-off card, reusing our awesome new directive elsewhere? Isn't that part of the point?
It sure is. We're lacking a precise scope.
Just like controllers, we want to define what our scope is. We want to be able to say "Render a card, with these details, in whatever context I need to render it in." A card shouldn't rely on a controller's data to know what information to render inside it. The controller should pass that data to our directive, so it's freestanding and not relying on anyone but itself.
That's where directive.scope
comes in, and this lets us decide what attributes our element should have! For our card example, maybe we want to render a card with just a string somewhere outside of this controller. We want to make our own card with our own hardcoded text.
Try this. In your index.html
, adjust our <card>
element to say:
<wdi-card question="{{card.question}}"></wdi-card>
In context, you'll see that the ng-repeat
is giving us the variable card
, and we're actually just rendering that out as a string. But we've decided we want to have an attribute called question
to pass data through. We made that up, it's appropriate to our example, but it can be anything.
There are only two other pieces we need to make this reality.
In our cardDirective.html
partial, let's adjust to:
<div class='card'>
<h4 class="card-title">{{question}}</h4>
<h6>Cards Against Assembly</h6>
</div>
No longer reliant on a variable named card
, it's now just reliant on an element having the attribute of question
.
And finally, in scripts/cardDirective.js
:
angular.module('CardsAgainstAssembly')
.directive('wdiCard', wdiCard);
function wdiCard(){
var directive = {
//'A' == attribute, 'E' == element, 'C' == class
restrict: 'E',
replace: true,
templateUrl: "templates/cardDirective.html",
scope: {
question: '@'
}
};
return directive;
}
In scope
, we just define an object. The key is whatever the attribute on the element is named. So if we want <wdi-card bagel=""></wdi-card>
, then we'd need a key named bagel
in our scope object.
The value is one of 3 options.
scope: {
desiredObject: '=', // Bind the ngModel to the object given (two-way binding)
desiredFunc: '&', // Pass a reference to a method
desiredString: '@' // Store the string associated with fromName attribute
}
The corresponding options would look like:
<div scope-example desired-object="to" desired-func="sendMail(email)" desired-string="[email protected]" />
The =
is a mechanism for binding data that might change; the &
is for passing a reference to a function you might want to call; and the @
is simply storing a string & passing it through to the template.
Our last test is to see if we can make a card using just a hardcoded string. Then we'll know our card is really reusable.
Somewhere outside the context of the controller, let's say just above the footer in our index.html
, throw a handmade card in:
<!-- ... -->
</section>
<hr/>
<wdi-card question="Why is Angular so awesome?"></wdi-card>
<footer>
<!-- ... -->
Would you look at that? Our own custom directive - a reusable, semantic HTML component that we designed ourselves.
This cheat sheet from egghead.io is a great resource for learning more about the specs allowed in the directive definition object.
All content is licensed under a CCBYNCSA 4.0 license. All software code is licensed under GNU GPLv3. For commercial use or alternative licensing, please contact [email protected].