Giter Club home page Giter Club logo

angular-custom-directives's Introduction

Writing Custom Directives

Why is this important?

This workshop is important because:

In Angular, custom directives are the way to write reusable components. "Components" are a modern pattern for creating modular web applications. If we can build a bunch of working components, we can tie them all together to build a full web app. In fact, in Angular 2, one specific category of reusable directive is called a "component." Building a solid directive is the type of contribution you could make to the open source community!

What are the objectives?

After this workshop, developers will be able to:

  • Explain the justifications for using custom directives
  • Describe the directive definition object and implement it in creating a directive.
  • Integrate a third party directive into your code.

Where should we be now?

Before this workshop, developers should already be able to:

  • identify and use Angular's built-in directives.
  • build out a working AngularJS front end.

Custom Directives - Intro

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 see what we can do if we start making some of our own.

Building a custom directive can eliminate repetitive code that renders data over and over again. If you're using the same component all over your client side interface, you still want your code to remain DRY (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:

Real World Example

As an example, we're going to mess around with duplicating something that's become a common pattern in interface design โ€“ the concept of a card. Applications like Twitter, Pinterest, Facebook, and others have moved towards this design pattern.

Twitter

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:

Bootstrap

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:

Cards Against Assembly

Know The Code - Independent

GET THE STARTER CODE HERE!

Take five minutes and inspect our starter code. You'll see a pretty normal Angular app, and since we're repeating using those cards, and there's a few consistent tags we're repeating every time we render a card, we're going to experiment with making those cards a custom-defined directive.

Let's be organized!

Rather than just throw this wherever, let's make a file dedicated just to that function. Clean code, yo.

I called it:

cardDirective.js

Directives are as easy as...

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.

Let's make a function!

angular.module('CardsAgainstAssembly')
  .directive('wdiCard', wdiCard);

Because we defined the directive without defining the second argument, 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. When making a directive, you use a Directive Definition Object to specify the capabilities of your directive. The Directive Definition Object has specific keys that it expects in order to define attributes and behavior of your directive.

Directive Options

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.

  1. restrict
  2. template/templateUrl
  3. replace
  4. scope

1. restrict

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;
}

2. template/templateUrl

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 obvious-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',
    templateUrl:  'templates/cardDirective.html'
  };

  return directive;
}

3. replace

Replace is pretty straightforward. Should this directive replace the HTML that calls the directive? 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 cardsCtlr.questionList" >
  <wdi-card></wdi-card>
</div>

Would actually render as:

<div ng-repeat="card in cardsCtlr.questionList" >
  <div class='card'>
    <h4 class="card-title">{{question}}</h4>
    <h6>Cards Against Assembly</h6>
  </div>
</div>

See, <wdi-card></wdi-card> is gone, it's been replaced with the longer-form template that we defined above. Without replace, it would render as:

<div ng-repeat="card in cardsCtlr.questionList" >
  <wdi-card>
    <div class='card'>
      <h4 class="card-title">{{question}}</h4>
      <h6>Cards Against Assembly</h6>
    </div>
  </wdi-card>
</div>

Let's say we like the replace option for our example. We simply add replace: true to our directive definition object:

function wdiCard(){
  var directive = {
    restrict: 'E',
    replace: true,
    templateUrl:  'templates/cardDirective.html'
  };
  return directive;
}

Get it connected

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 cardsCtlr.questionList" >
  <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.

Cards Against Assembly

This is awesome. This is a great, reusable component. Except for one thing.

4. scope

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 cardsCtlr.questionList 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 example, in 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 <wdi-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 want the attribute on the element to be named. So if we want <wdi-card bagel=""></wdi-card>, then we'd need a key named bagel in our scope object.

The Different Types of Scope for a Directive

The value is one of 3 options.

scope: {
  desiredObject: '=',     // Bind the ngModel to the object given
  desiredFunc: '&',      // Pass a reference to a method
  desiredString: '@'     // Store the string associated by fromName
}

The corresponding options would look like:

<div scope-example desired-object="to" desired-func="sendMail(email)" desired-string="How is your day today?" />

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.

Since we've decided to use @/strings, let's try it!

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

Custom Card

Would you look at that? Our own custom directive - a reusable, semantic HTML component that we designed ourselves.

A deeper dive on the directive definition object

Check out thisdirective definition object cheat sheet from egghead.io.. Specifically look at the controller and link options that can add functionality to a directive.

We can use directives as simple templating as we did above with the card directive, but we can also make directives that have their own behaviors!

Our code can be separated into small, organized pieces that have a single representation in the code as a directive.

Integrate a third party directive

UI Bootstrap provides a wide array of useful directives that can bring cool functionality to your applications! Let's integrate the ui bootstrap rating widget into our Cards Against Assembly app.

Resolving dependencies

UI Bootstrap has a handful of dependencies that we need to integrate before this directive will work.

Use bower to install bootstrap, angular-animate, angular-sanitize, and of course, UI bootstrap itself, which is called angular-bootstrap when you download it.

In your index.html, include all of these dependencies:

<script src="bower_components/angular/angular.min.js"></script>
<script src="bower_components/angular-animate/angular-animate.min.js"></script>
<script src="bower_components/angular-sanitize/angular-sanitize.min.js"></script>
<script src="bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js"></script>

<script src="scripts/app.js"></script>
<script src="scripts/controllers/cardsController.js"></script>

<link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">

Include these dependencies in app.js as well

angular.module('CardsAgainstAssembly', ['ngAnimate', 'ngSanitize','ui.bootstrap']);

Building a controller

Using the rating widget demo's js file as your starting point, build a rateController with the necessary attributes. Remember that the example uses $scope and we'll use the vm syntax:

angular.module('CardsAgainstAssembly')
  .controller('rateController', rateController);

function rateController(){
  var vm = this;
  vm.rate = 7;
  vm.max = 10;

  // etc... keep filling in the controller so that it has all of the
  // attributes that the demo has
}

Make sure to list rateController as a <script> in index.html!

Including the HTML

In a section below the cards, use the rating directive to add a feedback mechanism for users. Note again, the demo provides a good start, but we need to adjust the syntax for our case, adding rateCtrl where appropriate:

<section ng-controller="rateController as rateCtrl">
  <h2>Rate our app!</h2>
  <span uib-rating ng-model="rateCtrl.rate" max="rateCtrl.max" read-only="rateCtrl.isReadonly" on-hover="rateCtrl.hoveringOver(value)" on-leave="rateCtrl.overStar = null" titles="['one','two','three']" aria-labelledby="default-rating"></span>
</section>

Use the demo as inspiration to add additional features as desired!

Resources

Directive definition object cheat sheet from egghead.io - a great resource for learning more about the specs allowed in the directive definition object.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.