Saturday, December 1, 2012

Audit trail with Postgresql hstore data type

Making audit trail on Postgres is just a walk in the park with hstore data type.


test=# SELECT 'apple=>1,orange=>6,guava=>8'::hstore;
                  hstore                   
-------------------------------------------
 "apple"=>"1", "guava"=>"8", "orange"=>"6"
(1 row)



So what is hstore? This is the explanation from Postgres documentation:

This module implements the hstore data type for storing sets of key/value pairs within a single PostgreSQL value. This can be useful in various scenarios, such as rows with many attributes that are rarely examined, or semi-structured data. Keys and values are simply text strings.


To enumerate those key value pairs, use the each function:

test=# select * from each(
(SELECT 'apple=>1,orange=>6,guava=>8'::hstore));
  key   | value 
--------+-------
 apple  | 1
 guava  | 8
 orange | 6
(3 rows)


Do a light bulb lit up on your head? Yeah me too, I'm so using that hstore to do an audit trail on Postgres trigger. But before we head to that, please do note note that you cannot reduce an existing table columns to an hstore type directly:

test=# with sample_row as 
(select 'Lennon'::text as lastname, 'John'::text as firstname, 1940 as birth_year) 
select hstore(*) from sample_row;

ERROR:  function hstore() does not exist
LINE 1: ...n'::text as firstname, 1940 as birth_year) select hstore(*) ...
                                                             ^
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.

To reduce all the columns of a table row to one column, just pass the table name as the parameter to hstore:

test=# with sample_row as 
(select 'Lennon'::text as lastname, 'John'::text as firstname, 1940 as birth_year) 
select hstore(sample_row) from sample_row;
                             hstore                              
-----------------------------------------------------------------
 "lastname"=>"Lennon", "firstname"=>"John", "birth_year"=>"1940"
(1 row)

Better yet, give the table an alias:
test=# with sample_row as 
(select 'Lennon'::text as lastname, 'John'::text as firstname, 1940 as birth_year) 
select hstore(p) from sample_row p;
                             hstore                              
-----------------------------------------------------------------
 "lastname"=>"Lennon", "firstname"=>"John", "birth_year"=>"1940"
(1 row)


Now that the table columns is now reduced to an hstore data type. We could enumerate them now using each function:

test=# with sample_row as 
(select 'Lennon'::text as lastname, 'John'::text as firstname, 1940 as birth_year) 
select * from each((select hstore(p) from sample_row p));
    key     | value  
------------+--------
 lastname   | Lennon
 firstname  | John
 birth_year | 1940
(3 rows)


Armed with this knowledge, we could now proceed to make an audit trail for a table.


First, we create the audit trail table:

create extension hstore; 
create sequence group_op;

create table data_trail
(
data_trail_id serial not null primary key,
table_name text not null,
table_op text not null,
group_op bigint not null,
field_name text not null,
current_value text,
old_value text
);

Then create a sample table:
create table person
(
 person_id serial not null primary key,
 lastname text not null,
 firstname text not null,
 nickname text null,
 favorite_number int null
);

group_op is for grouping related operation on a table.


Now let's create the audit trail trigger, the insert trigger is the easiest of the bunch, here's how to handle INSERT audit:

create or replace function log_insert() returns trigger
as
$$
declare s bigint;
begin
 s := nextval('group_op');

 insert into data_trail(table_name, table_op, group_op, field_name, current_value)
 select TG_TABLE_NAME, TG_OP, s, x.key, x.value 
 from each(hstore(new.*)) as x;
 return new;
end;
$$ language 'plpgsql';

I prefer to use the new.* to new, so as to fully-qualify that reserved identifier, you may have a field named new in your table.

To wire that trigger on the table:
create trigger log_insert_trigger
after insert on product
for each row
execute procedure log_insert();

Finally to test:
insert into person(lastname,firstname,nickname, favorite_number) values
('lennon','john winston','john',default),
('mccartney','james paul','paul',default),
('harrison','george',default,default),
('starr','richard','ringo', 10);

Here's the content of person table:
test=# select * from person;
 person_id | lastname  |  firstname   | nickname | favorite_number 
-----------+-----------+--------------+----------+-----------------
         1 | lennon    | john winston | john     |                
         2 | mccartney | james paul   | paul     |                
         3 | harrison  | george       |          |                
         4 | starr     | richard      | ringo    |              10
(4 rows)

Here's the content of data trail:
test=# select * from data_trail;
 data_trail_id | table_name | table_op | group_op |   field_name    | current_value | old_value 
---------------+------------+----------+----------+-----------------+---------------+-----------
             1 | person     | INSERT   |        1 | lastname        | lennon        | 
             2 | person     | INSERT   |        1 | nickname        | john          | 
             3 | person     | INSERT   |        1 | firstname       | john winston  | 
             4 | person     | INSERT   |        1 | person_id       | 1             | 
             5 | person     | INSERT   |        1 | favorite_number |               | 
             6 | person     | INSERT   |        2 | lastname        | mccartney     | 
             7 | person     | INSERT   |        2 | nickname        | paul          | 
             8 | person     | INSERT   |        2 | firstname       | james paul    | 
             9 | person     | INSERT   |        2 | person_id       | 2             | 
            10 | person     | INSERT   |        2 | favorite_number |               | 
            11 | person     | INSERT   |        3 | lastname        | harrison      | 
            12 | person     | INSERT   |        3 | nickname        |               | 
            13 | person     | INSERT   |        3 | firstname       | george        | 
            14 | person     | INSERT   |        3 | person_id       | 3             | 
            15 | person     | INSERT   |        3 | favorite_number |               | 
            16 | person     | INSERT   |        4 | lastname        | starr         | 
            17 | person     | INSERT   |        4 | nickname        | ringo         | 
            18 | person     | INSERT   |        4 | firstname       | richard       | 
            19 | person     | INSERT   |        4 | person_id       | 4             | 
            20 | person     | INSERT   |        4 | favorite_number | 10            | 
(20 rows)

As we can see, all those related inserts can be identified by the group_op.


Now let's try to make an audit trail for UPDATE:
create or replace function log_update() returns trigger
as
$$
declare 
 s bigint;


begin
 s := nextval('group_op');

        
 with changes as
 (
  select n.key, n.value as new_value, o.value as old_value
  from each(hstore(new.*)) as n
  join each(hstore(old.*)) as o using(key)
  where n.value is distinct from o.value 
 )
 insert into data_trail(table_name, table_op, group_op, field_name, current_value, old_value)
 select TG_TABLE_NAME, TG_OP, s, key, new_value, old_value
 from changes

 union

 select TG_TABLE_NAME, TG_OP, s, key, n.value, o.value
 from each(hstore(new.*)) as n
 join each(hstore(old.*)) as o using(key)
 where 
  exists(select * from changes)
  and 
  key in 
   (select column_name 
   from information_schema.key_column_usage
   where constraint_name = 
    (select constraint_name 
    from information_schema.table_constraints 
    where (table_schema,table_name,constraint_type) = (TG_TABLE_SCHEMA,TG_TABLE_NAME,'PRIMARY KEY')
    )
   );
   
  
 return new;
end;
$$ language 'plpgsql';

We only log those columns that changed, and we also include the primary key regardless if the modified row had its primary key value changed or not, hence the need to use the information_schema.key_column_usage.

Then to wire the trigger on person table
create trigger log_update_trigger
after update on person
for each row
execute procedure log_update();

To test if our logic works, issue this command:
update person set lastname = 'lennon', firstname = 'john ono', nickname = 'john', favorite_number = 9 where person_id = 1;

We only change the firstname and favorite number, the lastname and nickname stayed the same:

Then check the audit trail:
test=# select * from data_trail;
 data_trail_id | table_name | table_op | group_op |   field_name    | current_value |  old_value   
---------------+------------+----------+----------+-----------------+---------------+--------------
             1 | person     | INSERT   |        1 | lastname        | lennon        | 
             2 | person     | INSERT   |        1 | nickname        | john          | 
             3 | person     | INSERT   |        1 | firstname       | john winston  | 
             4 | person     | INSERT   |        1 | person_id       | 1             | 
             5 | person     | INSERT   |        1 | favorite_number |               | 
             6 | person     | INSERT   |        2 | lastname        | mccartney     | 
             7 | person     | INSERT   |        2 | nickname        | paul          | 
             8 | person     | INSERT   |        2 | firstname       | james paul    | 
             9 | person     | INSERT   |        2 | person_id       | 2             | 
            10 | person     | INSERT   |        2 | favorite_number |               | 
            11 | person     | INSERT   |        3 | lastname        | harrison      | 
            12 | person     | INSERT   |        3 | nickname        |               | 
            13 | person     | INSERT   |        3 | firstname       | george        | 
            14 | person     | INSERT   |        3 | person_id       | 3             | 
            15 | person     | INSERT   |        3 | favorite_number |               | 
            16 | person     | INSERT   |        4 | lastname        | starr         | 
            17 | person     | INSERT   |        4 | nickname        | ringo         | 
            18 | person     | INSERT   |        4 | firstname       | richard       | 
            19 | person     | INSERT   |        4 | person_id       | 4             | 
            20 | person     | INSERT   |        4 | favorite_number | 10            | 
            21 | person     | UPDATE   |        5 | person_id       | 1             | 1
            22 | person     | UPDATE   |        5 | favorite_number | 9             | 
            23 | person     | UPDATE   |        5 | firstname       | john ono      | john winston
(23 rows)

As we can see, person_id #1's lastname and nickname columns are not logged even they are included in the UPDATE command. This is a desirable functionality on audit trail trigger as most ORMs merely set every fields of the table regardless if the table class' column(s) was changed or not.

And also, try to re-issue the same update command above, nothing will be logged on your audit trail table. The update trigger above was designed to handle that scenario.


Finally for the delete trigger:
create or replace function log_delete() returns trigger
as
$$
declare s bigint;
begin
 s := nextval('group_op');
 insert into data_trail(table_name, table_op, group_op, field_name, current_value)
 select TG_TABLE_NAME, TG_OP, s, x.key, x.value 
 from each(hstore(old.*)) as x
 where x.key in 
  (select column_name 
  from information_schema.key_column_usage
  where constraint_name = 
   (select constraint_name 
   from information_schema.table_constraints 
   where (table_schema,table_name,constraint_type) = (TG_TABLE_SCHEMA,TG_TABLE_NAME,'PRIMARY KEY')
   )
  );

 return old;
end;
$$ language 'plpgsql';


create trigger log_delete_trigger
after delete on person
for each row
execute procedure log_delete();

We just logged the key of the deleted row. All else are excluded. To test, issue this command:
delete from person where person_id = 1;

Output:
test=# select * from data_trail;
 data_trail_id | table_name | table_op | group_op |   field_name    | current_value |  old_value   
---------------+------------+----------+----------+-----------------+---------------+--------------
             1 | person     | INSERT   |        1 | lastname        | lennon        | 
             2 | person     | INSERT   |        1 | nickname        | john          | 
             3 | person     | INSERT   |        1 | firstname       | john winston  | 
             4 | person     | INSERT   |        1 | person_id       | 1             | 
             5 | person     | INSERT   |        1 | favorite_number |               | 
             6 | person     | INSERT   |        2 | lastname        | mccartney     | 
             7 | person     | INSERT   |        2 | nickname        | paul          | 
             8 | person     | INSERT   |        2 | firstname       | james paul    | 
             9 | person     | INSERT   |        2 | person_id       | 2             | 
            10 | person     | INSERT   |        2 | favorite_number |               | 
            11 | person     | INSERT   |        3 | lastname        | harrison      | 
            12 | person     | INSERT   |        3 | nickname        |               | 
            13 | person     | INSERT   |        3 | firstname       | george        | 
            14 | person     | INSERT   |        3 | person_id       | 3             | 
            15 | person     | INSERT   |        3 | favorite_number |               | 
            16 | person     | INSERT   |        4 | lastname        | starr         | 
            17 | person     | INSERT   |        4 | nickname        | ringo         | 
            18 | person     | INSERT   |        4 | firstname       | richard       | 
            19 | person     | INSERT   |        4 | person_id       | 4             | 
            20 | person     | INSERT   |        4 | favorite_number | 10            | 
            21 | person     | UPDATE   |        5 | person_id       | 1             | 1
            22 | person     | UPDATE   |        5 | favorite_number | 9             | 
            23 | person     | UPDATE   |        5 | firstname       | john ono      | john winston
            24 | person     | DELETE   |        6 | person_id       | 1             | 
(24 rows)



Finally, it maybe already obvious, but it's worth noting to point out that the above trigger is not tied to one table only:

create table product
(
 product_id serial not null primary key,
 product_name text not null,
 product_description text not null
);


create trigger log_insert_trigger
after insert on product
for each row
execute procedure log_insert();


create trigger log_update_trigger
after update on product
for each row
execute procedure log_update();



create trigger log_delete_trigger
after delete on product
for each row
execute procedure log_delete();



insert into product(product_name, product_description) values
('keyboard','interface between you and the machine'),
('mouse','interface between you and Angry Bird');


Output on audit trail:
            13 | person     | INSERT   |        3 | firstname           | george                                | 
            14 | person     | INSERT   |        3 | person_id           | 3                                     | 
            15 | person     | INSERT   |        3 | favorite_number     |                                       | 
            16 | person     | INSERT   |        4 | lastname            | starr                                 | 
            17 | person     | INSERT   |        4 | nickname            | ringo                                 | 
            18 | person     | INSERT   |        4 | firstname           | richard                               | 
            19 | person     | INSERT   |        4 | person_id           | 4                                     | 
            20 | person     | INSERT   |        4 | favorite_number     | 10                                    | 
            21 | person     | UPDATE   |        5 | person_id           | 1                                     | 1
            22 | person     | UPDATE   |        5 | favorite_number     | 9                                     | 
            23 | person     | UPDATE   |        5 | firstname           | john ono                              | john winston
            24 | person     | DELETE   |        6 | person_id           | 1                                     | 
            25 | product    | INSERT   |        7 | product_id          | 5                                     | 
            26 | product    | INSERT   |        7 | product_name        | keyboard                              | 
            27 | product    | INSERT   |        7 | product_description | interface between you and the machine | 
            28 | product    | INSERT   |        8 | product_id          | 6                                     | 
            29 | product    | INSERT   |        8 | product_name        | mouse                                 | 
            30 | product    | INSERT   |        8 | product_description | interface between you and Angry Bird  | 
(30 rows)

Thursday, November 29, 2012

Can your RDBMS do a convenient audit trail?

Can your RDBMS do this?

create table person
(
 person_id serial not null primary key,
 lastname text not null,
 firstname text not null,
 nickname text null,
 favorite_number int null
);


insert into person(lastname,firstname,nickname, favorite_number) values
('lennon','john winston','john',default),
('mccartney','james paul','paul',default),
('harrison','george',default,default),
('starr','richard','ringo', 10);

select skeys(hstore(p.*)) as field, svals(hstore(p.*)) as value from person p;



Output:

      field      |    value     
-----------------+--------------
 lastname        | lennon
 nickname        | john
 firstname       | john winston
 person_id       | 2
 favorite_number | 
 lastname        | mccartney
 nickname        | paul
 firstname       | james paul
 person_id       | 3
 favorite_number | 
 lastname        | harrison
 nickname        | 
 firstname       | george
 person_id       | 4
 favorite_number | 
 lastname        | starr
 nickname        | ringo
 firstname       | richard
 person_id       | 5
 favorite_number | 10
(20 rows)

hstore can be used as a convenient mechanism for audit trail

http://www.sqlfiddle.com/#!1/d5729/1


Saturday, November 24, 2012

Detect kissing and overlapping points in Highcharts

formatter: function() {
    
   // console.log(this);

   // Michael Buen is here                    
 
 var search = this.series.chart.series[0].data;

 var a = this.point;

 var overlapCount = 0;                    
 for(var i in search) {
        
  var b = search[i];
  var d = getDistance(a, b);
  
  // choose an overlap threshold
  var kiss = 5 * 2;
  var halfOverlap = 5;
  var fullOverlap = 2.5; 
  if (d <= kiss) {
   ++overlapCount;
  }
        
 }
 
      
 return this.x +' cm, '+ this.y +' kg' + a.plotX + ' <br/><b>Overlaps:</b> ' + overlapCount;
}



function getDistance(point1,point2)
{
  var xs = 0;
  var ys = 0;
  xs = point2.plotX - point1.plotX;
  xs = xs * xs;
  ys = point2.plotY - point1.plotY;
  ys = ys * ys;
  return Math.sqrt( xs + ys );
}



Live code: http://jsfiddle.net/HxTjK/

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.

Sunday, October 28, 2012

jQuery filter, promotes DRY principle

You have this:

<input type='radio' name='TravelToCountryId' value='PH'> Philippines
<input type='radio' name='TravelToCountryId' value='CA'> Canada
<input type='radio' name='TravelToCountryId' value='ZH'> China​​​​​​​​

...

var country = $('input[name=TravelToCountryId]');
    
$(country).change(function() {     
    alert($(this).val());
});





You want to set the default(say Canada) using this:

$('input[name=TravelToCountryId][value=CA]').prop('checked',true);


Though that works, but as an astute developer you are, you feel that you are violating DRY principle and noticed that you can re-use the country object.


To re-use, use filter function. filter and find functions are almost synonym, however don't confuse them, find is for getting the descendant elements of existing elements, filter is for getting the elements from existing elements based on filter.


Do this:

$(country).filter('[value=CA]').prop('checked',true);​

Tuesday, October 9, 2012

Set focus on input upon click on ng-repeat

Live test: http://jsfiddle.net/epAxT/5/


Model and Controller:

function TestController($scope) {
        
    // Models
    $scope.messages = 
    [
    { Message: 'Life is what happens to you...', By: 'John Lennon' },
    { Message: 'If you can\'t explain...', By: 'Albert Einstein' },
    { Message: 'The answer to life...', By: 'Douglas Adams' }
    ];
    
    
    $scope.currentMessage = null;
    
    // Controller's actions
    
    $scope.setCurrentMessage = function(msg) {
        $scope.currentMessage = msg;
    };

    $scope.isCurrentMessage = function(msg) {
        return msg == $scope.currentMessage;
    };
    
    $scope.addMessage = function() {
        var msg = {Message: 'blah', By: 'Anonymous'};
        $scope.currentMessage = msg;
        $scope.messages.push(msg);
    };
    
}
    
    


    
    
var appGreat = angular.module('AppGreat', []);

appGreat.directive('toFocus', function ($timeout) {
    return function (scope, elem, attrs) {
        scope.$watch(attrs.toFocus, function (newval) {
            if (newval) {
                $timeout(function () {
                    elem[0].focus();
                }, 0, false);
            }
        });
    };
});
​


View:
<br/>
<div ng-app='AppGreat' ng-controller='TestController'>
    
    <table>
        <thead>
            <tr>
                <th>Message</th><th>By</th>
            </tr>
        </thead>
        <tbody>
            <tr ng-repeat='m in messages'>
                <td width='200px'>
                    <div ng-hide='isCurrentMessage(m)' 
                         ng-click='setCurrentMessage(m)'>
                        {{m.Message}}
                    </div>
                    <div ng-show='isCurrentMessage(m)'>
                        <input ng-model='m.Message'
                               to-focus='isCurrentMessage(m)'
                               style='width: 100%' />
                    </div>
                    
                    
                </td>
                
                <td>
                    <div ng-hide='isCurrentMessage(m)' 
                         ng-click='setCurrentMessage(m)'>
                        {{m.By}}
                    </div>
                    <div ng-show='isCurrentMessage(m)'>
                        <input ng-model='m.By'/>                        
                    </div>
                </td>
            </tr>
        </tbody>
    </table>
    
    
    <input type='button' ng-click='addMessage()' value='Add Message'/>
    
</div>       ​