Data Model Architecture in Angular

This post highlights my AngularJS Portland presentation in Feb. 2015 about building a data model to handle difficult JSON. It’s used on the search landing pages at Bodybuilding.com that average 125,000 daily views.

You may have had the luxury of working with well-structured, concise JSON data, ready to be bound to $scope and rendered on page when building your Angular apps. For various UI components maybe the server’s JSON data is a single list of objects or a list of lists.

On a recent project using 3rd party web services, we weren’t so fortunate. The data set was very large at about 150KB of raw JSON, or about 14,000 lines, and filled with objects for multiple UI components. But more the data was in poor shape and needed refining. In this post I’d like to share our solution to dealing with difficult data.

Our search pages have several UI components on each page. Here’s one. We’re looking at store categories.

categorypic.png

Should be easy.

Our controller makes an $http request, assigns the response data to a $scope.property, and the View uses ng-repeat to iterate over the objects.

 Controller

app.controller('controller', function($scope, AjaxService) {
        'use strict';
        AjaxService.getCategories()
            .then(function() {
                $scope.categories = AjaxService.categories;
            });
    }
);

 AjaxService

app.service('AjaxService',  function($http) {
        'use strict';
        $http.get('/categories')
            .success(function(data) {
                AjaxService.categories = data;
            })
            .error(function() {
                // error
            });
    }
);

 JSON from server

{
    categories:  [
        {
            image: 'b-elite_food.jpg',
            title: 'B-Elite Fuel',
            description: 'You are elite.  This is your fuel.',
            type: 'brand',
            url: '/store/b-elite.htm'
        },
        {
            image: 'organic_foods.jpg',
            title: 'Organic Foods',
            description: 'Make healthy food and snack choices.',
            type: 'goal',
            url: '/store/healthy-snacks.htm'
        },
        ...
    ]
}

 View

<div ng-repeat="category in categories" class="category--3-col">
    <img ng-src="{{category.image}}" class="category__image">
    <div class="category__title">
        {{category.title}}
    </div>
    <div class="category__description">
        {{category.description}}
    </div>
    <a href="{{category.url}}" class="category__button">
        {{category.button}}
    </a>
</div>



Pretty easy, right? But what if the JSON data is complex?

 Complex JSON

In our case the JSON holds data for several UI components in our search pages, such as store categories, products, articles, etc. Being large and complex, it presented 2 challenges:

 Challenge #1:

Objects belonging to a single UI component were at times scattered across multiple lists in the JSON. It was the front-end’s job to insert these objects into one list and de-dup them for ng-repeat to iterate over.

 Challenge #2

A single object in the JSON could have upwards of 30 properties, and the data value was at times several properties deep within an object.

obj.propA.propB.propC.propD[0]

To show the problems with #2, I could push the data directly out to the View. It’ll look like this.
unrefinedcategory.png

Certainly not ideal. Imagine 1,000 lines of markup using this approach. Hard coding, JavaScript methods in View, lots of subscripts. This is hard to test and a pain to maintain.

So how do we work with this JSON?


 Solution: Build a Data Model

This is the “M” in Angular’s MV*. To build a Model in Angular we’ll use a collection of factories to build a service layer and a model builder layer. Categories will be our exampe.

Let’s start with our controller, not officially in the model but a good starting point. No surprises here.

The controller calls the DataService service layer for an unrefined list of categories. After the list is returned it’s passed to the ModelBuilder layer to refine the data.

/**
* Controller delegates request for list to DataService
* (solves #1).  If list not null, delegates building refined
* models to ModelBuilder (solves #2).
*/
app.controller('StoreController',
    function($scope, DataService, ModelBuilder, CategoryConfig) {
        'use strict';

        var categoryList = DataService.getCategoryList();
        if(categoryList !== null) {
            $scope.categories = 
                ModelBuilder.buildModelList(
                    categoryList, CategoryConfig);
        }
        ...
    }
);

DataService initiates the AJAX request, then traverses the JSON building a list for each UI type into a hash map of lists. In our case getCategoryList() returns the list of categories.

app.factory(‘DataService’, function(AJAXService) {
        'use strict';
        /** hashMap - each key refers to list */
        var hashMap = {};

        /**
        * Fetches JSON, traverses it to build hash map
        * of lists for each UI component from json data 
        * object from serve.  Assigns hashMap where 
        * each key refers to a list.
        */
        (function traverseJSON() {
            var json = AJAXService.makeRequest();
            /**
            * implementation specific
            */  
            hashMap = json;
        }());

        return {
            /**
            * @returns a list per UI type from hashMap
            */
            getCategoryList: function() {
                return hashMap.categories;
            },
            getProductList: function() {
                return hashMap.products;
            }
            ...
        };
    }
});

To aggregate objects of the same UI type into lists (e.g. categories, products, members) and de-dup them in the service layer will be implementation specific. In my case I had to convert trees to lists.

We solved Challenge #1. Next our controller passes the list to the model builder layer.

ModelBuilders job is to refine the model objects and return them to the controller, ready to be bound to the View. It’s passed an unrefined list of objects and config object to create concise keys on our model objects.

app.factory('ModelBuilder', function() {
        'use strict'; 
        return {  
            /**
            * For each item, iterates over config objects keys, 
            * invoking function, passing record and assigning 
            * returned value to model key. 
            * @param configObj - config object (e.g. categories)
            * @returns refined list of model objects
            */
            buildModelList: function(list, configObj) {
                var modelList = [];
                angular.forEach(list, function(item) {
                    var model = {}, key;

                    for(key in configObj) {
                        if(configObj.hasOwnProperty(key) &&
                            typeof configObj[key] === 'function'){
                            model[key] = configObj[key](item);
                        }
                    }

                    modelList.push(model);
                });
                return modelList;
            }
        }
    }
);

The category config object is the critical piece that maps the long JSON values to simple keys. It’s like a config file giving granular flexibility such as appending //store.bbcomcdn.com/ strings.

/**
* Defines a category model object. Converts long JSON properties 
* into concise keys for model objects. They prevent pushing long 
* JSON properties into View.
*/
app.factory('CategoryConfig',function() {
        'use strict';
        return {
            url: function(record) {
                return 'store' + record.properties['seoUrl'][0];
            },
            imagePath: function(record) {
                return '//store.bbcomcdn.com/' + 
                    record.properties['category.largeImage'][0];
            },
            title: function(record) {
                return record.label;
            },
            description: function(record) {
                return record.properties["description.en_US"][0];
            },
            buttonText: function(record) {
                return record.properties.parentDimension;
            }
        };
    }
);

It’s easy to scale additional UI components by adding config objects, such as products and articles. This technique fixes #2 by turning the long, hard to test properties into simple targeted ones for our View.

 Outcome: View

Drum roll.

We turned this View
unrefinedcategory.png

into

<div ng-repeat="category in categories" class="category--3-col">
    <a ng-href="{{category.url}}" class="category__image-cont">
        <img ng-src="{{category.image}}" class="category__image">
    </a>
    <div class="category__title">
        {{category.title}}
    </div>
    <div class="category__description">
        {{category.description | Truncate}}
    </div>
    <a href="category.url" class="category__button">
        {{category.buttonText}}
    </a>
</div>

This View renders our category UI component.

categorypic.png

We have view models with concise and refined properties. Much better than having deep properties and business logic as shown above.

Our 2 challenges are solved. We

  1. Grouped objects into lists using hash map with each key referring to a UI type (categories, products, members) in DataService.
  2. Collapsed our multi-level object properties into simple ones via ModelsFactory, ModelBuilder, and CategoryConfig.

I hope this post was useful. I’d enjoy your feedback. Feel free to reach out on LinkedIn.

 
213
Kudos
 
213
Kudos

Now read this

Designing Normalized SQL Tables

SQL databases are reliable for complex queries, partial updates, transactions, and decoupling data modeling from application specific contexts. In this post we’ll cover how to normalize tables in 1NF, 2NF, and 3NF. We have a table. Name... Continue →