Implementing a Highlighting Directive for Angular

I have been now working with Angular for a bit of time.  I think I am getting a better grasp on the framework.  In a feature I was working on recently, I needed to implement highlight of a search term in search results, similar to search engines.  The actual search was done in a service component, and the results were shown through repeat directive in a view.  Controller was adding a few fields to the scope, exposing search results, search term and a signal that search was completed.  If you are using Angular and following the basic principals, you should not put UI functionality into your controller, using directives for DOM manipulation instead.  So, let’s break down this feature.  BTW, I am using TypeScript with Angular in this project.

First of all, I need a JavaScript to implement highlighting.  You can find the one you like.  The one I use just have a single highlight method and it is implemented as a jQuery plugin.  First of all, I need to add TypeScript type definition file to match this plugin.  It is a brand new.d.ts file with a single interface in it.  This plays nicely with my other type definitions, including the one from jQuery itself.

interface JQuery {
    highlight: (pattern: string) => JQuery;
}

declare var $: JQuery;

This is it.  Now I need to create a scope definition that my directive will consume and my controller will populate.

interface IHighlightScope extends ng.IScope {
    lastSearchTerm: string;
    isSearchTermChanged: bool;
}

I only need two members in it.  One for the search term I am going to use for highlighting, the other I am just going to observe for changes and trigger a highlight when its Boolean value changes from true to false.  I am setting it to false in controller after the search is finished by the service.  In my controller’s constructor I am setting initial values on the scope to blank string and false.

        constructor($scope: IHighlightScope) {
            $scope.isSearchTermChanged = false;
            $scope.lastSearchTerm = '';

Now, when search completes, controller will just set isSearchTermChanged to false. 

Now the actual directive.

/// <reference path="../interfaces/interfaces.ts" />
/// <reference path="../../Scripts/typings/angularjs/angular.d.ts" />
/// <reference path="../../Scripts/typings/jquery/hihghtlight.d.ts" />
module directives {

    angular.module('app.directives', [])
        .directive('highlight', function () {
            return {
                restrict: 'A',
                link: function (scope: IHighlightScope, elem: JQuery, attrs) {
                    scope.$watch('isSearchTermChanged', function (newValue, oldValue, scope : IHighlightScope) {
                        if (oldValue && !newValue) {
                            setTimeout(function () {
                                elem.highlight(scope.lastSearchTerm);
                            }, 300);
                        }
                    });
                }
            }
        });
}

 

What you see above is the brand new module that I am adding the directive to.   I am strongly typing the scope as the interface I defined previously.  I am watching the signal variable, and when it changes from true to false, I am firing the highlighter through timeout.  I need timeout because I need to allow Angular template engine to actually build HTML with the search results first.  Finally, I need to add this directive to a div element with my results.  I am using data- attributes to avoid HTML validation errors.

<div id="searchResultsDiv" data-highlight="">

Moral of the story: 

Make sure your DOM manipulations are inside a directive to keep your controller from touching the UI, thus keeping it isolated and more testable.  You do inherit the scope of the controller in the directive, thus having access to scope’s variables that controller populates.  You should make scope for directives strongly typed to avoid errors and to share the type between controller and directive.

Enjoy.

Leave a Reply

Your email address will not be published. Required fields are marked *