Monday, March 12, 2012

Dojo: Implementing a ViewModelStore

In the 1.6 release of Dojo, a new Store API was released with the intentions of improving on the design of the Data API. While I agree with most of the new Store API design, there is one feature of the Data API that I make significant use of and it was not carried forward into the Store API. The missing feature is the data structure that is used to initialize the store. In this post I will discuss why I find it important and create a store that uses the legacy structure and also implements the new Store API.

I use a data store as a page view model providing the needed data to the widgets on the page. The data has many references and I want to be able to query the store for any object within it. The legacy Data API uses a flat list of items. Any references to other items are identified using a "_reference" attribute. When the store initializes, it will replace the "_reference" object with the actual object. This allows for a normalized model with all objects queryable in the store.

The Data

For this example, the view model contains multiple orders. I want to be able to create a combobox with all of the customers and a combobox with all possible order statuses. The orders that are shown should match the customer and order status selected in the comboboxes.

var dataArr = [
  { id: 'customer::1', _type: 'customer', 
    name: 'Anderson, Jose',
    address: { _reference: 'address::1' } 
  },
  { id: 'address::1', _type: 'address', 
    address1: '2272 Stratford Drive', 
    address2: 'Honolulu, HI 96814' 
  },
  { id: 'customer::2', _type: 'customer', 
    name: 'Gomez, Marcus',
    address: { _reference: 'address::2' } 
  },
  { id: 'address::2', _type: 'address', 
    address1: '2718 College Avenue', 
    address2: 'Dayton, OH 45459' 
  },

  // ... other customers ...

  { id: 'product::1', _type: 'product', 
    name: 'Ball Original Classic Pectin Small Batch', 
    price: 1.07 
  },
  { id: 'product::2', _type: 'product', 
    name: 'Ball 4-Pk 16 Oz. Wide Mouth Class Canning Jars with Lids', 
    price: 4.48 
  },

  // ... other products ...

  { id: 'orderStatus::1', _type: 'orderStatus', 
    name: 'Pending', displayOrder: 10 
  },
  { id: 'orderStatus::2', _type: 'orderStatus', 
    name: 'Back Ordered', displayOrder: 30 
  },
  { id: 'orderStatus::3', _type: 'orderStatus', 
    name: 'Shipped', displayOrder: 20 
  },
  { id: 'order::1', _type: 'order', orderNumber: 1234, 
    customer: { _reference: 'customer::1' }, 
    orderStatus: { _reference: 'orderStatus::1' },
    lineItems:[
        { _reference: 'orderLine::1' }, 
        { _reference: 'orderLine::2' }
    ]
  },
  { id: 'orderLine::1', _type: 'orderLine', 
    order: { _reference: 'order::1' }, 
    price: 1.07, quantity: 10, 
    product: { _reference: 'product::1' } 
  },
  { id: 'orderLine::2', _type: 'orderLine', 
    order: { _reference: 'order::1' }, 
    price: 4.48, quantity: 1, 
    product: { _reference: 'product::2' } 
  },

  // ... other orders ...
];

The ViewModelStore

The ViewModelStore extends the MemoryStore and the core component is the implementation of setData. Here, each item in the array is registered. After an item is registered and if previous objects contain a reference to the object, a setter function will be called and the reference to the newly registered object will be set. Using the data above as an example, after registering the address object, a previously registered setter function will be called to set the address property of the customer to this address object.
setData: function(data){
    ...
    this.index = {};
    
    // when a reference to an item has not been registered,  
    // a setter function is added and will be called when 
    // the item is registered. 
    this._referenceSetters = {};
    
    // loop through the flat list of items
    dojo.forEach(this.data, function(itm, idx) {
        
        var itmId = itm[this.idProperty]; 
        this.index[itmId] = idx;
        
        for(var prop in itm) {
            this._registerItem(itm, prop); 
        }
        
        // process the queue of deferred setters for this object.
        if (this._referenceSetters[itmId]) {
            dojo.forEach(this._referenceSetters[itmId], 
              function(fnSet) { fnSet(itm); }
            );
            this._referenceSetters[itmId] = null;
        }
        
    }, this);
    
    delete this._referenceSetters;
}
Registering an item determines if it is an array or not. If so, it will call the inspectValue function for each item of the array, otherwise it is called for the item itself. The inspectValue function determines if it is a reference to another object in the data. If it isn't, then there is nothing more to do. If it is a reference and the referenced item has already been registered, then that item replaces the object identified by the reference. If the item has not been registered, then a function to set the value is registered to be called when the reference is registered.
_inspectValue: function(val, fnSetter) {
    
    var refProperty = this.referenceProperty || '_reference';
    
    // not a 'referece', so return the value.
    if (!val || !dojo.isObject(val) || !val[refProperty]) {
        fnSetter(val);
        return; 
    }
    
    var refId = val[refProperty];
    var refItm = this.get(refId);
    
    // the referenced item has already been loaded, return it. 
    if (refItm) {
        fnSetter(refItm); 
        return;
    }
    
    //we don't have the reference yet, qo queue a deferred setter that 
    //will set the value once the item is registered.
    if (! this._referenceSetters[refId]) {
        this._referenceSetters[refId] = [];
    }
    this._referenceSetters[refId].push(fnSetter); 
},

Using the ViewModelStore

The code snippet below creates two comboboxes and populates them using the data from the store. The onChange events are then wired to a function that queries the store using the filters specified in the comboboxes and renders the orders. If a new status is to be added later, it simply has to be put into the view model and the page will handle it accordingly.
var customer = new ComboBox({
    store:  store,
    query:  { _type: 'customer' },
    labelAttr: 'name'
}, dojo.byId('customerNode'));
customer.startup();

var status = new ComboBox({
    store:  store,
    query:  { _type: 'orderStatus' },
    labelAttr: 'name'
}, dojo.byId('statusNode'));
status.startup();

var fnDisplayOrders = function() {
    
    var contentNode = dojo.byId("content");
    var displayedOrder = false;
    domConstruct.empty(contentNode);
    
    var result = store.query({ 
        _type:          'order',
        customer:       customer.get('item'),
        orderStatus:    status.get('item'),
    });
    
    result.forEach(function(order) {
        displayedOrder = true;
        var div = dojo.create('div', {}, contentNode);
        dojo.addClass(div, 'order');
        
        var header = dojo.create('div', {}, div);
        dojo.addClass(header, 'orderHeader');
        header.innerHTML = dojo.replace(
          'Order #: {orderNumber}<br/>{customer.firstName}
          {customer.lastName}<br/>{customer.address.address1}
          <br/>{customer.address.address2}',
          order );
        
        ... more rendering logic ...
    });
    
};

dojo.connect(customer, 'onChange', fnDisplayOrders);
dojo.connect(status, 'onChange', fnDisplayOrders);
The complete example can be found at https://gist.github.com/2010137

Monday, March 5, 2012

Dojo Configurations

When using the Dojo Toolkit in an application, I setup the application so that one of three different configurations can be used.  There is a production, an uncompressed, and a debug configuration. Each of these configurations can be used to load the Dojo Toolkit and any custom javascript.  

The production configuration uses the output of the Dojo build. The javascript files are minified and combined into a single file. This is the default configuration and used by default. Since this code is minified, it is very hard to debug.

As part of the Dojo build process, a second set of files are created. These files end in .uncompressed.js. The uncompressed configuration uses these files. It is still a single file for javascript and loads fast, but the javascript code is human readable. This makes it easier to step through code using a browser's javascript debugger. While it is easy to debug, it still requires a build cycle to deploy changes.

The third configuration is the debug configuration. This configuration points directly at the source code and does not use the build process. This is noticeably slower when loading pages because the browser is loading many files but this configuration doesn't require a build to see changes made to source.

When an application is deployed, the debug configuration is not made available. The production configuration is the default and having the uncompressed available allows a developer to troubleshoot a problem on an end user's machine. The debug configuration is valuable when developing a significant piece of javascript and you do not want to be continually building to see your changes.


Implementation

I use a query string parameter to specify a different configuration. This requires server side code to interpret the query string value and swap out a configuration. You may find it useful for the server code to place the value in the session. Doing so allows yout to navigate to differen
t pages without continuing to modify the query string.

A simplified folder structure is:

static
    dojo-release
        dojo
            dojo.js
            dojo.js.uncompressed.js
        myDojoCode
            custom-dojo.js
            custom-dojo.js.uncompressed.js
    dojo-source*
        dojo
            dojo.js
        myDojoCode
            [a bunch of custom javascript files]

* only exists on development machines


The HTML output when using the production configuration would look something like:

<script type="text/javascript" src="/static/dojo-release/dojo/dojo.js" djConfig="parseOnLoad: false"></script>
<script type="text/javascript" src="/static/dojo-release/myDojoCode/custom-dojo.js"></script>

The uncompressed like this:

<script type="text/javascript" src="/static/dojo-release/dojo/dojo.js.uncompressed.js" djConfig="parseOnLoad: false"></script>
<script type="text/javascript" src="/static/dojo-release/myDojoCode/ custom-dojo.js.uncompressed.js"></script>

The debug like this:

<script type="text/javascript" src="/static/dojo-source/dojo/dojo.js" djConfig="parseOnLoad: false, debug: 'true'"></script>

dojo.js is the core dojo kernal and custom-dojo.js contains the auxiliary javascript code in a single file.  The source of this code exists in multiple files in the source folder.  In the debug configuration, we do not include an all encompassing file because we want Dojo to load the individual source files.