Multiple Combo boxes with YUI and CakePHP
Sunday, January 17th, 2010As 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…‘.
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.
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.
// 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:
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.
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:
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.



January 19th, 2010 at 9:59 am
[...] This post was mentioned on Twitter by YUI Library, Les Green. Les Green said: New blog post: Multiple Combo boxes with YUI and CakePHP http://grasshopperpebbles.com/ajax/multiple-combo-boxes-with-yui-and-cakephp/ [...]
January 23rd, 2010 at 5:16 am
Interesting, I’ll use it in one project (it is allready done),but idea with json responses is better than my current realisation.
Regards,
Milen