Lesson 2: Expanding Your Toolkit with Filters and Service Types

In this Lesson, we will cover the following recipes:

  • Using the uppercase and lowercase filters
  • Using the number and currency filters
  • Using the date filter
  • Debugging using the json filter
  • Using data filters outside the template
  • Using built-in search filters
  • Chaining filters
  • Creating custom data filters
  • Creating custom search filters
  • Filtering with custom comparators
  • Building a search filter from scratch
  • Building a custom search filter expression from scratch
  • Using service values and constants
  • Using service factories
  • Using services
  • Using service providers
  • Using service decorators

Introduction

In this Lesson, you will learn how to effectively utilize AngularJS filters and services in your applications. Service types are essential tools required for code reuse, abstraction, and resource consumption in your application. Filters, however, are frequently glazed over in introductory courses as they are not considered integral to learning the framework basics. This is a pity as filters let you afford the ability to abstract and compartmentalize large chunks of application functionality cleanly.

All AngularJS filters perform the same class of operations on the data they are passed, but it is easier to think about filters in the context of a pseudo-dichotomy in which there are two kinds: data filters and search filters.

At a very high level, AngularJS data filters are merely tools that modulate JavaScript objects cleanly in the template. On the other half of the spectrum, search filters have the ability to select elements of an enumerable collection that match some of the criteria you have defined. They should be thought of as black box modifiers in your template—well-defined layers of indirection that keep your scopes free of messy data-parsing functions. They both enable your HTML code to be more declarative, and your code to be DRY.

Service types can be thought of as injectable singleton classes to be used throughout your application in order to house the utility functionality and maintain states. The AngularJS service types can appear as values, constants, factories, services, or providers.

Although filters and services are used very differently, a cunning developer can use them both as powerful tools for code abstraction.

Using the uppercase and lowercase filters

Two of the most basic built-in filters are uppercase and lowercase filters, and they can be used in the following fashion.

How to do it…

Suppose that you define the following controller in your application:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function ($scope) {
  $scope.data = {
    text: 'The QUICK brown Fox JUMPS over The LAZY dog',
    nums: '0123456789',
    specialChars: '!@#$%^&*()',
    whitespace: '   '
  };
});

You will then be able to use the filters in the template by passing them via the pipe operator, as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <p>{{ data.text | uppercase }}</p>
    <p>{{ data.nums | uppercase }}</p>
    <p>{{ data.specialChars | uppercase }}</p>
    <p>_{{ data.whitespace | uppercase }}_</p>
  </div>
</div>

The output rendered will be as follows:

THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG
0123456789
!@#$%^&*()
_   _

Similarly, the lowercase filter can be used with predictable results:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <p>{{ data.text | lowercase }}</p>
    <p>{{ data.nums | lowercase }}</p>
    <p>{{ data.specialChars | lowercase }}</p>
    <p>_{{ data.whitespace | lowercase }}_</p>
  </div>
</div>

The output rendered will be as follows:

the quick brown fox jumps over the lazy dog
0123456789
!@#$%^&*() 
_   _

How it works…

The uppercase and lowercase filters are essentially simple AngularJS wrappers used for native string methods toUpperCase() and toLowerCase() available in JavaScript. These filters ignore number characters, special characters, and whitespace when performing appropriate substitutions.

There's more…

As these filters are merely wrappers for native JavaScript methods, you almost certainly won't ever have a need to use them anywhere outside the template. Their primary utility is in their ability to be invoked in the template and their ability to chain themselves alongside other filters that might require them. For example, if you had created a search filter that only matched identical string matches in its results, you might want to pass a search string through a lowercase filter before passing it through the search comparator.

See also

  • The Chaining filters recipe demonstrates how you would go about using lowercase filters in conjunction with other filters

Using the number and currency filters

AngularJS has some built-in filters that are less simple, such as number and currency; they can be used to format numbers into normalized strings. They also accept optional arguments that can further customize how the filters work.

Getting ready…

Suppose that you define the following controller in your application:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function ($scope) {
  $scope.data = {
    bignum: 1000000,
    num: 1.0,
    smallnum: 0.9999,
    tinynum: 0.0000001
  };
});

How to do it…

You can apply the number filter in your template, as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <p>{{ data.bignum | number }}</p>
    <p>{{ data.num | number }}</p>
    <p>{{ data.smallnum | number }}</p>
    <p>{{ data.tinynum | number }}</p>
  </div>
</div>

The output rendered will be as follows:

1,000,000
1
1.000
1e-7

This outcome might seem a bit arbitrary, but it demonstrates the next facet of filters examined here, which are arguments. Filters can take arguments to further customize the output. The number filter takes a fractionSize argument, which defines how many decimal places it will round to, defaulting to 3. This is shown in the following code:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <!—- data | number : fractionSize(optional) -->
    <p>{{ data.smallnum | number : 4 }}</p>
    <p>{{ data.tinynum | number: 7 }}</p>
    <p>{{ 012345.6789 | number : 2 }}</p>
  </div>
</div>

The output rendered will be as follows:

0.9999
0.0000001
12,345.68

The currency filter is another AngularJS filter that takes an optional argument, symbol:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <!—- data | currency : symbol(optional) -->
    <p>{{ 1234.56 | currency }}</p>
    <p>{{ 0.02 | currency }}</p>
    <p>{{ 45682.78 | currency : "&#8364;" }}</p>
  </div>
</div>

The output rendered will be as follows:

$1,234.56
$0.02
€45,682.78

How it works…

JavaScript has a single format in which it stores numbers as 64-bit double precision floating point numbers. These AngularJS filters exist to neatly format this raw number format by examining the values passed to it and by deciding how to appropriately format it as a string. The number filter handles rounding, truncation, and compression in negative exponents. It optionally accepts the fractionSize argument, in order to allow you to customize the filter to your needs, something that greatly increases the utility of filters. The currency filter handles rounding and appending of the designated currency symbol. It optionally accepts the symbol argument, which will insert the provided symbol in front of the formatted number.

There's more…

Both of these filters inherently utilize the $locale service, which acts as a fallback for default arguments (for example, providing a $ character for the currency filter in regions that use dollar, ordering of dates, and more). This service exists as a part of AngularJS's mission to act as a region agnostic framework.

See also…

  • The Chaining filters recipe demonstrates how you will go about using these filters in conjunction with other filters

Using the date filter

The date filter is an extremely robust and customizable filter that can handle many different kinds of raw date strings and convert them into human readable versions. This is useful in situations when you want to let your server defer datetime processing to the client and just be able to pass it a Unix timestamp or an ISO date.

Getting ready…

Suppose, you have your controller set up in the following fashion:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function ($scope) {
  $scope.data = {
    unix: 1394787566535,
    iso: '2014-03-14T08:59:26Z',
    date: new Date(2014, 2, 14, 1, 59, 26, 535)
  };
});

How to do it…

All the date formats can be used seamlessly with the date filter inside the template, as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <p>{{ data.unix | date }}</p>
    <p>{{ data.iso | date }}</p>
    <p>{{ data.date | date }}</p>
  </div>
</div>

The output rendered will be as follows:

Mar 14, 2014
Mar 14, 2014
Mar 14, 2014

The date filter is heavily customizable, giving you the ability to generate a date and time representation using any piece of the datetime passed to it:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <!—- AngularJS matches the expression components 
    to datetime components, then stringifies as specified -->
    <p>{{ data.unix | date : "EEEE 'at' H:mma" }}</p>
    <p>{{ data.iso | date : "longDate" }}</p>
    <p>{{ data.date | date : "M/d H:m:s.sss" }}</p>
  </div>
</div>

This code uses various pieces of the date filter syntax to pull out elements from the datetime generated inside the filter, and assemble them together in the output string, the template for which is provided in the optional format argument. The output rendered will be as follows:

Friday at 1:59AM
March 14, 2014
3/14 1:59:26.535

How it works…

The date filter wraps a robust set of complex regular expressions inside the framework, which exists to parse the string passed to it into a normalized JavaScript date object. This date object is then broken apart and molded into the desired string format specified by the filter's argument syntax.

Note

The AngularJS documentation at https://docs.angularjs.org/api/ng/filter/date provides the details of all the possible input and output formats required for date filters.

There's more…

The date filter provides you with two levels of indirection: normalized conversion from various datetime formats and normalized conversion into almost any human readable format. Note that in the absence of a provided time zone, the time zone assumed is the local time zone, which in this example is Pacific Daylight Time (UTC - 7), which is accommodated through the $locale service.

Debugging using the json filter

AngularJS provides you with a JSON conversion tool, the json filter, to serialize JavaScript objects into prettified JSON code. This filter isn't so much in use for production applications as it is used for real-time inspection of your scope objects.

Getting ready…

Suppose your controller is set up as follows with a prefilled user data object:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function ($scope) {
  $scope.user = {
    id: 123,
    name: {
      first: 'Jake',
      last: 'Hsu'
    },
    username: 'papatango',
    friendIds: [5, 13, 3, 1, 2, 8, 21], 
    // properties prefixed with $$ will be excluded
    $$no_show: 'Hide me!'
  };
});

How to do it…

Your user object can be serialized in the template, as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <pre>{{ user | json }}</pre>
  </div>
</div>

The output will be rendered in HTML, as follows:

{
  "id": 123,
  "name": {
    "first": "Jake",
    "last": "Hsu"
  },
  "username": "papatango",
  "friendIds": [
    5,
    13,
    3,
    1,
    2,
    8,
    21
  ]
}

How it works…

The json filter simply wraps the JSON.stringify() method in JavaScript in order to provide you with an easy way to spit out formatted objects for inspection. When the filtered object is fed into a <pre> tag, the JSON string will be properly indented in the rendered template. Properties prefixed with $$ will be skipped by the serializer as this notation is used internally in AngularJS as a private identifier.

There's more…

As AngularJS lets you afford two-way data binding in the template, you can see the filtered object update in real time in your template, as various interactions with your application change it; this is extremely useful for debugging.

Using data filters outside the template

Filters are built to perform template data processing, so their utilization outside the template will be infrequent. Nonetheless, AngularJS provides you with the ability to use filter functions via an injection of $filter.

Getting ready

Suppose that you have an application, as follows:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function ($scope) {
  $scope.val = 1234.56789;
});

How to do it…

In the view templates, the argument order is scrambled with the following format:

data | filter : optionalArgument

For this example, it would take the form in the template as follows:

<p>{{ val | number : 4 }}</p>

This will give the following result:

1,234.5679

In this example, it's cleanest to apply the filter in the view template, as the purpose of formatting the number is merely for readability. If, however, the number filter is needed to be used in a controller, $filter can be injected and used as follows:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function ($scope, $filter) {
  $scope.val = 1234.56789;
  $scope.filteredVal = $filter('number')($scope.val, 4);
});

With this, the values of $scope.val and $scope.filteredVal will be identical.

How it works…

Although the syntax is very different compared to what is found in a template, using a dependency injected filter is functionally the same as applying it in the view template. The same filter method is invoked for both formats and both generate the same output.

There's more…

Although there are no cardinal sins committed by injecting $filter and using your filters that way, the syntax is awkward and verbose. Filters aren't really designed for that sort of use anyway. AngularJS is meant for building declarative templates, and that is exactly what data filters provide when used in templates—lightweight and flexible modulation functions for cleaning and organizing your data.

One of the primary use cases for using filters outside the template is when you are building a custom filter that uses one or more existing filters inside it. For example, you might want to use the currency filter inside a custom filter, which decides whether to use a $ or a ¢ prefix based on whether or not the amount is greater or less than $1.00.

Using built-in search filters

Search filters serve to evaluate individual elements in an enumerable object and return whether or not they belong in the resultant set. The returned value from the filter will also be an enumerable set with none, some, or all of the original values that were removed. AngularJS provides a rich suite of ways to filter an enumerable object.

Getting ready

Search filters return a subset of an enumerable object, so prepare a controller as follows, with a simple array of strings:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function ($scope) {
  $scope.users = [
    'Albert Pai',
    'Jake Hsu',
    'Jack Hanford',
    'Scott Robinson',
    'Diwank Singh'
  ];
});

How to do it…

The default search filter is used in the template in the same fashion as a data filter, but invoked with the pipe operator. It takes a mandatory argument, that is, the object that the filter will compare against.

The easiest way to test a search filter is by tying an input field to a model and using that model as the search filter argument, as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <input type="text" ng-model="search.val" />
  </div>
</div>

This model can then be applied in a search filter on an enumerable data object. The filter is most commonly applied inside an ng-repeat expression:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <input type="text" ng-model="search.val" />
    <p ng-repeat="user in users | filter : search.val">
      {{ user }}
    </p>
  </div>
</div>

Entering ja will return the following output:

Jake Hsu
Jack Hanford

Entering s will return the following output:

Jake Hsu
Scott Robinson
Diwank Singh

Entering a will return the following output:

Albert Pai
Jake Hsu
Jack Hanford
Diwank Singh

How it works…

With this setup, the string in the search.val model will be matched (case insensitive) against each element in the enumerable object and will only return the matches for the repeater to iterate through. This transformation occurs before the object is passed to the repeater, so the filter combined with AngularJS data binding results in a very impressive real-time, in-browser filtering system with minimal overhead.

See also

  • The Chaining filters recipe demonstrates how to utilize a string search filter in conjunction with existing AngularJS string modulation filters
  • The Filtering with custom comparators recipe demonstrates how to further customize the way an enumerable collection is compared to the reference object

Chaining filters

As AngularJS search filters simply reduce the modulation functions that return a subset of the object that is passed to it, it is possible to chain multiple filters together.

When filtering enumerable objects, AngularJS provides two built-in enumeration filters that are commonly used in conjunction with the search filters: limitTo and orderBy.

Getting ready

Suppose that your application contains a controller as follows with a simple array of objects containing a name string property:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function ($scope) {
  $scope.users = [
    {name: 'Albert Pai'}, 
    {name: 'Jake Hsu'},
    {name: 'Jack Hanford'},
    {name: 'Scott Robinson'},
    {name: 'Diwank Singh'}
  ];
});

In addition, suppose that the application template is set up as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <input type="text" ng-model="search.val" />
    <!—- simple repeater filtering against search.val -->
    <p ng-repeat="user in users | filter : search.val">
      {{ user.name }}
    </p>
  </div>
</div>

How to do it…

You can chain another filter following your first with an identical syntax by merely adding another pipe operator and the filter name with arguments. Here, you can see the setup to apply the limitTo filter to the matching results:

(index.html)

<p ng-repeat="user in users | filter : search.val | limitTo: 2">
  {{ user.name }}
</p>

Searching for h will result in the following output:

Jake Hsu
Jack Hanford

You can chain another filter, orderBy, which will sort the array, as follows:

(index.html)

<p ng-repeat="user in users | filter : search.val | orderBy: 'name' | limitTo : 2">
  {{ user.name }}
</p>

Searching for h will result in the following output:

Diwank Singh
Jack Hanford

How it works…

AngularJS search filters are functions that return a Boolean, representing whether or not the particular element of the enumerable object belongs to the resultant set. For the array of string primitives in the preceding code, the filter performs a simple case-insensitive substring match operation against the provided matching string taken from the model bound to the <input> tag.

The subsequent chained filters orderBy and limitTo also take an enumerable object as an argument and perform an additional operation on it. In the preceding example, the filter first reduces the string array to a subset string array, which is first passed to the orderBy filter. This filter sorts the subset string array by the expression provided, which here is alphabetical order, as the argument is a string. This sorted array is then passed to the limitTo filter which truncates the sorted substring subset string array to the number of characters specified in the argument. This final array is then fed into the repeater in the template for rendering.

There's more…

It's worth mentioning that chained AngularJS filters are not necessarily commutative; the order in which filters are chained matters, as they are evaluated sequentially. In the last example, reversing the order of the chained filters (limitTo followed by orderBy) will truncate the subset string array and then sort only the truncated results. The proper way to think about this is to compare them to nested functions—similar to how foo(bar(x)) is obviously not the same as bar(foo(x)), and x | foo | bar is not the same as x | bar | foo.

Creating custom data filters

At some point, the provided AngularJS data filters will not be enough to fill your needs, and you will need to create your own data filters. For example, assume that in an application that you are building, you have a region of the page that is limited in physical dimensions, but contains an arbitrary amount of text. You would like to truncate that text to a length which is guaranteed to fit in the limited space. A custom filter, as you might imagine, is perfect for this task.

How to do it…

The filter you wish to build accepts a string argument and returns another string. For now, the filter will truncate the string to 100 characters and append an ellipsis at the point of truncation:

(app.js)

angular.module('myApp', [])
.filter('simpletruncate', function () {
  // the text parameter 
  return function (text) {
    var truncated = text.slice(0, 100);
    if (text.length > 100) {
      truncated += '...';
    }
    return truncated;
  };
});

This will be used in the template, as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <p>{{ myText | simpletruncate }}</p>
  </div>
</div>

This filter works well, but it feels a bit brittle. Instead of just defaulting to 100 characters and an ellipsis, the filter should also accept parameters that allow undefined input and optional definition of how many characters to truncate to and what the stop character(s) should be. It would be even better if the filter only cut off the text at a set of whitespace characters if possible:

(app.js)

angular.module('myApp', [])
.filter('regextruncate',function() {
  return function(text,limit,stoptext) {
    var regex = /s/;
    if (!angular.isDefined(limit)) {
      limit = 100;
    }
    if (!angular.isDefined(stoptext)) {
      stoptext = '...';
    } 
    limit = Math.min(limit,text.length);
    for(var i=0;i<limit;i++) {
      if(regex.exec(text[limit-i]) 
         && !regex.exec(text[(limit-i)-1])) {
        limit = limit-i;
        break;
      }
    }
    var truncated = text.slice(0, limit);
    if (text.length>limit) {
      truncated += stoptext;
    }
    return truncated;
  };
});

This will be used in the template as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <p>{{ myText | regextruncate : 150 : '???' }}</p>
  </div>
</div>

How it works…

The final version of the filter uses a simple whitespace-detecting regular expression to find the first point in the string that it can truncate. After setting the default values of limit and stoptext, the data filter iterates backwards through the relevant string values, watching for the first point at which it sees a non whitespace character followed by a whitespace character. This is the point at which it sets the truncation, and the string is broken apart, and then the relevant segment is returned with the appended stoptext statement.

These filter examples don't modify the model in any way, they are merely context-free data wrappers that package your model data neatly into a format that your template can easily digest. Each model change causes the filter to be invoked in order to keep the data in the template up-to-date, so the filter processing must be lightweight as it is assumed that the filter will be frequently invoked.

There's more…

A rich suite of data filters in your application will allow a cleaner decoupling of the presentation layer and model. The demonstration in this recipe was limited to the string primitive, but there is no reason you could not extend your filter logic to encompass and handle complex data objects in your application's models.

The entire purpose of filters is to improve readability and reusability, so if the construction and application of a custom filter enables you to do that, you are encouraged to do so.

Creating custom search filters

AngularJS search filters work exceedingly well out of the box, but you will quickly develop the desire to introduce some customization of how the filter actually relates the search object to the enumerable collection. This collection is frequently composed of complex data objects; a simple string comparison will not suffice, especially when you want to modify the rules by which matches are governed.

Searching against data objects is simply a matter of building the search object in the same mould as the enumerable collection objects.

Getting ready

Suppose, for example, your controller looks as follows:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function($scope) {
  $scope.users = [
    { 
      firstName: 'John',
      lastName: 'Stockton'
    },
    {
      firstName: 'Michael',
      lastName: 'Jordan'
    }
  ];
});

How to do it…

When searching against this collection, in the case where the search filter is passed a string primitive, it will perform a wildcard search, as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <input ng-model="search" />
    <p ng-repeat="user in users | filter:search">
      {{ user.firstName}} {{ user.lastName }}
    </p>
  </div>
</div>

With this, if you were to enter jo in the input field, both John Stockton and Michael Jordan will be returned. When asked to compare a string primitive to an object, AngularJS has no choice but to compare the string to every field it can, and any objects that match are declared to be a part of the match-positive resultant set.

If instead you only want to compare against specific attributes of the enumerable collection, you can set the search object to have correlating attributes that should be matched against the collection attributes, as shown here:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <input ng-model="search.firstName" />
    <p ng-repeat="user in users | filter:search">
      {{ user.firstName}} {{ user.lastName }}
    </p>
  </div>
</div>

Now, if you were to enter jo in the input field, only John Stockton will be returned.

Filtering with custom comparators

If you want to search only for exact matches, vanilla wildcard filtering becomes problematic as the default comparator uses the search object to match against substrings in the collection object. Instead, you might want a way to specify exactly what constitutes a match between the reference object and enumerable collection.

Getting ready

Suppose that your controller contains the following data object:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function($scope) {
  $scope.users = [
    { 
      firstName: 'John',
      lastName: 'Stockton',
      number: '12'
    },
    {
      firstName: 'Michael',
      lastName: 'Jordan',
      number: '23'
    },
    {
      firstName: 'Allen',
      lastName: 'Iverson',
      number: '3'
    }
  ];
});

How to do it…

Instead of using just a single search box, the application will use two search fields, one for the name and one for the number. Having a wildcard search for the first name and last name is more useful, but searching for wildcard numbers is not useful in this situation.

The search fields are constructed as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <input ng-model="search.$" />
    <input ng-model="search.number" />
    <p ng-repeat="user in users | filter:search">
      {{ user.firstName}} {{ user.lastName }}
    </p>
  </div>
</div>

The first input field appears with $; this is done merely to assign the wildcard search to the entire search object so that it does not interfere with other assigned search attributes. The second input field specifies that the application should only search against the collection's number attribute.

As expected, testing this code reveals that the number search field is performing a wildcard search, which is not desirable. To specify exact matches when searching, the filter takes an optional comparator argument that mandates how matches will be ascertained. A true value passed will enable exact matches:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <input ng-model="search.$" required />
    <input ng-model="search.number" required />
    <p ng-repeat="user in users | filter:search:true">
      {{ user.firstName}} {{ user.lastName }}
    </p>
  </div>
</div>

With this setup, both inputs will create an AND filter to select data from the array with one or multiple criteria. The required statement will cause the model bound to it to reset to undefined, when the input is an empty string.

How it works…

The comparator argument will be resolved to a function in all cases. When passing in true, AngularJS will treat it as an alias for the following code:

function(actual, expected) { 
  return angular.equals(expected, actual);
}

This will function as a strict comparison of the element in the enumerable collection and the reference object.

More generally, you can also pass in your own comparator function, which will return true or false based on whether or not actual matches expected. This will take the following form:

function(actual, expected) {
  // logic to determine if actual
  // should count as a match for expected
}

The functions from the comparator argument are the ones used to determine whether each piece of the enumerable collection belongs in the resultant subset.

See also

  • The Building a search filter from scratch and Building a custom search filter expression from scratch recipes demonstrate alternate methods of architecting search filters to match your application's needs

Building a search filter from scratch

The provided search filters can serve your application's purposes only to a point. Eventually, you will need to construct a complete solution in order to filter an enumerable collection.

Getting ready

Suppose that your controller contains the following data object:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function($scope) {
  $scope.users = [
    { 
      firstName: 'John',
      lastName: 'Stockton',
      number: '12'
    },
    {
      firstName: 'Michael',
      lastName: 'Jordan',
      number: '23'
    },
    {
      firstName: 'Allen',
      lastName: 'Iverson',
      number: '3'
    }
  ];
});

How to do it…

Suppose you wanted to create an OR filter for the name and number values. The brute force way to do this is to create an entirely new filter in order to replace the AngularJS filter. The filter takes an enumerable object and returns a subset of the object. Adding the following will do exactly that:

(app.js)

.filter('userSearch', function () {
  return function (users, search) {
    var matches = [];
    angular.forEach(users, function (user) {
      if (!angular.isDefined(users) || 
          !angular.isDefined(search)) {
        return false;
      }
      // initialize match conditions
      var nameMatch = false,
        numberMatch = false;
      if (angular.isDefined(search.name) && 
          search.name.length > 0) {
        // substring of first or last name will match
        if (angular.isDefined(user.firstName)) {
            nameMatch = nameMatch || 
          user.firstName.indexOf(search.name) > -1;
        }
        if (angular.isDefined(user.lastName)) {
            nameMatch = nameMatch || 
          user.lastName.indexOf(search.name) > -1;
        }
      }
      if (angular.isDefined(user.number) && 
          angular.isDefined(search.number)) {
        // only match if number is exact match
        numberMatch = user.number === search.number;
      }
      // either match should populate the results with user
      if (nameMatch || numberMatch) {
        matches.push(user);
      }
    });
    // this is the array that will be fed to the repeater
    return matches;
  };
});

This would then be used as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <input ng-model="search.name" 
           required />
    <input ng-model="search.number" 
           required />
    <p ng-repeat="user in users | userSearch : search">
      {{ user.firstName }} {{ user.lastName }}
    </p>
  </div>
</div>

How it works…

Since this filter is built from scratch, it's constructed to handle all the edge cases of missing attributes and objects in the parameters. The filter performs substring lookups on the first and last name attributes and exact matches on number attributes. Once this is done, it performs the actual OR operation on the two results. However, having entirely rebuilt the search filter, it must return the entire collection subset.

There's more…

Rebuilding the filtering mechanism from top to bottom, as shown in this recipe, only makes sense if you need to significantly diverge from the existing filtering mechanism functionality.

See also

  • The Building a custom search filter expression from scratch recipe shows you how to perform custom filtering while working within the existing search filter mechanisms

Building a custom search filter expression from scratch

Instead of reinventing the wheel, you can create a search filter expression that evaluates to true or false for each iteration in the enumerable collection.

How to do it…

The simplest way to do this is to define a function on your scope, as follows:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function ($scope) {
  $scope.users = [
    ...
  ];
  $scope.usermatch = function (user) {
    if (!angular.isDefined(user) || 
        !angular.isDefined($scope.search)) {
      return false;
    }
    var nameMatch = false,
      numberMatch = false;
    if (angular.isDefined($scope.search.name) && 
        $scope.search.name.length > 0) {
      if (angular.isDefined(user.firstName)) {
        nameMatch = nameMatch || 
          user.firstName.indexOf($scope.search.name) > -1;
      }
      if (angular.isDefined(user.lastName)) {
        nameMatch = nameMatch || 
          user.lastName.indexOf($scope.search.name) > -1;
      }
    }
    if (angular.isDefined(user.number) &&
        angular.isDefined($scope.search.number)) {
      numberMatch = user.number === $scope.search.number;
    }
    return nameMatch || numberMatch;
  };
});

Now, this can be passed to the built-in filter as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <input ng-model="search.name" required />
    <input ng-model="search.number" required />
    <p ng-repeat="user in users | filter:usermatch">
      {{ user.firstName }} {{ user.lastName }}
    </p>
  </div>
</div>

In the Name search box, typing Jo now returns Michael Jordan and John Stockton and in the Number search box, typing 3 only returns Allen Iverson. Searching for both Mi and 3 will return Michael Jordan and Allen Iverson, as the filter constructed here is an OR filter. If you want to change it to an AND filter, you can simply change the return line to the following:

return nameMatch && numberMatch;

How it works…

All of these search filter techniques can be framed through a perspective that pays attention to what you are filtering. Search filters merely apply the question: "Does this fit my definition of a match?", over and over again. AngularJS's data binding causes this question to be asked to each member of the enumerable collection each time the object changes in content or population. The preceding recipes merely define how this question gets asked.

There's more…

Filters are merely applied JavaScript functions and the mechanisms by which they can be configured are flexible. Rarely in production applications will the built-in search filter infrastructure be sufficient, so it is advantageous to instead be able to mould exactly how the filter interprets a match.

Furthermore, as you begin to examine performance limitations, you will begin to consider ways to optimize repeaters and filters. If kept lightweight, filters are inexpensive and can be run hundreds of times in rapid succession without consequence. As complexity and data magnitude scale, filters can allow you to maintain a performant and responsive application.

Using service values and constants

AngularJS service types, at their core, are singleton containers used for unified resource access across your application. Sometimes, the resource access will just be a single JS object. For this, AngularJS offers service values and service constants.

How to do it…

Service values and service constants both act in a very similar way, but with one important difference.

Service value

The service value is the simplest of all service types. The value service acts as a key-value pair and can be injected and used as follows:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function($scope, MyValue) {
  $scope.data = MyValue;
  $scope.update = function() {
    MyValue.name = 'Brandon Marshall';
  };
})
.value('MyValue', {
  name: 'Tim Tebow',
  number: 15
});

An example of template use is as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="update()">Update</button>
    {{ data.name }} #{{ data.number }}
  </div>
</div>

You'll notice that AngularJS has no problem with you updating the service value. Since it is a singleton, any part of your application that injects the value service and reads/writes to it will be accessing the same data. Service values act like service factories (discussed in the Using service factories recipe) and cannot be injected into the providers or the config() phase of your application.

Service constant

Like service values, service constants also act as singleton key-value pairs. The important difference is that service constants act like service providers and can be injected into the config() phase and service providers. They can be used as follows:

(app.js)

angular.module('myApp', [])
.config(function(MyConstant) {
  // can't inject $log into config()
  console.log(MyConstant);
})
.controller('Ctrl', function($scope, MyConstant) {
  $scope.data = MyConstant;
  $scope.update = function() {
    MyConstant.name = 'Brandon Marshall';
  };
})
.constant('MyConstant', {
  name: 'Tim Tebow',
  number: 15
});

The template remains unchanged from the service value example.

How it works…

Service values and service constants act as read/write key-value pairs. The main difference is that you can choose one over the other based on whether you will need to have the data available to you when the application is being initialized.

See also

  • The Using service providers recipe provides details of the ancestor service type and how it relates to the service type life cycle
  • The Using service decorators recipe demonstrates how a service type initialization can be intercepted for a just in time modification

Using service factories

A service factory is the simplest general purpose service type that allows you to use the singleton nature of AngularJS services with encapsulation.

How to do it…

The service factory's return value is what will be injected when the factory is listed as a dependency. A common and useful pattern is to define private data and functions outside this object, and define an API to them through a returned object. This is shown in the following code:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function($scope, MyFactory) {
  $scope.data = MyFactory.getPlayer();
  $scope.update = MyFactory.swapPlayer;
})
.factory('MyFactory', function() {
  // private variables and functions
  var player = {
    name: 'Peyton Manning',
    number: 18
  },  swap = function() {
    player.name = 'A.J. Green';
  };
  // public API
  return {
    getPlayer: function() {
      return player;  
    },
    swapPlayer: function() {
      swap();
    }
  };
});

Since the service factory values are now bound to $scope, they can be used in the template normally, as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="update()">Update</button>
    {{ data.name }} #{{ data.number }}
  </div>
</div>

How it works…

This example might feel a bit contrived, but it demonstrates the basic usage pattern that can be used with service factories for great effect. As with all service types, this is a singleton, so any modifications done by a component of the application will be reflected anywhere the factory is injected.

See also

  • The Using services recipe shows how the sibling type of service factories is incorporated into applications
  • The Using service providers recipe provides you with the details of the ancestor service type and how it relates to the service type life cycle
  • The Using service decorators recipe demonstrates how service type initialization can be intercepted for a just in time modification

Using services

Services act in much the same way as service factories. Private data and methods can be defined and an API can be implemented on the service object through it.

How to do it…

A service is consumed in the same way as a factory. It differs in that the object to be injected is the controller itself. It can be used in the following way:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function($scope, MyService) {
  $scope.data = MyService.getPlayer();
  $scope.update = MyService.swapPlayer;
})
.service('MyService', function() {
  var player = {
    name: 'Philip Rivers',
    number: 17
  },  swap = function() {
    player.name = 'Alshon Jeffery';
  };
  this.getPlayer = function() {
    return player;  
  };
  this.swapPlayer = function() {
    swap();
  };
});

When bound to $scope, the service interface is indistinguishable from a factory. This is shown here:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="update()">Update</button>
    {{ data.name }} #{{ data.number }}
  </div>
</div>

How it works…

Services invoke a constructor with the new operator, and the instantiated service object is the delivered injectable. Like a factory, it still exists as a singleton and the instantiation is deferred until the service is actually injected.

See also

  • The Using service factories recipe shows how the sibling type of service is incorporated in applications
  • The Using service providers recipe provides the details of the ancestor service type and how it relates to the service type life cycle
  • The Using service decorators recipe demonstrates how service type initialization can be intercepted for a just in time modification

Using service providers

Service providers are the parent service type used for factories and services. They are the most configurable and extensible of the service types, and allow you to inspect and modify other service types during the application's initialization.

How to do it…

Service providers take a function parameter that returns an object that has a $get method. This method is what AngularJS will use to produce the injected value after the application has been initialized. The object wrapping the $get method is what will be supplied if the service provider is injected into the config phase. This can be implemented as follows:

(app.js)

angular.module('myApp', [])
.config(function(PlayerProvider) {
  // appending 'Provider' to the injectable 
  // is an Angular config() provider convention
  PlayerProvider.configSwapPlayer();
  console.log(PlayerProvider.configGetPlayer());
})
.controller('Ctrl', function($scope, Player) {
  $scope.data = Player.getPlayer();
  $scope.update = Player.swapPlayer;
})
.provider('Player', function() {
  var player = {
    name: 'Aaron Rodgers',
    number: 12
  },  swap = function() {
    player.name = 'Tom Brady';
  };
  
  return {
    configSwapPlayer: function() {
      player.name = 'Andrew Luck';
    },
    configGetPlayer: function() {
      return player;
    },
    $get: function() {
      return {
        getPlayer: function() {
          return player;
        },
        swapPlayer: function() {
          swap();
        }
      };
    }
  };
});

When used this way, the provider appears to the controller as a normal service type, as follows:

(app.js)

 .controller('Ctrl', function($scope, Player) {
    $scope.data = Player.getPlayer();
    $scope.update = Player.swapPlayer;
 })

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="update()">Update</button>
    {{ data.name }} #{{ data.number }}
  </div>
</div>

How it works…

Providers is the only service type that can be passed into a config function. Injecting a provider into the config function gives access to the wrapper object, and injecting a provider into an initialized application component will give you access to the return value of the $get method. This is useful when you need to configure aspects of a service type before it is used throughout the application.

There's more…

Providers can only be injected as their configured services in an initialized application. Similarly, types like service factories and services cannot be injected in a provider, as they will not yet exist during the config phase.

See also

  • The Using service decorators recipe demonstrates how a service type initialization can be intercepted for a just in time modification

Using service decorators

An often overlooked aspect of AngularJS services is their ability to decorate service types in the initialization logic. This allows you to add or modify how factories or services will behave in the config phase before they are injected in the application.

How to do it…

In the config phase, the $provide service offers a decorator method that allows you to inject a service and modify its definition before it is formally instantiated. This is shown here:

(app.js)

angular.module('myApp', [])
.config(function($provide) {
  $provide.decorator('Player', function($delegate) {
    // $delegate is the Player service instance
    $delegate.setPlayer('Eli Manning');
    return $delegate;
  });
})
.controller('Ctrl', function($scope, Player) {
  $scope.data = Player.getPlayer();
  $scope.update = Player.swapPlayer;
})
.factory('Player', function() {
  var player = {
    number: 10
  },  swap = function() {
    player.name = 'DeSean Jackson';
  };
  
  return {
    setPlayer: function(newName) {
      player.name = newName;
    },
    getPlayer: function() {
      return player;
    },
    swapPlayer: function() {
      swap();
    }
  };
});

As you have merely modified a regular factory, it can be used in the template normally, as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="update()">Update</button>
    {{ data.name }} #{{ data.number }}
  </div>
</div>

How it works…

The decorator acts to intercept the creation of a service upon instantiation that allows you to modify or replace the service type as desired. This is especially useful when you are looking to cleanly monkeypatch a third-party library.

Note

Constants cannot be decorated.

See also

  • The Using service providers recipe provides details of the ancestor service type and how it relates to the service type life cycle
See also
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.187.97