DayPath Journal

Design Diary: my small Angular JS 1.x patterns

My recent work with the redesign/upgrade of kintespace.com leaves me with a few Angular JS patterns that must be memorialized here, literally for my own (mental) health.

Using a data Service with dirt-simple caching based on $q

‘Dirt-simple’ caching is a system that loads data once and stores the data until the system restarted. I am sure there is a more academic name for this design—nevertheless, this is what ‘dirt-simple’ caching looks like as an Angular service:

What we see in the gist is an Angular service with three methods: cacheData(), getDataFromCache(), and loadIndex(). This service is from an Angular app displays Index data so loadIndex() is called every time the App is loaded—and every time the Index partial View is requested.

These two lines of code represent the “secret sauce” of this dirt-simple design:

var cachedResponse = this.getDataFromCache(uri);
return cachedResponse? $q.when(cachedResponse) : $http.get(uri).then(…);
    

That last line—that ternary operation featuring $q.when() (from Angular JS) makes things dirt simple.

Using ngView and custom directives

I started using ngView to avoid delving into building my own directives. In “My Angular JS 1.x single-page layout,” I am making myself familiar with the need to use very simple custom directives—typically header and footer directives, wrapping ngView—to give me a little headroom in layout expressiveness. Here’s an example of a header Directive:

var doHeaderDirective = function () {
    return {
        restrict: "E",
        scope: false,
        templateUrl: "./app/partials/headerFlow.html"
    };
};
    

Using a Client View Model to provide binding for a Directive outside of ngView scope

The header Directive shown above, has its scope option set to false (which means it will inherit its Controller scope). But when the header is loaded outside of ngView what controller is associated with it? To answer this question, I’ve developed this pattern using ngController:

<!DOCTYPE html>
<html data-ng-app="rxApp">
<head>…</head>
<body>
<div class="container" data-ng-controller="clientController">
    <rx-header />
    <div class="row" data-ng-view="">…</div>
</div>
<rx-footer />
</body>
</html>
    

I can define View Model inside of clientController that can be used for data binding, etc. outside of ngView. This may be obvious to many Angular folks but I can see how a beginner can fall in the trap of thinking one should use ngView or ngController instead of both of them.

Using Angular UI pagination with Underscore-JS sorting

My little gist about paging and sorting shows key fragments of the design, featuring a Pagination Service driven by a Controller that uses Underscore JS to sort the data before passing it to this service. We of course see the pagination through the markup in the partial, documented on GitHub.

The Pagination Service has only one expectation for the data it uses: the data must be an array. The start() method starts pagination and it called from the controller:

$scope.clientVM.dataService.loadData("index-" + indexMetaId).then(function (response) {
    that.data = _(response.data.ChildDocuments)
        .chain()
        .filter(function (i) {
            return (!_.isUndefined(i) && !_.isNull(i));
        }).sortBy(function (i) {
            return i.CreateDate;
        })
        .value()
        .reverse();
    that.pagination.start(that.data);
    that.pagination.isVisible = true;
    $scope.clientVM.isDataLoaded = true;
    $scope.clientVM.isSplash = false;
});
    

Using ngClass, $parent, $first and the Client View Model with ngRepeat

The import discovery for me here is ngClass. I feel like I should have learned about ngClass before I started building Angular JS sites—this is a super-easy way to associate CSS class names with Controller logic (it is effectively the equivalent of .addClass(), .hasClass(), .removeClass(), .toggleClass() in jQuery).

This gist sketches out how a repeated set of headers, associated with ngView routes, changes CSS classes based on the route:

The use of $parent in the partial implies that the partial is loaded in ngView and ClientVM is the Client View Model of the $parent scope ‘above’ the controller of the ngView. (See “Using a Client View Model to provide binding for a Directive outside of ngView scope” above.)

The use of $first in the markup is passed to the isFirst parameter of clientVM.isIndexSubsetHeaderSelected(). It is used to make a default selection for initial load.

Using a custom function for a filter with nqRepeat

This declaration refers to a function, vm.filterGroups():

data-ng-repeat="i in groups | filter:vm.filterGroups
    

In this particular case, the filter function is part of a View Model that is entirely devoted to filtering:

$scope.vm = {
    filterExpression: null,
    filterGroup: function (data) {
        var filterExpression = $scope.vm.filterExpression;
        if (!filterExpression) {
            return true;
        }
        filterExpression = filterExpression.toLowerCase();
        var title = data.Title;
        var isContainedInTitle = (title && title.toLowerCase().indexOf(filterExpression) === -1) ? false : true;
        return isContainedInTitle;
    },
    filterGroups: function (data) {
        if (!data.group) {
            return true;
        }
        var filteredItems = _(data.group).filter($scope.vm.filterGroup);
        var hasGroupItems = (filteredItems && (filteredItems.length > 0)) ? true : false;
        return hasGroupItems;
    },
};
    

The filterExpression property is bound to an input[type="text"] element:

<input data-ng-model="vm.filterExpression" type="text" class="form-control" placeholder="search">
    

The Angular documentation clearly specifies the use of a “predicate function” for filter expressions.