Here is another talk from the St. Louis Angular Lunch we sponsor / operate here at the Oasis Digital. In this talk, Mark Volkmann of OCI explains how to use AngularJS to extend HTML to include custom, modular components.
The ability to define custom directives is one of the most powerful features of AngularJS, and as always Mark does a great job summarizing it in a short lunch talk.
Enjoy the video, embedded below.
We have transcribed the talk about to text, provided below. This is a rough, first-draft transcription, so any errors are probably from that process.
I'm Mark Volkmann from OCI. I do a lot of Angular consulting and now teaching classes in Angular.
So, as I was starting to say, the kind of directives I'm going to talk about are not the ones that Angular provides, but the ones that you might want to create yourself to do things that the provided ones don't give you.
So, I kinda break directives up into these categories:
You might be wanting to create a directive for a custom widget, and we'll be looking at a playing card example here, custom dialog, and date picker. Then there are directives that just give you event handling. Like, you want to make sure that when the user is typing in a value in an input, only the digit keys get recognized, and every other key is suppressed. Things like that.
And then there's functionality that you might want to implement. It's not a widget, it's not event handling, it's something else like ng-include that's pulling in a chunk of HTML, or ng-show, that's conditionally deciding whether you want to display something. Those are provided directives, and you can write ones that do similar things to that.
And then a final category might be taking a large page and splitting it up into pieces, and there's several ways you can do that. You could just use ng-include, but you also could create a custom directive that is managing each part of that page.
When you're picking a name for your directives, you want to make sure that you don't conflict with the names of other directives. So you want to stay away from things that are built into HTML, reusing those, and you want to avoid conflicting with the ones that Angular provides, its directives, and maybe there's a third party library that is getting you some directives. You don't want to conflict with those.
And a weird thing happens if you have more than one directive with the same name. With other components in Angular, when you have a name conflict, it's like, "Last one in wins." Seems reasonable. But with directives, it will process all of them, so it's possible that you've added a directive to some element, but there's more than one definition for that name, and they all have it. So you certainly don't want that to happen, I don't believe.
So, you probably want to put a prefix on your directive names. And it could be an abbreviation for the name of your company, the name of your application, the name of a library you're working with or that you're creating, but something to avoid those those name conflicts.
One thing that people find really confusing about directives, and there was a request at the mailing list recently to see if we could change this, is this idea that when you write your directive, you give it a name that is camelCase, and then when you use it in HTML, it has to be a snake_case name. Nobody from the Angular team replied to that request, so I assume they have no intention of changing this rule.
It's hard to talk about directives for very long without bringing up this term isolate scope. So I'll talk about it a little here and get into more detail later. Isolate scope is, as the name implies, isolated from your other scopes. And there's a good reason you might want to do that. It makes your directives a little easier to reuse in different settings, and it means that the way you've implemented your directive is not assuming that there's going to be certain things in an ancestor scope that you're going to make use of.
Other kind of conflicts you might run into… We'll be talking in a bit about how you set up this isolate scope and the directive. But you might write a directive that uses isolate scope, and then another directive that uses isolate scope, and use them both on the same element. Now what happens is that that element can only have one scope. So even though you've asked for these isolated things, they won’t be. It will create one isolate scope and another directive will be sharing that one.
Additional conflicts — we'll be seeing, coming up, that the way you define a directive is that you're often going to tell it what HTML it's going to emit. And you do that with the templateURL properties. So you're not allowed to have two directives on the same element. They both specify HTML, only one of them gets to supply the HTML that's going to be rendered.
And then later I'll talk about this concept of transclusion. It's not nearly as complicated as it sounds. It just means that I'm going to apply directly to some element. That element has content inside it. What do I want to do with that content? Maybe the directive wants to grab that and stick it somewhere in the middle of the HTML that it's going to add. So that's not too complicated. But as you can imagine, it's not fair for two directives on the same element to both say that they want to do this transclusion. They can't both grab the content and include it.
So, let's walk though these properties. And I won't go into detail on all of them, and I've tried to list them in the order which I think they're most often used. Main ones first.
So the first thing we hit is, how do you want to allow this directive to be used. You have four choices, only two of which make any sense. So you could say: "This directive can be used as an element", "It can be used as an attribute", "It could be a CSS class name", or "It can be an HTML comment" I've never seen anybody use the last two.
Participant: I was going to ask why you'd want it to be a comment.
Participant: I was going to ask why anyone want it to be a comment.
Participant: I literally just looked this up. It's apparently a backwards compatibility. Like an early, early version had that for some specific need, and it just stayed around for a long time, and now it's probably going to be …
Mark: So just the comment, or leaving in the CSS class?
Participant: That's very useful.
Participant: Yeah, I can actually think of uses for that.
Participant: When have you used it?
Participant: You have a bunch of HTML, and you've already noted something important with some CSS class. Maybe some CSS class that you have to use because some CSS framework or CSS library needs that CSS class, trigger a certain thing to appear. Rather than add a duplicate by adding another different name to every piece. You could say, “Well, I've already written a bunch of HTML that tags a certain thing in a CSS class. Now I want to throw some behavior in based on that.” That’s a way of doing it.
Mark: Yeah. I can see how that saves a little bit of typing, but at the same time it seems that that would be surprising to people…
Participant: Angular is full of surprises.
Participant: That's probably middle of the surprises.
Participant: Right. Then you can hang on to preexisting elements and you can take elements that are already in HTML and add more behavior. So if you're looking to avoid surprise, you're probably the wrong framework.
Participant: A similar case that I thought of, that I haven't actually done this, and I'm not sure how it would work in practice, but when I first came across this one, I said, “Okay, I'm going to dream up a need for this. Let's imagine that you've got some CMS that you're working in, that you can actually define classes or select classes for certain elements, but that's like the extent of your customizability in terms of, like, workflow. But if, behind the scenes, you can load in Angular, and that's your hook for customizing elements yourself.
Participant: … not Angular, but other languages that have worked real well.
Participant: So, again, it's one tick mark above hack. But if that's the only avenue you've got, I could see that being useful. But yes, in practice, element and attribute normally the only ones that people use.
Mark: Okay. So after you've decided how it's going to be used (and by the way, it defaults to only being able to use it as an attribute), then you have to specify, if you want, the HTML that's going to be inserted. So earlier I talked about directives that maybe only do some event handling. So in that case there may not be any HTML. But if there is some, I can use template or templateURL, and I would choose based on how much HTML there is. If it's just a little snippet of HTML then I would use template. With templateURL I can place a bunch of it in a separate file, and that seems to be what I use most often.
The next is replace. So the directive is on some element in my HTML. The directive itself has some HTML potentially. Should that HTML replace the element on which the directive was placed, or should it just supply content for that element and leave the element there? So replace, it's going away. Not replace, it's going to be inserted into the content of that element where it appears.
Scope. We will talk about this in a few more slides, but for now I'll say, "The directive gets to specify whether a new scope needs to be created. Is that scope one that inherits from an ancestor scope? Or is it an isolated kind of a scope?" So those are kinda the choices you have, and we'll see some detail on that coming up.
So, inside my link function, since I was given access to the scope, I can now add things to it, just like you would in a normal Angular controller. And so that could be data, or a function I want to put on the scope. I can perform DOM manipulations here because I got access to the element.
I can get to data that was available on the element though the attributes that were specified. And we'll see, coming up, that another way I could get values of attributes is to use an isolate scope and specify names of attributes there. And then, by the time I get to the link function, those values are already on the scope. But if I didn't do that, I still have access to all the attributes. So, this link function gets called once for each instance of this directive, each place where I'm using it, and so they all can have different data available to them.
I mentioned transclude earlier. Some directives go on elements that might have content, and then I want to retain that content and use it inside like the templateURL, and insert it into specific places. And there are lots of examples of provided directives that do that: ng-if, switch and repeat. So you know when you do a repeat. There's some that inside that element, and the repeat is repeating that content over and over again. So you might want directives that do something like that too.
That's the end of what I consider the most commonly used properties for defining a directive. Bill will talk about controller next time. Priority seems to rarely come up, but we've talked about how an element can have more than one directive on it, and maybe you care about the order in which they get evaluated, you can control that with the priority of the directive.
Participant: Hey Mark, we've been using controller somewhat often, because we find we can often write code as more — kinda at an Angular mastery level instead of the component-builder level. It seems there are simpler, easier, more high-level ways to do common things with controller.
Participant: So rather than write a bunch of stuff to use JQuery-like code to hook up event handlers, in the link function, you end up being able to use ordinary Angular … in the controller.
Mark: But why can't you do the same things in a link function?
Participant: You can, but the link function is deeper in the toolbox. So the controller is kinda shallow in the toolbox, and like, high-level capability. And the link function is deep in the toolbox.
Participant: Yeah. I've actually found — I was surprised a little bit when you said you used link and don't use controller and that you’re deferring controller to me. Because I ended up doing the same thing, and part of the reason for that is because, if you're doing Angular, you're doing controllers. And the thinking, when you get over to directives, because directives are kinda an advanced topic anyway and people are a little scared of them. But the controller is something that you're going to be intimately familiar with if you’ve done any Angular code at all, so it's a fairly easy transition back and forth.
Participant: But like you said, you can do all this same stuff in the link function, but I found controller to be a little more natural. But that's — I don't think what you're saying is wrong, I just find it interesting that the different emphasis that people do.
Mark: I suppose it's because it's easy to run into a case where you have to have a link function, and once you're there, if you can do everything there and you're not needing also the controller…
Participant: Yeah, I can see that.
Mark: …Lean toward that. But it'll be interesting to see your talk and see some examples of…
Participant: But I do think it's funny that all my early directives have a link function. Most of my late directives have a controller. So, without realizing it, I kinda shifted to using it controller style.
Mark: So you have a controller with no link function?
Mark: And are you able to get access to the element …?
Participant: Unless I’m just wildly misremembering, yeah. Because I know I'm doing some stuff like that.
Mark: Okay. Maybe I'll see examples like that next one.
Require and compile — I'll just skip over this. Some examples here. So, if I want to use a directive as an element, and suppose the name of the directive was someLongElement in camelCase, then in my HTML it's going to look like this, and I have to have a closing tag for that. There's no way out of that . And if I use it as an attribute, it looks like this. And I think some people prefer attributes just because it's less typing. They can have this three-letter div tag and say that one time instead of repeating it as a closing tag.
So here's a basic example that shows a directive used in all four forms. So I'm going to write two directives here. The first one I can use it as either an attribute, a class name, or an element. And then this last one, I can use it as a comment, right there.
So, here are all the uses of it, and what I expect my output to be is just this static content, four times. This first one, I'm basically using that as an attribute. And notice how I took this camelcase name and then I changed it to snake case. And all this directive is doing — notice — I'm returning an object, which is my directive restrictor. So it's restricted to using in those three ways, and the template is just this static content.
And we're going to use it here, it just spits it out. I didn't say to do a replace, and so that means that if you actually looked at the HTML that's output, there is a div tag wrapped around that. And then I'm doing the same thing as a class name, and then as an element name. This won't work in IE8 unless I take that other step and then bind an element with that name.
And then the last one, I'm going to use it as a comment. I have to use replace in that case. Because there was no element available. It's going to have to create one. And then in my template, I have to wrap an element around it. It can't just be that kind of a string.
Participant: One more use case for comment before I forget. There are certain elements that the browser won't actually load DOM for incorrect sub-element types. I think table may be one of those examples, to where if you have a table element, the only thing that the browser will load in the DOM after that are table rows and table data. It comes across an element that isn't recognized, it won't actually load it to DOM, so Angular doesn't actually get the chance to operate on that directive.
Participant: However, if you put a comment in there, then you can actually inject a custom directive inside something like the table element. So that's another use case for a comment.
Mark: Okay. That's interesting.
Participant: You're trying to inject it in a CMS or some system you don't have much control over. You put comments in, to lightweight, get into it.
Participant: … use an attribute in that case?
Participant: Because you may not want a directive to be operating on the whole table, or even a table row or table data. You may want some custom table content that needs to be wrapped in a directive.
Mark: So I mentioned earlier breaking up a large page into pieces, and the two main ways I've seen that done are using ng-include, or using a custom directive. So if I want to break it up with ng-include, then I can have a bunch of div tags like this, that each specify a different controller that's going to manage that part of the page, and then I can reference some HTML file.
Notice the single quotes inside here. That's because this is waiting for an Angular expression, and it will look for that as a property on the scope if you don't put single quotes around it.
Participant: That drove me crazy.
Mark: Yes. So, it's debatable whether you want to specify the controller here. An alternative would be to specify the controller on the root element in this file. And a thing to think about is, are you going to reuse that template in other settings, possibly with different controllers? If you want to do that, then you don't want to specify it in the HTML file. You want to do it there.
So that's one way to do it. And then another way is to create a custom directive for each section of the page, and I'll show you an example of that right here. And by the way, the stars here don't mean anything for you. What it means is that I printed off these slides already for a class that I'm teaching, and in their version, this is different, and I'm letting them know that I've made a change. But you're seeing the latest version.
So in some HTML file, I'll use a bunch of directives like this, so this used to be a really large page that has three main sections to it. And then for each section, I'll create a directive. So here's my part one directive, and I'll specify that I want to use a certain controller for that section of the page, and here's its HTML. And then I'm done. So I don't need a link function or a controller, I'm just trying to associate a controller that I already have with some snippet of HTML, just to break a big page up into smaller pieces.
So here I have the controller that I'm referencing right there, and I can design the same sort of things for the other parts. A directive for parts 2 and 3, a controller for parts 2 and 3. So I find this makes the app a little easier to manage. That will break up big pages in one of those two ways.
Now I'm going to look at how you pass data into a directive. There's several ways of doing this, and so we'll look at four different cases here. I'm going to say hello to all the stooges. Got a little bit of CSS that isn't really important. And then right here, four divs that correspond to these, they're all using different directives, hello world 1 though 4. And then I'm going to supply a first name and a last name, and somehow that data has to make it into the directive.
So in my first shot at this, I've got the hello world 1 directive, it has a template, and I'm going to output the first and last name just using a binding expression inside that template. I'm not using an isolated scope here. Instead, I'm going to rely on the fact that I get this adders array, so I have access to all the attributes. And so I can go into that and grab the first and last name. They were attributes on the element where I applied this directly, and I'm just going to add these as scope properties, and that's what allows me to get to them in this binding expression. So that's a simple way of doing it.
Another way would be to use an isolated scope. And so I'm specifying here that I want an isolated scope. Just the fact that I've assigned a literal object to this is what tells it to be an isolated scope. And then this @ sign, we'll see more on this coming up, but that basically means that I want to pass by value. I want the string that was the attribute value. So it's going to look for an attribute that has this same name and put that on the scope for me. So now I have the same template as up here, but I don't need a link function, because that already populated the scope.
One of the things that is missing here is that you might want to say that first and last are required attributes, and if they're not present, there should be an error. And so that's what I want to do in the next example.
The last thing I want to show is that if I want to replace the element on where that directive appears, then I just say to replace true, still using the isolated scope, but because I'm replacing it, my template has to result in an element. It can't just be a literal string, and so I've added a paragraph tag, and so that's going to replace the div with that paragraph tag.
So, I've been mentioning these different options for the scope on your directive, and you have basically three choices. You can say, "Scope false," which means, "Don't create a new scope for me, just let me use the one that was there where the directive appeared." So whatever that containing scope was, I'll just use the same one. One thing this means is that if I add things to the scope, say, in my link function, then the things outside that are going to see it, because I'm sharing that scope.
Another thing I could say is to use an inherited scope, so I'm going to create a brand new one, and the one that's created inherits from the one that existed where I used the directive.
A big thing to think about here is what happens if I add things to the scope. In both cases, I'll be able to use that within my directive, but what about things that are outside my directive? Will they see what I've done? In this case they will see it, in this case they will not.
And this kinda gets into this issue that seems to sneak up on new people to Angular. That they've added something to the scope, and they're wondering why they can't see it. And they're not catching the sort of prototypal inheritance relationship between the scopes. So, if I have a property on a scope and the name of the property is foo, and then I go into a descendent scope, and I try to change the value of foo, it's not working.
What's happening is I'm adding a brand new foo property to this descendent scope that is hiding the upper one. But once I get out of that, that upper one doesn't know that I've done that. And so that's why it's recommended that in many cases you should be adding properties to an object that is on the scope.
So here I’ve got my scope reference and there’s a parent property that is an object and I’m adding properties to that. And that makes it easier to share things across the scope, should you need to do that.
Participant: Couple of things. That last thing you said is a hugely important point. When I first started using Angular, I made the mistake of thinking that the scope was the model. The scope is not the model. The scope is where you put your model. And the models are those objects that we’re talking about. Once I figured that out, a lot of errors just went away.
So, three different values you can assign. You've got false, which is the default, true, and then give it a literal object, which means I want an isolated scope. So if I do that, and say that I want an isolated scope, then I can say how values are going to come into it. And I have these three choices: @ sign means just give me the value as a string. Equal means I want this thing by reference, and that means that if I'm making changes to, say, an array or an object, it's the same one.
And then, so whoever provided that to me, I'm changing their array or object. And then finally the one that's used the least often, but sometimes useful, is this ampersand, which means there's an expression specified in the attribute, and I want it to be evaluated out in that containing scope. Don't evaluate it as part of the directive's scope.
A weird thing about this isolate scope is that it's not as isolated as you might guess. You still can reach out if you choose to. You probably should not do this, but from your isolate scope, if you reference dollar parent, you're getting out to some other scope. So what's going on here is you can directly get to these things, even to the root scope, but when you're looking up something in your scope, it won't automatically walk through that and do the prototypal inheritance thing for you.
So I mentioned these three different characters. So there's "Give it to me as a string", "Give me the same object by reference and evaluate it in the parent scope." I can't remember why I was repeating all of that here. Oh, the last thing to point out here is that I think usually you're going to be okay with saying the name of this scope property, it's okay if it's the same name as the attribute.
But if you want them to have different names, then following these characters, like in this example they equal, you can give it a different name so that your scope property or the directive will have a different…
Participant: The reason I've sometimes done that is, for one, I always prefix my stuff so I don't end up with collisions. But inside of my directive, where I'm just dealing with the attributes as properties on an object, I don't have to worry about name collisions, so I can use unprefixed names. And I tend to use really nice, verbose attribute names, but I probably don't want those those inside of my code, so a little lookup table makes it very easy to use short and long names.
Mark: Yeah. And then the other thing to remember is that the name that goes on this side…. Boy, I messed this up. Do you remember — so the name that goes here, this is the actual attribute name, but it has to be camel case here?
Mark: And the name that goes here is also camel case?
Participant: I believe so, yeah.
I'm hesitant, because I know I went though like every possible combination to make sure that I got that right on a couple of directives, because I kinda stumbled my way into it initially because I was using really simple names. As they got more complicated, I was doing it wrong for the first couple of….
Participant: There's actually code that you can get to in Angular or one of the Angular libraries, because I ended up need to do some translation of names back and forth, and you can get access to the very routines that they used to turn camel case names and snake case names in the other direction as well.
Mark: Cool. Alright, so I have a bunch of demos and I'm down to 20 minutes, so let's see how many of these I can get though. So the first one is this playing card example, and let me demonstrate this to you. And so, from here, I can say, "Deal me another hand, and it gives me five more cards." And I have a deck, and so it's pulling them from the deck, and at some point, we get to the end, and it'll be just two cards left. I see the final two cards, and then I say, "Okay, give me a brand-new deck."
So, what is a directive here, is each of those cards, so let's walk through that code.
So I've got my main controller, card game controller. And then right here is where I'm displaying the five cards. It's all on this one line. So I'm going to be moving through all the card objects that are in hand, so that's going to be an array that's on the scope. And each one of them has the playing card directive. And I'm giving it the the card that I'm iterating through one at a time.
Then at the bottom I have these two buttons to deal me a new hand and give me a brand-new deck. And I only show this button if I've got five cards in my hand. When I get down to the case that there was only the two left, that's not going to be seen and you have to ask for a new deck at that point.
By the way, my card objects have two properties: A rank, and a suit. So, nothing super interesting in the CSS files. Rounded corners to make them look like cards.
So my card game module depends on game directives, and so here we're just looking at playing the game. And then the next piece, this one, we'll look at how I actually create the directive. So, I'm going to inject in this playing card service, and that has this method deal hand, so I can deal out five cards from my deck, and automatically it will have already created the first deck, and it's ready to go. We'll see that code coming up.
So this is the function I'm going to call if you click on the new deck button. I added a playing card service for getting me another deck, and then deal out the first hand. And then when it first comes up, I want to deal the first hand.
There's a lot of code in here that really isn't related to Angular, and I'll try to skip over a lot of that, but now we're in the module that contains my service and my playing card directive. So it's game directives, it depends on ng-sanitize so that I can avoid escaping those ampersands.
And we have the well-known Fisher-Yates shuffling algorithm to shuffle a deck of cards, I'll skip past that part. But then here's my deck of cards. It starts out empty, and I know what all the ranks are going to be and what all the suits are going to be. So, to create a new deck, I just need to iterate through all the suits, iterate through all the ranks, and then create an object that has a rank and a suit, and so this deck isn't shuffled at all. It's in a very fixed order, but then I call shuffle deck in my previous slide, and now it's randomized for me.
So we're looking at the playing card service here, so dealing a card is just popping one off the top of the array, and dealing a whole hand means I have to iterate through, and I've passed in how many I want like, I want five cards. And it's going to start with an empty array, push some cards on, and finally return it. And I could have written this in different ways, but the way it's working now is that I'm always going to loop though, say, five times, but there might not be five cards left. And so when I ask for a card, I might not get a value back. And so I'm saying, "If I got one, then push it on, otherwise don't do that.”
Then, we already walked through that. It starts out by creating a new deck immediately, so it's ready for you to call deal again. This is the way that I've landed on writing pretty much all of my services. I've seen a lot of different styles, but it seems that most services I write have more than one method. And so I start by creating a service object that's empty, and I end by returning it, and in the middle I assign a bunch of things to it, whether it's data or functions.
So here's my directive. So the template is that I want to have a div that has this class playing-card that gives it the rounded corners, and then some CSS styling here, I'm going to set the color, and it's going to end up being red or black. And then the content that goes on the card, I'm specifying not as a binding expression here, but using ng-bind-html as part of the ng-sanitize thing to avoid that escaping.
I'm going to replace the element where this appears, and I'm going to take in as an attribute from that element, playing-card, and I need a reference to it. Because that's an object that has a rank and a suit. So I don't want this just as a string. Then in my link function that will be populated now, so I can get the suit. And it turns out, I don’t know why they did this, but in unicode, the way that you get hearts and diamonds…. let's see….. the way that you get everything except diamonds is just the name.
So there's clubs and hearts and spades, but they decided to abbreviate diamonds, so I have to check for that case and say, "We'll really use that instead." So then, here's my unicode character, ampersand and a suit and a semicolon. So I have the rank first, and then a break, so I want to put the suit down on the next line, so I set content, and that goes in right here. And I see the card.
I skipped over setting the color. That's pretty straightforward. If it's hearts or diamonds, it's red. Otherwise it's black. And the color, scope.color, it's referenced right here. Any questions about that?
Participant: This is a great game, by the way. I don't know if you came up with this game…
Mark: I did.
Participant: It's awesome.
Mark: Let's see. Running short on time, so I've got some event handling directives, and I'll make these slides available and you can look this over later. But one of them is this digits-only and I can apply that to an input and make it so that as I'm typing in here, all I can type is numbers. Well, not exactly. I also want to allow navigation keys so you can use the arrows to move left and right, and you can use tab and delete.
So you could look at this and see how I'm filtering that out, but basically, my directive is just setting up an event handler, and then I get a key code that I don't like, I just have to call event dot prevent default, unless, of course, I'm in IE which doesn't have such a thing. Going to have to return true or false. And so this directive handles that.
The other one I have is this button, where I say if I hover over it, I want to change its background color. You see that right here. Hover over it, and change the color to pink. So it's just listening for mouse events to see if I'm over it.
So I'm going to skip over that implementation because it's pretty straight forward. This is a good one too. Let me at least demonstrate this one. My color picker widget….
So in some of these examples I run into problems where Chrome doesn't like that I'm referencing some things out on the web and some things locally, and so I usually get around this by running a little Node.js server using express, and that's what I'm doing here.
By the way, all of this code is available from my GitHub account. It's GitHub.com/MVolkmann, you'll se a repo there called AngularJS examples.
Okay. Server's running, listening on port 3000. Okay. Here it is, and so the color picker is these three sliders and then a sample. And then the text here you see matches that. And so I can grab the slider bars, and as I do that, the color of the slots changes, and the color of the text changes along with that. And if I want to reset this at some point, because yellow is my favorite color, I have a button for that. And then the sliders adjust to that, and then the sample changes.
Boy, I wish I had time to walk though the code for that. But now that you've seen it, if you want to do something similar, hopefully this will be meaningful. I really want to walk through the next one, which is this dialog here. And the reason is that, I think a lot of use of custom directives is that you want to wrap some kind of widget that another library is providing to you. And in this case, this is a modal from Twitter Bootstrap, but I want to turn it into an Angular directive.
So I want to demonstrate that one. Okay. So I click on that button, and there's the dialog. And I can pick blue, it changes my background color. Green… Okay, so that one, we really should take a look at the code.
So let's start with the directive, and then I'll show you how I use it. So the name of the directive is "rmvDialog", starting with my initials. And I'm saying you can use it as an attribute or an element. It uses this HTML. It's going to replace the element on which it appears, and then finally here's the transclude, first time we're seeing that. So the element where you apply this could have content inside it. I want that to appear in the middle of my dialog.
So there are two things you can specify as attributes that will come into this isolate scope. So button map was that object where the keys are in the button strings, and the values are the functions to invoke when you click it. And then the title, that's just a string.
This is the mess that is Twitter Bootstrap. I love Twitter Bootstrap, but it leads you to having a lot of HTML to get what you want. And so, if you want a modal in Twitter Bootstrap, you have to have this class "Modal". Then you have to have a modal dialog inside. Modal-content, modal-header, come on, this is a lot of fun to type. So this is a good reason why you want to create a directive so you're not doing this all over your application.
So, in this modal-header part, here's where I'm referencing the title that will be in my isolate scope. ng-transclude is a provided directive that indicates where the content should go. So all I had to say is, "Yes, I want to use transclusion, and then over here, this is where you should put the content." Then in the footer, this is where I want to have the buttons, and this ng-show is saying, “Well, you might not have given me any buttons.”
Maybe you just want to have some content to display. And so I won't have the footer part if you didn't give me any buttons, but if you did, then I'm going to use ng-repeat to loop through everything that was in that object you passed me. There's the button map, and I'm looping through all the key-value pairs, and the text that appears on the button is that text from the map, and if you click that button, call that function. That was the value of each of those. So that's very simple to set that up.
So with that in hand I want to make use of this, I can do this. I want to have an rmvDialog, and I've got an ID for it, and that's so that, over here, I can use this other thing from Twitter Bootstrap to say that if I click the button, I want to display that thing. So I'm tying it to that ID.
Giving it a title, that was the thing that was coming into my isolate scope, and then the button map. Well, we have to see where that gets defined still. That's going to be…
Oh, yeah. Right there. That's where it's going to get defined.
So just to tell you what else is in these slides, if you decide to flip through these, the last example I have is about recursive directives, and I bet you will have one like this next month. But the one that I'm showing uses $compile, kind of an advanced thing, where I can pass it a string of HTML, and it will compile that into DOM for me, and let me add that to my document.
So here I talk about the steps that you go though to use this, and it's almost a bunch of jibberish. I tried to make it as clear as possible, and then I read through it again today, and I'm still not quite clear on it. If I really think about it, it makes sense. But really the best way to understand this is to look at this example.
But if it's not that, it’s a primitive. And then I'm going to check to see if it was a string, then I'd like to surround its value by double quotes. But if it's not a string, I'm not going to surround it by anything. And then I can output just a simple span, and I'll use the quote character which may be nothing, and then the actual value and then another quote character, and I end this span. So you can see, if you use this directive, and you just give me a primitive value, you get something really simple out of it.
On the other hand, if you gave me an object, then I want to display this as an unordered list with bullets. And so I want to output a ul, and then a bunch of list items. So let's say it's an object that has three properties. So I'm looping through those items, and I get a key-value for each property in that object, and then I want to output the key, and now what am I going to do about the value?
The value could be primitive, or it could be yet another object. So I need to do a recursive thing here. I need another element that does an inspect on that value, so you can see how this thing could keep calling itself recursively. Well, either path I take, I end up with a string template that I want to turn into DOM and insert it into the document.
So that's what $compile will do for me. I give it the template, and give it — see, this is returning a function to me, and then you pass to that function your scope, and a function where it will give you a cloned element.
So, it's called cloned element because in some uses, it's making a copy of something that already existed. That's not what's happening here. We manufactured new elements, but I was just sticking with the name that you'll see in the documentation when they describe this.
But at this point, I've turned the string of HTML into a little DOM tree, and I want to append that to the element where you first used the inspect directive. Like many examples on the slide, it looks really simple, but I don't even want to tell you how many hours it took me to figure out how to get those to work. So that's a more advanced thing. But I think the main point to get out of this is that custom directives, not doing things like this, are really not that complicated. It can be as simple as just having a template. You say, "How can it be used? Should it replace the element on which it appears?" You could have a link function which is very basic, just does a few little things with the scope, maybe does some DOM manipulation on the element. If you can really keep these simple, and I would say that probably 90% of the directives that I've created are nothing like this one. They're like the simpler ones that we looked at earlier.
So that's it. I'll make the slides available. Do you have any questions?
Participant: You ending with that one just kinda silenced the crowd.
Participant: On that compile line, there’s probably some syntax in there that I'm not aware of. You have what looks like two parameter sets there.
Mark: I'm calling $compile, and it's return value is a function, and then I'm calling that function with those parameters.
You could break that up into multiple lines. I guess I saw an example that was like that. But that is the way they do it in a lot of the documentation.
I could say var fn = $compile template, and then down here I could say fn that.
Participant: Well, I agree, but he's right.
Participant: It’s just kinda weird that this is what it does.
Mark: Alright. Thanks for coming. See you next month.