Monday, October 29, 2012

AngularJS: Write Less Code, Go Have Girlfriend Sooner

Programming is a craft that is notorious for not being too conducive for having a relationship. Aside from it having the stigma for being too geeky, it is notorious for siphoning all your precious time that could be otherwise spent on some important things in life, like being able to leave work on time and get the chance (before you can even get the chance, you have to get the time first) to date the girl you're dreaming of; or if you already are a family man, spend quality time with your family.


Let's look at sample code that could be shortened if we are using a good framework. First, the longer code:

http://jsfiddle.net/3Xtnd/

Countries <select id='TravelToCountryId'></select>
<br/>
Cities <select id='TravelToCityId'></select>​

...

$(function() { 
    
    // Models
    
    var countries = 
    [
        { 
            CountryId: 1, CountryName: 'Philippines',
            Cities :
            [
                { CityId: 1, CityName: 'Manila' },
                { CityId: 2, CityName: 'Makati' },
                { CityId: 3, CityName: 'Quezon' },            
            ]            
        },
        { 
            CountryId: 2, CountryName: 'Canada',
            Cities:
            [
                { CityId: 4, CityName: 'Toronto' },
                { CityId: 5, CityName: 'Alberta' },
                { CityId: 6, CityName: 'Winniepeg' },
            ]           
            
        },
        { 
            CountryId: 3, CountryName: 'China',
            Cities:
            [
                { CityId: 7, CityName: 'Beijing' },
                { CityId: 8, CityName: 'Shanghai' }
            ]           
        },    
    ];    
           
    
    // Controller that live two lives,
    // can't focus well on model :-)
    
    
    // Populate then wire event...
    
    var country = $('#TravelToCountryId');
    var city = $('#TravelToCityId');
    
    
    $.each(countries, function() {
        var option = $('<option />').val(this.CountryId).text(this.CountryName);
        country.append(option);
    });
    
    $(country).change(function() {
        filterCitiesByCountry();
    });
    
    // ...Populate then wire event
    
    
    // Init..
    var initialCountryId = countries[1].CountryId; 
    $(country).val(initialCountryId);
    filterCitiesByCountry();    
    // ...Init
    
    function filterCitiesByCountry() {
        var selectedCountryId = country.val();
        
        
        // filter works on all browsers, except <= IE8
        var countryObj = countries.filter(function(v) {
            return v.CountryId == selectedCountryId;
        })[0];
        

        var cities = countryObj.Cities;
                
        city.empty();
        
        $.each(cities, function() {
            var option = $('<option/>').val(this.CityId).text(this.CityName);
            
            city.append(option);
        });

        
    } // filterCitiesByCountry()
         
    
});      



Then let's use a framework that facilitates declarative programming and allows separation of concerns. Let's use an MVC framework for JavaScript, let's use AngularJS. Following is the equivalent AngularJS code, there's nothing in the code that deals directly with the UI. You'll notice that the code don't have anything that imperatively populates the HTML, most are done declaratively, thus making your code shorter. Barring HTML tags, jQuery approach took 88 lines of code, while AngularJS took 56 lines only. Lesser code, lesser to debug when something goes wrong, lesser time coding, lesser development time.

http://jsfiddle.net/nDw2Z/

<div ng-controller='TravelController' ng-init='init()'>
Countries 
    <select ng-model='TravelToCountryId'     
    ng-options='i.CountryId as i.CountryName for i in countries' 
    
    ng-init='filterCitiesByCountry()'
    ng-change='filterCitiesByCountry()'></select>
    
<br/>
Cities 
    <select ng-model='TravelToCityId'
    ng-options='i.CityId as i.CityName for i in citiesFromSelectedCountry'></select>
</div>

...

function TravelController($scope) {
    
    // Models
    
    $scope.countries = 
    [
        { 
            CountryId: 1, CountryName: 'Philippines',
            Cities :
            [
                { CityId: 1, CityName: 'Manila' },
                { CityId: 2, CityName: 'Makati' },
                { CityId: 3, CityName: 'Quezon' },            
            ]            
        },
        { 
            CountryId: 2, CountryName: 'Canada',
            Cities:
            [
                { CityId: 4, CityName: 'Toronto' },
                { CityId: 5, CityName: 'Alberta' },
                { CityId: 6, CityName: 'Winnipeg' },
            ]           
            
        },
        { 
            CountryId: 3, CountryName: 'China',
            Cities:
            [
                { CityId: 7, CityName: 'Beijing' },
                { CityId: 8, CityName: 'Shanghai' }
            ]           
        },    
    ];    
    
    $scope.TravelToCountryId = null;
    $scope.TravelToCityId = null;
    
    
    // Controller's actions
    
    $scope.init = function() {
        $scope.TravelToCountryId = $scope.countries[1].CountryId;    
    };
        
    
    $scope.filterCitiesByCountry = function() {
               
        var cities = $scope.citiesFromSelectedCountry = $scope.countries.filter(function(v){
            return v.CountryId == $scope.TravelToCountryId; 
        })[0].Cities;
        
        $scope.TravelToCityId = cities[0].CityId;                
        
    };
                
}

The user decided that the program could be more appealing and user-friendly by making the country selection done via radio button since there are only few of them to select from. This user requirement necessitates changing your code as the dropdown list and radio button have different mechanism to carry information; on dropdown list it comes from option tags, and on radio button it is directly on input's value attribute. Dropdown list's mechanism for setting the default value is a mere .val(valueHere), while radio button need to find the object and then set its checked attribute.


http://jsfiddle.net/KhANV/

Countries <div id='divTravelToCountryId'></div>
<br/>
Cities <select id='TravelToCityId'></select>​

...

$(function() { 
    
    // Models
    
    var countries = 
    [
        { 
            CountryId: 1, CountryName: 'Philippines',
            Cities :
            [
                { CityId: 1, CityName: 'Manila' },
                { CityId: 2, CityName: 'Makati' },
                { CityId: 3, CityName: 'Quezon' },            
            ]            
        },
        { 
            CountryId: 2, CountryName: 'Canada',
            Cities:
            [
                { CityId: 4, CityName: 'Toronto' },
                { CityId: 5, CityName: 'Alberta' },
                { CityId: 6, CityName: 'Winnipeg' },
            ]           
            
        },
        { 
            CountryId: 3, CountryName: 'China',
            Cities:
            [
                { CityId: 7, CityName: 'Beijing' },
                { CityId: 8, CityName: 'Shanghai' }
            ]           
        },    
    ];    
           
    
    
    // Controller that live two lives,
    // can't focus well on model :-)
    
    // Populate then wire event...
    
    var divCountry = $('#divTravelToCountryId');
    
        
    $.each(countries, function() {
        
        var option = $(
            '<input type="radio" ' + 
            ' id="TravelToCountryId"' + 
            ' name="TravelToCountryId" />').val(this.CountryId);
        
        divCountry.append(option).append(' ' + this.CountryName).append('<br/>');
        
    });
    
    var country = $('input[name=TravelToCountryId]');
    var city = $('#TravelToCityId');
    
    
    $(country).change(function() {     
        filterCitiesByCountry();
    });
    
    // ...Populate then wire event
    
    
    // Init...
    var initialCountryId = countries[1].CountryId;     
    $(country).filter('[value="' + initialCountryId + '"]').prop('checked',true);        
    filterCitiesByCountry();
    // ...Init
    
            
    function filterCitiesByCountry() {
                
        var selectedCountryId = $(country).filter(':checked').val();
                   
        
        // filter works on all browsers, except <= IE8
        var countryObj = countries.filter(function(v) {
            return v.CountryId == selectedCountryId;
        })[0];
        

        var cities = countryObj.Cities;
                
        city.empty();
        
        $.each(cities, function() {
            var option = $('<option/>').val(this.CityId).text(this.CityName);
            
            city.append(option);
        });
        
    }
       
});      
​

Now let's try it on AngularJS: http://jsfiddle.net/ZjehV/

<div ng-controller='TravelController' ng-init='init()'>

Countries     
    <div ng-repeat='i in countries'>
    <input type=radio ng-model='$parent.TravelToCountryId'  
    name='TravelToCountryId'        
    ng-init='filterCitiesByCountry()'
    ng-change='filterCitiesByCountry()' value='{{i.CountryId}}' />&nbsp;{{i.CountryName}}
    </div>    
        
<br/>
Cities <select id='TravelToCityId'
    ng-model='TravelToCityId'
    ng-options='i.CityId as i.CityName for i in citiesFromSelectedCountry'
    ></select>
    
</div>
​
...

function TravelController($scope) {
    
    // Models
    
    $scope.countries = 
    [
        { 
            CountryId: 1, CountryName: 'Philippines',
            Cities :
            [
                { CityId: 1, CityName: 'Manila' },
                { CityId: 2, CityName: 'Makati' },
                { CityId: 3, CityName: 'Quezon' },            
            ]            
        },
        { 
            CountryId: 2, CountryName: 'Canada',
            Cities:
            [
                { CityId: 4, CityName: 'Toronto' },
                { CityId: 5, CityName: 'Alberta' },
                { CityId: 6, CityName: 'Winnipeg' },
            ]           
            
        },
        { 
            CountryId: 3, CountryName: 'China',
            Cities:
            [
                { CityId: 7, CityName: 'Beijing' },
                { CityId: 8, CityName: 'Shanghai' }
            ]           
        },    
    ];    
    
    $scope.TravelToCountryId = null;
    $scope.TravelToCityId = null;
    
    
    // Controller's actions
    
    $scope.init = function() {
        $scope.TravelToCountryId = $scope.countries[1].CountryId;    
    };
        
    
    $scope.filterCitiesByCountry = function() {
               
        var cities = $scope.citiesFromSelectedCountry = $scope.countries.filter(function(v){
            return v.CountryId == $scope.TravelToCountryId; 
        })[0].Cities;
        
        $scope.TravelToCityId = cities[0].CityId;        
                
    };
            
}
​


Let's measure the level of effort that was spent between the two when you accomodate changes in user requirement. jQuery's lines of code have increased from 88 to 97, while AngularJS stays the same, 56. So what changed on those 56 lines? Zero, zilch, nada. When you come to think of it, even your user request another changes on your UI, be they wanted to select from ul+li, table+td, div+a and whatnot, there should be no changes on your code to make that happen. Your code should be able to reflect only how your business operates, it should be devoid of idiosyncrasies of whatever UI choice imposes.


As an exercise, please try to convert jQuery version to use radio buttons too for city selection. Here's the AngularJS changes: http://jsfiddle.net/DL74y/, it has no changes on code, only the tags were changed.


With AngularJS or any decent MVC framework for javascript, you'll be able to accomplish more with lesser amount of effort. With AngularJS, your code can work exclusively on how your business operates, it doesn't have to pay attention on how the UI will be presented and used. User interface is a very fickle business, and you don't want your code to be impacted too much whenever some change is requested.



A minor aside on AngularJS, you cannot directly use ng-model='TravelToCountryId' when it's inside an ng-repeat, as different TravelToCountryId will be allocated on each element repetition, think of variable inside a loop. To prevent that from happening, make sure to reference the parent scope of TravelToCountryId, you can do that by prefixing the variable name with $parent.



The actual slogan of AngularJS: Write less code, go have beer sooner.

No comments:

Post a Comment