AngularJS UI-Select2 And The Web API Service

Let me tell you a little story. It goes a little something like this. Once there was a deprecated control that didn’t work quite the way it should have. It could do everything that I ask of it, just not quite in the same way that I wanted it to. If I asked it to make a soup it was always too salty. When I asked it to clean up the dishes, it never rinsed off all the bits off first before loading them in the dish washer.image

The worst of the offenses was when I wanted the query method to stop pestering the server with requests every time someone pressed a key.
“You can achieve this if you just use my Ajax interface”, it would say. “But, I don’t want to use that interface”, I would say. “I can’t unit test that interface as easily as I can the query interface”. On and on it would go, until the young, and good looking, Code Prince came up with an idea!
Since you are just like any other angular directive , I can just extend you with a service. That is, of course, if I just don’t rewrite your directive completely in the first place! No longer will I have to be satisfied with the job you’re doing currently, when I can bend you to my will. A grin set upon the Prince’s face as he hatched a plan. He would write some code to allow him to make the control behave. He knew that he could!

Now that he though up a plan, it was time to set about doing the actual work. Here is what he did!

Requirements

Now before we begin to show his plan, you are going to need a few things. Here is a list of the requirements that you will need if you’re going to follow along at home.

Basic Select 2 Sample

Let me start with just your basic UI-Select2 example and elaborate from there. Below is a simple example of the app.js file, that will allow you to pull up staff members that have been defined in another JavaScript file.

var app = angular.module("myApp", ['ui.select2', "testController", '$scope', function(scope) {
	//	some code here
	scope.loadCounter = 42;
	scope.select2Value = '';
	scope.minLength = 5;
	scope.waitMs = 3000;
	
	scope.select2Options = {
		minimumInputLength: 3,
		multiple: false,
		closeOnSelect: false,
		query: function(options) {
			var data = { results: allStaffMembers, more: false };
			options.callback(data);
		}
	};
}]);

Demo-straight Bad Query Design

The above test code just pulls from a static list. But what if you want to call the server to get a list of staff members? After all no REAL WORLD application keeps all of its staff members in an array on the client side. For this we need to create a call to the server side. Let’s do this in the next example.

var app = angular.module("myApp", ['ui.select2', "testController", '$scope', '$http', function(scope, http) {
	//	some code here
	scope.loadCounter = 42;
	scope.select2Value = '';
	scope.minLength = 5;
	scope.waitMs = 3000;
	
	scope.select2Options = {
		minimumInputLength: 3,
		multiple: false,
		closeOnSelect: false,
		query: function(options) {
			var data = { results: [] };
			http.get('api/getStaff/' + query.term)
			    .then(function(response) {
                                for (i = 0; i < response.data.length; i++) {
                                     data.results.push({ 
                                            id: response.data[i].id, 
                                            text: response.data[i].name 
                                       });
                                 };
                                 query.callback(data);
                           }, // Error Handling );
		}
	};
}]);

This is bad because now I have to code this every time I want to implement the UI-Select2 control somewhere. This will lead to brittle code and cause you to spread the URL calls all throughout the code. No no no, we can do better.

Create Service

We are going to create a service to do all of our heavy lifting. Here is what I put together to do all of my work so that I can keep my controller code very simple. I will call this service the webApiFunctions service.

//  This file will contain functions to communicate with the server on behalf of the ui-select2 control.
app.service('webApiFunctions', ['$http', '$timeout', '$q', function(http, ngTimeout, ngQ) {

    function isNullOrUndefined(value) {
        return (value == null || typeof(value) == "undefined");
    }

    function isNullOrEmpty(value) {
        if (isNullOrUndefined(value) || value.trim().length == 0)
            return true;

        return false;
    };

    function executeQuery(uri, searchOptions, searchTerm, queryResultFn, excludeFn) {        
        var deferred = ngQ.defer();
        http.get(uri, { cache: false })
            .then(function(response) {
                var queryResult = queryResultFn(response, excludeFn);
                deferred.resolve(queryResult);
            });

        return deferred.promise;
    };

    var nameIdSearchResultFn = function(response, excludeFn) {
        var nameIdSearchResult = {
            results: _.reject(_.map(response.data.results, function(item) { 
                return { id: item.id, text: item.name }
            }), excludeFn),
            more: false //  Prevent the drop down from loading more data automatically
        };

        return nameIdSearchResult;
    };
    
    var nameIdResultFn = function(response, excludeFn) {
        var nameIdsResult = {
            results: _.reject(_.map(response.data, function(item) { 
                return { id: item.id, text: item.name }
            }), excludeFn),
            more: false //  Prevent the drop down from loading more data automatically
        }
        return nameIdsResult;
    };

    this.nameIdSearchResultFunc = nameIdSearchResultFn;
    this.nameIdResultFunc = nameIdResultFn;

    var timeoutPromise;

    this.select2QueryFunc = function(searchOptions) {
        if (isNullOrEmpty(this.queryUri))
            throw 'The queryUri property of the search options MUST be specified before you can use this control';
        
        var excludeFunc = isNullOrUndefined(this.excludeMethod) ? function(p) { return false; } : this.excludeMethod;
        var quietMillis = isNullOrUndefined(this.quietMillis) ? 0 : this.quietMillis;
        var minimumInputLength = isNullOrUndefined(this.minimumInputLength) ? 1 : this.minimumInputLength;
        var queryResultFunc = isNullOrUndefined(this.resultMethod) ? nameIdSearchResultFn : this.resultMethod;

        var searchTerm = searchOptions.term.replace(/[^a-z, 0-9]/gi, '').trim();
        if (isNullOrEmpty(searchTerm))
            return;
        if (searchTerm.length < minimumInputLength)
            return;

        var searchUri = (this.queryUri + searchTerm);
        if (quietMillis === 0) {
            executeQuery(searchUri, searchOptions, searchTerm, queryResultFunc, excludeFunc)
                .then(function(result) { searchOptions.callback(result); }, // Error Handling );
            return;
        }

        if (timeoutPromise)
            ngTimeout.cancel(timeoutPromise);

        timeoutPromise = ngTimeout(function() {
            executeQuery(searchUri, searchOptions, searchTerm, queryResultFunc, excludeFunc)
                .then(function(result) { searchOptions.callback(result); }, // Error Handling );
        }, quietMillis);
    }
}]);

Create Unit Test

Now that we have created all of this functionality as an Angular Service we can replace it with a mock. This has the added benefit of allowing us to test the functionality instead of calling the server side, or mocking the server side calls. This should enable you to create Karma tests that will test all of your controllers functionality. I will create a blog post in the future that will demonstrate karma tests in more detail.

Conclusion

Now our story has a happy ending, the prince is now happy again. He has a control that now does what he needed it too. It doesn’t even tip out the glass of milk when I ask it what time it is anymore. Picture the robot with a wrist watch. The prince learned a lot on this journey, and I hope you did too.

The Fear Of The Machine Is Fear Of Ourselves.

See you next time…

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s