Multiple Combo boxes with YUI and CakePHP

As a developer, I hate writing similar code for the same process over and over again. Just like highly optimized database design, developers should always try to write code as efficiently as possible.

I’m working on a project where I use YUI and CakePHP to fill multiple combo boxes on a page using Json data. At first, I wrote separate code with the same functionality for each combo box. Every time I needed to change the functionality, I had to change it for each combo box. After doing this multiple times, I decided to combine the functionality on both the front-end (YUI) and the back-end (CakePHP).

The Back-end (CakePHP)

The key to creating the optimized code on the back-end is creating a method in 1 controller. The data for each combo box come from separate tables, so initially I was going to create a new method in each of the relevant CakePHP controllers. But if I decided to change the functionality, I would have to change it in each controller file. So I am creating the method only in the controller of the relevant view. In this case, I have a Requestors page (view), so the method is created in the requestors_controller.

function getComboData($cntrl=null, $id="id", $desc="name") {
        // set autoRender to false for Ajax requests
	$this->autoRender = false;
        // set debug to 0 so debug information is not sent back to the application
	Configure::write('debug', 0);
	$result = $this->Requestor->$cntrl->find('all', array('fields' => array($cntrl.".$id", $cntrl.".$desc"), 'recursive' => -1));
	return json_encode($result);
}

So I have a getComboData method that takes the controller, field id, and field description as parameters.

($cntrl=null, $id="id", $desc="name")

I set up the method with default values for id and description so that if you named your table fields using CakePHP’s naming conventions, you don’t have to pass these values.

Notice in the find all method: When calling different model within a CakePHP controller, you have to prefix it with the existing model. In this case, the controller model is Requestor.

$this->Requestor->$cntrl->find

If I passed Language as the controller, the statement would look like:

$result = $this->Requestor->Language->find('all', array('fields' => array(Language.id, Language.name), 'recursive' => -1));

For more information on creating ajax methods and CakePHP’s recursive option, read my post: YAHOO.util.Connect, CakePHP, Json Data.

The Front-end – YUI

Now that the back-end is complete, we will create the front-end code. Again, we want to optimize the code as much as possible. We know that we have multiple combo boxes on this page (view). We will use the Yahoo Connection Manager (Yahoo.util.connect) to retrieve the Json data.

First we create the combo boxes on the page:

<label for="state_id">State:</label><select name="state_id" id="state_id"></select>
 
<label for="country_id">Country of Origin:</label><select name="country_id" id="country_id"></select>
 
<label for="language_id">Native Language:</label><select name="language_id" id="language_id"></select>
 
<label for="cbxStaff">Staff</label><select name="cbxStaff" id="cbxStaff"></select>

Next I am going to create a multidimensional array that will store the combo box name/id, the CakePHP controller name, the id field name, and the description field name for each combo box. I also created an optional default value for the selected option of the combo box ‘Select…‘.

?View Code JAVASCRIPT
var Dom = YAHOO.util.Dom, $ = Dom.get;
var aCB = new Array();
var cbSelect;
// cbCount will be used to keep track of which
// combo box I'll be updating based upon 
// it's position in the arrray
var cbCount = 0;
aCB[0] = new Array ('language_id', 'Language', 'id', 'name', 'Select...');
aCB[1] = new Array ('country_id', 'Country', 'id', 'name', 'Select...');
aCB[2] = new Array ('state_id', 'State', 'id', 'name', 'Select...');
aCB[3] = new Array ('cbxStaff', 'User', 'id', 'last_name', 'Select...');

I created a shortcut for YAHOO.util.Dom and another for Dom.get. cbSelect will store the selected combo box.

We will loop through the aCB array and use Yahoo.util.connect to retrieve the Json data for each combo box.

?View Code JAVASCRIPT
var aCBLngth = aCB.length;
var sUrl, request;
// set cbSelect to the first combo box
// aCB[0][0] = language_id
// remember, we set $ to Dom.get
cbSelect = $(aCB[0][0]);
for (var i=0; i<aCBLngth; i++) {
	sUrl = 'getComboData/' + aCB[i][1] + '/' + aCB[i][2] + '/' + aCB[i][3];
	request = YAHOO.util.Connect.asyncRequest('GET', sUrl, callback);	
}

I build the url string as I iterate over the array. I’m adding the parameters needed for the getComboData in the Requestor controller. For instance, when i == 0, I retrieve the values for language.

?View Code JAVASCRIPT
// So:
sUrl = 'getComboData/' + aCB[0][1] + '/' + aCB[0][2] + '/' + aCB[0][3];
 
// Becomes:
sUrl = 'getComboData/Language/id/name';

Remember the parameters for the getComboData method:

getComboData($cntrl=null, $id="id", $desc="name")

Also, notice that I did not need the name of the controller in the url string. I was a bit surprised about this as well. You would think that url string would be:

?View Code JAVASCRIPT
sUrl = 'Requestor/getComboData/Language/id/name';

Apparently, because I am using the Requestor controller to view this page, CakePHP knows that it should look within the Requestor controller to find the getComboData method.

So now we create the callback methods for YAHOO.util.Connect.

?View Code JAVASCRIPT
var callback =	{
  	success:handleSuccess,
  	failure:handleFailure
};
 
var handleFailure = function(o){
// do something 			
}
 
var handleCmbSuccess = function(o){
	var response = [];
 
	if (o.responseText !== undefined) {
		try {
                        // parse the json data
	                response = YAHOO.lang.JSON.parse(o.responseText);
	            }
	            catch (x) {
	                alert("JSON Parse failed! " + o.responseText);
	                return;
	            }
                // get the total number of records
		var nL = response.length;
 
                 // if the aCB array has a default option for the combo box 'Select...',
                 // create a select option
                 // cbCount is currently 0 which corresponds to the language array
                 // aCB[0] = new Array ('language_id', 'Language', 'id', 'name', 'Select...'); 
		if (aCB[cbCount][4]) {
                        // create new option
			var opt = document.createElement("option");
			// set the value-attribute to 0:
			opt.setAttribute('value',0);
			// set the displayed value: 'Select...'
			opt.innerHTML = aCB[cbCount][4];
                        // cbSelect = $(aCB[0][0]) = the language_id combo box
			cbSelect.appendChild(opt);
		}
		// get the controller
                // cbCount = 0 (the language array)
                // aCB[cbCount][1] = 'Language'
		var cntrl = aCB[cbCount][1];
		for (var i=0; i<nL; i++) {
                        // create an option
			var opt = document.createElement("option");
			// set the value-attribute:
                        // the json data is in the following format:
                        // {"Language":{"id":"1","name":"Afrikaans"}},{"Language":{"id":"2","name":"Albanian"}},{"Language":{"id":"3","name":"Amharic"}}]
                        //  var cVal = response[0]['Language'][aCB[0]['id']] = 1
			var cVal = response[i][cntrl][aCB[cbCount][2]];
			opt.setAttribute('value', cVal);
 
			// set the displayed value:
                        // var cVal = response[0]['Language'][aCB[0]['name']] = 'Afrikaans'
			opt.innerHTML = response[i][cntrl][aCB[cbCount][3]];
			cbSelect.appendChild(opt);
		}
                // update cbCount 
		cbCount++;
                // if the cbCount is less than the number of records in the aCB array,
                // change cbSelect to the next combo box 
                if (cbCount < aCBLngth) {
                        // cbCount now = 1, so 
                        // aCB[1][0] = country_id 
			cbSelect = $(aCB[cbCount][0]);
		}
	}
}

I’m updating cbCount so that the next time the handleSuccess method is called, it will create options for the next combo box in the array. Remember, I am looping through the aCB array, so that the handleSuccess method is called for each index in the array:

?View Code JAVASCRIPT
for (var i=0; i<aCBLngth; i++) {
	var request = YAHOO.util.Connect.asyncRequest('GET', sUrl, callback);	
}

That’s it. Using YUI and CakePHP together to update multiple combo boxes may be a bit difficult, but at least I don’t have to write the same handleSuccess method for each combo box.

Be Sociable, Share!

Checkout My New Site - T-shirts For Geeks