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

Introducing filters and services

traceypowersg edited this page Oct 26, 2013 · 40 revisions

In this chapter, we will add two features to our app. First, we will get the recipe data from a service instead of hardcoding mock data into the app. Second, we will look at another handy tool in Angular’s toolbox - filters. When you’re finished, you will be able to use built in filters as well as create your own custom filters. You will also be able to read data from a server-side service like a real app.

###The code Here is the code for the next version of our Recipe Book app. Run the code in Dart Editor. Notice the text box and checkboxes at the top of the app. You can use these inputs to control which recipes are shown in the list. Start typing into the filter box and watch the list of visible recipes shrink to match your search criteria. Next, limit your results further by checking a few categories to limit to. Try using both filters together by typing in a search criteria and also limiting based on category. Lastly, clear the filters by pressing the “Clear Filters” button, and watch all your results re-appear.

###Introducing filters Filters let you change how your model data is displayed in the view without changing the model data itself. They do things like allow you to show parts of the model data, or display data in a particular format. You can also use Angular’s custom filters feature to create your own filters to do anything you want.

####Built in filters Angular has some built in filters that provide some handy functionality. Here is a list of all the currently implemented Angular Dart filters: http://ci.angularjs.org/view/Dart/job/angular.dart-master/javadoc/angular.filter.html

These are pretty self explanatory. The CurrencyFilter formats numbers like money. The DateFilter formats dates. The UppercaseFilter and LowercaseFilters do what you would expect. The LimitToFilter limits the number of results for a list model object that will appear in the view.

In our app, we use the FilterFilter. Like the LimitToFilter, the FilterFilter also limits the number of list model objects that will appear in the view. It chooses which items to display based on whether or not they satisfy the filter criteria.

Here is how we use the FilterFilter.

First, we created a text input box and bound it through ng-model to a property called nameFilter. As the user types into the text input box, it updates the model object’s nameFilter property.

<input id="name-filter" type="text" 
  ng-model="ctrl.nameFilter"
  value=" "></input>

Next, we pipe the ng-repeat criteria through the filter, and tell the filter to use ctrl.nameFilter as the property to check.

<ul>
    <li class="pointer"
        ng-repeat="recipe in ctrl.recipes | filter:{name:ctrl.nameFilter}>

    </li>
</ul>

Lastly, we create a property on our RecipeBookController to store the nameFilter property for the input.

String nameFilter = "";

That’s all you need to start filtering your results.

####Custom filters Built in filters are nice, but what if you want to do something more specific. In our app, we also want to be able to reduce the number of recipes shown to those in a particular category. For this, we write a custom filter called CategoryFilter.

To create a custom filter in Angular, just create a simple Dart class that has a call method with the following signature:

call(valueToFilter, optArg1, optArg2);

The call method takes 3 parameters. First, it takes the incoming value to filter. This is the model object you wish to filter. In our case, it’s the recipe list.

Next, it takes some input from the view that will be applied in some way to the valueToFilter to perform the filtering. In our case, we use the second optional parameter to let us weed out recipes from the incoming list that we don’t want to display.

The call method returns the filtered value. In our case, it is a new recipe list that is a subset of the incoming recipe list.

Here is the call method from our CategoryFilter:

call(recipeList, filterMap) {
  if (recipeList is List && filterMap != null && filterMap is Map) {
    // If there is nothing checked, treat it as "everything is checked"
    bool nothingChecked = filterMap.values.every((isChecked) => !isChecked);
    if (nothingChecked) {
      return recipeList.toList();
    }
    return recipeList.where((i) => filterMap[i.category] == true).toList();
  }
}

The filterMap parameter deserves some further explanation. It’s the data that comes in off of the checkbox inputs. We will talk a little bit more about how checkboxes work in Angular in the next section.

Next, annotate the class to publish it as a filter:

@NgFilter(name: 'categoryfilter')

Add the new class to the bootstrapping code:

  var module = new MyAppModule()
    ...
    ..type(CategoryFilter)

and then use it from the view:

<div>
  <label for="category-filter">Filter recipes by category</label>
  <span id="category-filter" ng-repeat="category in ctrl.categories">
    <input type="checkbox"
        ng-model="ctrl.categoryFilterMap[category]">{{category}}
  </span>
</div>

Our view creates a checkbox input and label for each category. Angular stores each checkbox value as a boolean - true if checked, or false if not checked.

To create the checkboxes, we added a new piece of data to our app - a list of categories. We use the ng-repeat directive to iterate through the list and create a checkbox and label for each category. Inputs in Angular are bound to a model object with the ng-model directive. Here, we bind to a map called categoryFilterMap. The map’s keys are the category names, and the values are true or false depending on whether or not they’re checked.

Next, we plug the custom filter in the same way we would plug in a built in filter:

<li class="pointer"
    ng-repeat="recipe in ctrl.recipes | categoryfilter:ctrl.categoryFilterMap">

####Filter chaining Our app uses a feature called filter chaining to apply more than one filter to the same view element. Below, we see the ng-repeat directive has three filters separated by pipes. This is how Angular applies multiple filters to a single ng-repeat.

<li class="pointer"
    ng-repeat="recipe in ctrl.recipes | orderBy:'name' | filter:{name:ctrl.nameFilter} | categoryfilter:ctrl.categoryFilterMap">

###Introducing the Http service

Our last example had the data hard coded in the app. In reality, you’d request data from a server side service. Angular provides a built in service called the Http Service that handles making HTTP requests to the server. First lets look at the two new files we’ve added to the project: recipes.json and categories.json. These contain data that’s been serialized as a JSON string. We will use the Http service to make an HTTP request to the web server to fetch this data. Let’s look at how to do this.

First, we declare a property on the RecipeBookController class. Ours is called _http. The Http service is part of the core Angular package, so you don’t need to import anything new. Next, look at the RecipeBookController’s constructor. We’ve added a parameter and assigned it to the _http property. Angular instantiates the RecipeBookController class using Dependency Injection. In the main method, you set up the injector with your app’s Module. The call to ngBootstrap includes the AngularModule, which contains injection rules for all of Angular’s core features, including the Http service.

Http _http;
…

RecipeBookController(Http this._http) {
  _loadData();
}

Next, let’s look at how to use the Http service to fetch data from the server. Look at the changes we made to the _loadData method. Here is the new code:

void _loadData() {
  recipesLoaded = false;

  _http.get('/angular.dart.tutorial/Chapter_04/recipes.json')
    .then((HttpResponse response) {
      for (Map recipe in response.data) {
        recipes.add(new Recipe.fromJsonMap(recipe));
      }
      recipesLoaded = true;
    },
    onError: (Object obj) {
      recipesLoaded = false;
      loadingMessage = ERROR_MESSAGE;
    });
...

Let’s look more closely at the call to the Http service:

_http.get()
  .then(...)

The Http get method returns a Dart Future. A Future is the promise of a value sometime in the future. It is the core of asynchronous programming in Dart. Once the Future is complete, the then() method is invoked to process the response. then() takes two arguments, both functions. The first function is invoked when the Future is complete, and is used to process the data returned from the Future. The second optional argument is a function that will be invoked in the event that an error was returned from the call.

future.then((value) { 
        useValue(value); 
    },
    onError: (error) {
        handleError(error);
    }
);

The important thing to understand from this example is that Futures are asynchronous. Your app code will proceed while the data is loading from the server side. If you are connecting to a very slow service, it is possible that the app will finish loading before the data has been returned. The view should be prepared for this. In our case, we need two pieces of data before the app is useful:

List categories = [];
List<Recipe> recipes = [];

Until the future has returned, your recipe book contains an empty list of recipes and an empty list of categories. Any part of your view that uses or displays recipes or categories will see an empty list. If you don’t want empty spots displaying on the app, you can surround portions of your view with statements that display a “Loading...” message until the data is ready.

In our app, we keep track of whether we have enough data to load the app by storing the load state for recipes and categories and conditionally displaying a load message, or the whole view, depending on whether the app is done loading enough data to be useful.

<div ng-if="!ctrl.recipesLoaded && !ctrl.categoriesLoaded">
  {{ctrl.message}}
</div>
<div ng-if="ctrl.recipesLoaded &&ctrl.categoriesLoaded">
    ...

You can see the “Loading…” feature work by simulating a really slow loading data source. Put a breakpoint in the _loadData method and load the app. Notice that while you’re stopped at the breakpoint, your app shows the “Loading…” message. Now get out of the breakpoint and notice that your app’s real view pops into place. Also notice that we changed the loaded state from false to true inside the asynchronous part of _loadData (inside the then() call). If we’d put it at the end of the _loadData method outside of the asynchronous call, it would evaluate regardless of the state of the Future.

###Angular features ####ng-cloak You probably noticed that in previous examples, when you first loaded your app, you briefly saw curly braces like {{someVar}} just before your app “popped” into place, and the correct values appeared. The ng-cloak Directive combined with some CSS rules that you add to your app’s main CSS file allow you to avoid this blink. The blink happens between the time your HTML loads and Angular is bootstrapped and has compiled the DOM and has substituted in the real values for the uncompiled DOM values. While Angular is compiling the DOM, you can hide the page (or sections of it) by using ng-cloak:

<body class=”ng-cloak”>

or

<body ng-cloak>

The CSS rule causes the view to be hidden:

[ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
   display: none !important;
}

Once Angular is finished compiling the DOM, it secretly removes the ng-cloak class or directive from the DOM and allows your app to be visible.

###Explore more Now it’s your turn to add a feature to the app. Pretend you’re cooking for a large party, and create a filter that will multiply all the amounts in the recipes.

Hint: You’ll have to change the recipe model to include a more full featured “Ingredient” field that contains an amount, and then possibly a unit, and finally, an item name. Next, add a “multiplier” input that will allow the user to double, triple, or quadruple the recipe. Lastly, write a custom filter that multiplies each amount by the number specified in the multiplier input.

Home | Prev

Clone this wiki locally