Monday, September 17, 2012

"cmtypes[st] is undefined" and other quick (and dirty) fixes for jqGrid working with a treetable

This is jqGrid version 4.4.0

1. If you tried to create a tree table, and it didn't work giving an error "cmtypes[st] is undefined" on line 1513 of jquery.jqGrid.src.js:


$(ts).jqGrid("SortTree", st, ts.p.sortorder, cmtypes[st].stype, cmtypes[st].srcfmt);

change this line to the following:

$(ts).jqGrid("SortTree", st, ts.p.sortorder, sorttype, srcformat);

It is working after that.

2. Expanding tree nodes with dynamically loaded local data.

If you create a tree grid with local data, you would notice that even if you don't expand nodes, it still creates hidden rows with this data. This causes a problem of unnecessarily slow loading if you have a lot of data but only want to expand tree nodes on demand, when a user clicks on a node.

First of all, don't load data for hidden rows initially. Second, override the click event. This event in the grid wasn't assigned by the "live" method, but when creating the rows, so we need to change it at the time when loading the data:


    grid[0].addJSONData({
           ...
    });
            
    $("div.treeclick").off("click").on("click", onExpand);

The onExpand function will be the following:

var onExpand = function(e) {
    var target = e.target || e.srcElement;
    var rowid = $(target).closest("tr").attr("id");
    var record = grid.getRowData(rowid);
    if(record.isLeaf == 'false' || !record.isLeaf) {
        if(record.expanded == 'true' || record.expanded == true) {
            grid.jqGrid("collapseNode", record);
            grid.jqGrid("collapseRow", record);
            record['expanded'] = false;
            grid.jqGrid("setRowData", rowid, record);
        } else {
            if(grid.jqGrid("getNodeChildren", record).length > 0) {
                grid.jqGrid("expandNode", record);
                grid.jqGrid("expandRow", record);
                $("tr[id^='"+rowid+"_']").show();
            } else {
                expand(rowid);
            }
            record['expanded'] = true;
            grid.jqGrid("setRowData", rowid, record);
        }
    }
    return false;
}

"expand" is your function to get new data to expand the node.

Please, note that we manually change the grid row to collapsed and expanded as, unfortunately, jqGrid stores node data in 2 separate places, so the functionality is not very consistent.

When we initially build a node, we don't load it as a leaf because we're going to expand it(the icon wouldn't be a leaf). Unfortunately, jqGrid would go into an infinite clicking cycle (would trigger an event) if we don't make this node a leaf. So, make it temporary a leaf when expanding it! The function (a hack, as jgGrid stores data in 2 different places) is the following:

    //hack: set the parent as a leaf to prevent triggering another click
    function setLeaf(id, isleaf) {
        var record = grid.getRowData(id);
        var _index = grid.jqGrid("getGridParam", "_index");
        var data = grid.jqGrid("getGridParam", "data");
        data[_index[id]]["isLeaf"] = isleaf;
    }

Then setting the node data will look the following:

function setNode(rowid, gridData) {    
    //hack: set the parent as a leaf to prevent triggering another click
    function setLeaf(id, isleaf) {
        var record = grid.getRowData(id);
        var _index = grid.jqGrid("getGridParam", "_index");
        var data = grid.jqGrid("getGridParam", "data");
        data[_index[id]]["isLeaf"] = isleaf;
    }
        
    setLeaf(rowid, true);
    
    gridData.forEach(function(node) {
        var notleaf = !node['isLeaf'] || (node['isLeaf'] == 'false');
        grid.jqGrid("addChildNode", node["id"], node["parent"], node);
        setIcon(node["parent"], true);
        var record = grid.getRowData(node["id"]);
        if(notleaf) {
            record['isLeaf'] = false;
            record['expanded'] = false;
            grid.setRowData(node["id"], record);
            setIcon(node["id"], false, onExpand);
        }
    });
    return false;
}

The last piece will be getting the icons and handlers rights with all the messy business with the leaves-nonleaves. My own settings in jqGrid definition for the tree icons are: triangles for a parent, and no icon for a leaf.

treeIcons: {plus:'ui-icon-triangle-1-e', minus:'ui-icon-triangle-1-s', leaf:'ui-icon-none'}


I had to add my own css for no icon (in the css file):

.ui-icon-none {
    background-position: -250px -250px; 
}

So my function to get the icons right is the following:
//hack to get the icons right
function setIcon(id, expanded, clickhandler) {
    var _index = grid.jqGrid("getGridParam", "_index");
    var data = grid.jqGrid("getGridParam", "data");
    var $icon = $("tr[id='"+id+"']").find("div.ui-icon");
    if(expanded) {    
        $icon.removeClass("ui-icon-triangle-1-e");
        $icon.removeClass("tree-plus");
        $icon.addClass("ui-icon-triangle-1-s");
        $icon.addClass("tree-minus");
    } else {
        $icon.removeClass("ui-icon-triangle-1-s");
        $icon.removeClass("tree-minus");
        $icon.addClass("ui-icon-triangle-1-e");
        $icon.addClass("tree-plus");
    }
    if(clickhandler != undefined) {
        $icon.on("click", clickhandler);
    }
}

Hopefully, that's it for now!

Friday, June 22, 2012

A gem to create objects from external APIs

I published a gem to load into objects XML and JSON files provided over external APIs (urls). The gem is called api_object and available to be installed over "gem install api_object".

The gem is very easy to use.

  1. subclass your object to be loaded from ActiveApi::ApiObject
    class Station < ActiveApi::ApiObject 
    end 

  2. Specify the url to load the data from, optionally an action and a mode, an api key and parameters(options) for the url; such as the url would look like
    "http://<api_url>/<action>?&key=<api_key>&<parameter1=value1&parameter2=value2...>"

    This will be defined in the top object over the function "initialize_from_api". Options for this function:

    :url - specify url
    
    :action - specify action
    
    :mode - specify mode (such as 'verbose', 'terse' etc.)
    
    :key - api key
    
    :data_tags - specify tag parameters under which object data might be stored, for example <location value='San Francisco'> - "value" would be a data tag. :data_tags accepts a single value or an array. 
    
    :url_options - parameters
    

    The following is designed to generate real time departure estimates for BART stations:

    class Station
    
    initialize_from_api :url => "http://api.bart.gov/api/", :action => 'etd.aspx', :key => 'MW9S-E7SL-26DU-VV8V', :url_options => {:cmd =&gt; 'etd'}
    
    end
    

    In this example, the url generated to get real time departure estimates from the Richmond station will be: http://api.bart.gov/api/etd.aspx?cmd=etd&orig=RICH&key=MW9S-E7SL-26DU-VV8V

  3. Define class attributes and mapping of the attributes to the api where the api name is different. To define api simple type mappings, use "api_column <attribute name>, <api attribute name>". To define api association mapping, use "api_association <association attribute name>, <api attribute name>, :as => <association class name>". Either the second, or the third parameters could be omitted. If the third parameter is omitted, the class name will be the same as the attribute name.

    In the following example, a simple attribute name is "abbreviation", but the name defined in the api XML documents is "abbr". An association is defined in the attribute :est, the api mapping is :etd and it's an object of the class Estimate.

    class Station < ActiveApi::ApiObject 
    
    initialize_from_api :url => "http://api.bart.gov/api/", :action => 'etd.aspx', :key => 'MW9S-E7SL-26DU-VV8V', :url_options => {:cmd => 'etd'}
    
    attr_reader :name, :abbreviation, :date, :time, :est
    
    api_column :abbreviation, :abbr
    api_association :est, :etd, :as => Estimate
    
    end
    

  4. To load api data into an object, use the class method "get_results(options)". In the example, get real time estimates for the station Glen Park.

    data = Station.get_results(:orig => 'GLEN')
    

    Please, note that data loaded might be either a hash, or an array of hashes, depending on the api.

  5. Create an object from the data

    If the example, the data received is a hash, so create an object.

    station = Station.new(data)
    

    If the data is an array of hashes, then it might be used to create an array of objects

    stations = data.map {|d| Station.new(d)}
    

  6. The gem also allows to get location based data by ip. Please, refer to the documentation.

Saturday, March 31, 2012

Initialize Objects from a hierarchy of hashes (JSON format) in Ruby

There are some nice little functions to initialize Objects in Ruby from a hash. A good example could be found here.

But sometimes, when we read an external API (XML or JSON) and parse the result, we get data which is a set of nested hashes and arrays. Then we want to initialize an object in our application with this data. I wrote the following module to do that.

init_from_hash.rb
-----------------------------
module InitFromHash
  
  def initialize(*args)
    args.first.each do |k, v| 
      unless defined?(k).nil?    # check if it's included as a reader attribute
        result = v.instance_of?(Array) ? v.inject([]) {|arr, v1| arr << init_object(v1, k)} : init_object(v, k)
        instance_variable_set("@#{k}", result)
      end
    end if (args.length == 1 && args.first.is_a?(Hash)) 
  end
  
  def init_object value, klass
     value.instance_of?(Hash) ? (get_module_name + klass.to_s.capitalize).constantize.new(value) : value
  end
  
  def get_module_name
    (self.class.name =~ /^(.+::).+$/) ? $1 : ''
  end
    
end


Assumptions:
1. Our object also contains nested objects. For example, we have an author who has articles, and each article has comments.
2. Names of our classes match the keys in our data.
3. All the classes of objects to be initialized have the same module name.
4. We want only attributes specified in the reader_attr to be present

Example of usage:
Module Blog
  class Author
      include InitFromHash
      attr_reader :name, :article
  end
end

module Blog
  class Article
      include InitFromHash
      attr_reader :name, :abstract, :comment
  end
end

module Blog
  class Comment
      include InitFromHash
      attr_reader :text
  end
end


When we receive our data in the form:
data = {"author" => 'Mike', "article" => [{"name" => "Is there life on Mars",
"abstract" => "Probably", "comment" => [{"text" => "Really?"},
{"text" => "No way!"}]}, {"name" => "Milk is good for you", 
"abstract" => "Scientists discovered", "comment" => [{"text" => "Really?"},
{"text" => "No way!"}] }}


Then we can initialize our instance as simple as
author = Author.new(data)

Tuesday, February 28, 2012

Starting with Node.js - short introduction with resources

What is Node.js? It's a Javascript library run on the V8 engine, the same one used in Google Chrome; and it's used to create a server on a fly to run web apps.

Advantages of it:
- it's slim and fast
- it's event driven

1. First question I asked was where Node.js is useful.

Since Javascript is event driven, it's useful when a server action should be triggered on a user action. If we consider just a request/response model, then the response it returned to this particular user only. But sometimes we want updates for all the users, such as in a chat room or in an interactive game. Then we can take advantage of the Node.js event driven nature.

2. Installation

Follow those instructions to install Node.

3. Eclipse plugin for Node.js development

I'm an ex Java developer so I like using Eclipse for everything. VJET plugin can be downloaded from here . It's a plugin to develop Javascript projects. Don't forget to install the Node.js and other Javascript type libraries you might need.

I didn't find a way to start a server in Eclipse (is Javascript still considered front end only?), so just open a terminal, go to your workspace directory, then to your project and start your server from there.

4. Trying it out

There is a great beginner book to start with Node. Please, click and read the links provided in the text, they give extended information on the methodology.

5. Trying it out more

Build a simple chat using Node.js