Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Creating a Custom Directive

Patrice Chalin edited this page Jan 29, 2014 · 5 revisions

In previous chapters, you learned how to use the NgController and NgComponent annotations to create custom controllers and components. We will now learn how to add behavior to any element, using directives (NgDirective).

Directives offer a way to define behavior that can be triggered using CSS selectors—most commonly, element attributes. Each element can have multiple directives, just as it can have multiple HTML attributes. If you are familiar with the Decorator design pattern, then you can think of a directive as an HTML element decorator.

This chapter defines a directive called tooltip. HTML code can use the tooltip directive in a span or any other element:

<span tooltip="ctrl.tooltipForRecipe(recipe)">
  ...
</span>

Note: The NgDirective annotation can also be used to create structural directives, such as the built-in ng-if and ng-repeat directives. This is an advanced usage case, and we don’t cover it.

Running the sample app

The code for this chapter is in the Chapter_04 directory of the angular.dart.tutorial download. View it in Dart Editor by using File > Open Existing Folder... to open the Chapter_04 directory.

Now run the app. In Dart Editor's Files view, select Chapter_04/web/index.html, right-click, and choose Run in Dartium.

See how mousing over any item in the recipe list brings up a tooltip for that recipe.

Custom directives

Implementing the tooltip required changing the code in the following ways:

  • Adding two classes: Tooltip implements the directive, and TooltipModel encapsulates its data.
  • Modifying the controller (RecipeBookController) so that it adds an image for each recipe and creates tooltip models.
  • Modifying web/main.dart to register the Tooltip type.
  • Modifying web/index.html to use the tooltip directive.

Implementing a directive

The @NgDirective and @NgOneWay annotations specify the tooltip API:

...
import 'package:angular/angular.dart';

@NgDirective(
    selector: '[tooltip]'
)
class Tooltip {
  dom.Element element;
  
  @NgOneWay('tooltip')
  TooltipModel displayModel;
  ...
  Tooltip(this.element) {
    ...
  }
  ...
}

The selector argument to the NgDirective constructor specifies when a Tooltip object should be instantiated: whenever an element has an attribute named “tooltip”. The @NgOneWay annotation specifies that the value of the tooltip attribute is bound to the displayModel field in Tooltip.

Consider the following HTML:

<span tooltip="ctrl.tooltipForRecipe(recipe)">

Along with the preceding Dart code, this HTML tells AngularDart to create a Tooltip object and sets its displayModel field to the value returned by ctrl.tooltipForRecipe(recipe). The Tooltip constructor is passed a single argument, an Element representing the <span>.

In general, when you want to implement a directive as an attribute that takes a value, do it like this:

@NgDirective(
    selector: '[attributeName]'
)
class MyDirective {
  @NgOneWay('attributeName')
  Model model;
  ...
  MyDirective(/* Optional arguments */) {
    ...
  }
  ...
}

AngularDart’s dependency injection system calls the constructor, supplying context-specific objects for whatever arguments the constructor declares.

Back to the Tooltip implementation, most of its code is devoted to creating a <div> and children to display the tooltip, and then appending the <div> to the DOM. Here’s the code that creates and appends the <div> (tooltipElem) whenever the user mouses over an element that has a tooltip:

import 'dart:html' as dom;
... 
// In an onMouseEnter handler:
tooltipElem = new dom.DivElement();
// ...Create children using info from displayModel…
// ...Add the children to the <div>...
// ...Style the <div>...

dom.document.body.append(tooltipElem);

Important: Because Angular expects to have full control of the DOM, be careful if you dynamically create HTML elements.

Here are the rules for manipulating DOM elements in directives:

  • If changing the DOM structure (adding/removing/moving nodes), do so only outside of the directive’s constructor. (Modifying node properties, on the other hand, is OK inside the constructor or at any other time.)
  • Don’t destroy elements that AngularDart is managing.

In this case, adding the tooltip’s <div> element is OK because the new element (1) is appended to the <body> and (2) is added outside the constructor.

You can see all the code to create, position, and destroy tooltips in lib/tooltip/tooltip_directive.dart.

The implementation of the TooltipModel class is trivial. The class just encapsulates data that the tooltip needs:

class TooltipModel {
  String imgUrl;
  String text;
  int imgWidth;

  TooltipModel(this.imgUrl, this.text, this.imgWidth);
}

Modifying the controller to provide the model

Remember that we want the HTML to be able to use a tooltip directive like this:

<span tooltip="ctrl.tooltipForRecipe(recipe)">

That means the controller needs to implement tooltipForRecipe(Recipe). The tooltip directive expects a TooltipModel argument, so that’s the type that tooltipForRecipe() returns.

class RecipeBookController {
  ... 
  static final tooltip = new Expando<TooltipModel>();
  TooltipModel tooltipForRecipe(Recipe recipe) {
    if (tooltip[recipe] == null) {
      tooltip[recipe] = new TooltipModel(recipe.imgUrl,
          "I don't have a picture of these recipes, "
          "so here's one of my cat instead!",
          80);
    }
    return tooltip[recipe]; // recipe.tooltip
  }

The use of Expando is an implementation detail. An Expando is just a way to associate a property (in this case, a TooltipModel) with an existing object (a recipe). Instead of an Expando, the code could use a mixin, a TooltipModel subclass, or a map.

The other changes to lib/recipe_book.dart include adding an imgUrl field to the Recipe class.

Table: Angular annotations

The Recipe Book app now uses all three of Angular’s annotation classes. The following table summarizes how you typically use these annotations, and whether they create a new scope.

Annotation Usual usage New scope?
@NgController Application-specific logic

Example: recipe-book

A controller should contain only the business logic needed for a single app or view. It should not manipulate the DOM.

Yes
@NgDirective Decorator that adds to existing elements

Examples: tooltip, ng-class

Directives add to existing elements. A single element can have multiple directives.

No
@NgComponent Custom elements

Example: rating

Although custom elements can contain other elements, custom elements (unlike directives) can’t be combined into a single element.

Yes (special). Uses shadow DOM; creates an isolate scope with no automatic access to the parent scope.

Home | Prev | Next