Showing posts with label AJAX. Show all posts
Showing posts with label AJAX. Show all posts

Thursday, 20 March 2014

CakePHP-2 and AJAX The dependent list boxes problem (remake)

Forward

Back in 2010, I had written a post regarding the case of dependent list boxes in a CakePHP view. Back at those days CakePHP was at version 1.2 and support for Javascript and AJAX was very limited. Today I shall revise this using JQuery and JSON encoding which will make things simpler and easier to understand, implement and maintain.

The long story short

Suppose you have a page with two list boxes. One contains a standard set of values while the second one's list of values must be dynamically updated depending on the actual selected value of the first.

In our (sort of) real life example we have a list of commissions (aka production orders) that produce a series of products of varying lengths. The list of lengths that each commission is allowed to produce is available via a detail table and our goal is write an addProduct action and view that allows the user to specify the commission based on which the actual product was produced and the actual length of the product that should be one of the assigned commission lengths. The Product model has a commission_id and an actual_length fields. So each time the commission combo box changes the actual length field input options should also change in order to contain the commissions list of allowed lengths.

Getting Started

To begin with our tutorial make sure that your standard layout references the jQuery library. The easiest was to do this would be to open APP/View/Layout/default.ctp and make sure that a line like

<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>

can be found somewhere in your html's head section.

The next thing that needs to be done is to add the RequestHandler component in our AppController. Add or modify your existing source so it looks more or less like this.

class AppController extends Controller {
    public $components = array(
        'RequestHandler', 
        ...
    );

    ,,,
}

Next we need to inform our routing system that it should also parse json URLs. So the line

Router::parseExtensions('json');

should be added to the APP/Config/routes.php file right below the other Router::xxx() commands.

Create the AJAX method and view to return the JSON encoded list of lengths

The next thing to do is create the action that will return the list of lengths to be sent back to the view given commission id. The best place to put it will be the commissions controller. The code for the function looks a lot like the one baked by view. We just need to make sure that the data returned "contain" the correct detail information

    public function getRequestedLengths($id)
    {
        if (!$this->Commission->exists($id)) 
            throw new NotFoundException(__('Invalid record'));
        
        $options = array(
            'conditions' => array(
                'Commission.' . $this->Commission->primaryKey => $id
            ),
            'contain' => array(
                'CommissionProduct',
            )
        );
        $this->set('commission', $this->Commission->find('first', $options));
    }

The view for the method should be placed in: APP/View/Commissions/json/get_requested_lengths.ctp.

<?php
/*
 * Create and echo a json encoded list of the allowed lengths
 ^/
$output = array();
if (!empty($commission['CommissionProduct']))
    foreach ($commission['CommissionProduct'] as $commissionProduct)
        $output[] = $commissionProduct['actual_length'];


echo json_encode($output);

So now if we point our browser towards http://yourserver/projectPath/Commissions/getRequestedLengths/7.json, we will receive a response with a JSON array containing all allowed lengths for commission id 7. The actual response will be something like ["45900","23400"].

Building the addProduct view

So far we have the mechanism to retrieve the required data. The final step will be to use it in the actual add product view, the PHP part of which should look more or less like this:

<div class="products form">
    <?php echo $this->Form->create('Product'); ?>
        <fieldset>
            <legend>Add Product</legend>
            <?php echo $this->Form->input('commission_id', array('empty' => __('Please select a commission'))); ?>
            <?php echo $this->Form->input('operator_id'); ?>
            <?php echo $this->Form->input('shift'); ?>
            <?php
                echo $this->Form->input(
                'status',
                array(
                    'type' => 'select',
                    'options' => $statusList
                )
                );
            ?>
            <?php echo $this->Form->input('actual_length', array('type' => 'select')); ?>
            <?php echo $this->Form->input('gross_weight'); ?>
        </fieldset>
    <?php echo $this->Form->end('Save'); ?>
</div>

The final part will be the adding of Javascript code to make our form responsive.

<script type="text/javascript">
    var commissionsCombo;
    var allowedLengthsCombo;

    jQuery(function() {
        commissionsCombo = jQuery('#ProductCommissionId');
        allowedLengthsCombo = jQuery('#ProductActualLength');

        commissionsCombo.change( function() {
            var selectedCommission = this.value;  // or $(this).val()
            // build the url that contained the selected commission code
            var ajaxUrl =
                    '<?php echo Router::url(array('controller' => 'Commissions', 'action' => 'getRequestedLengths', 'admin' => FALSE), TRUE)?>'
                    + '/'
                    + selectedCommission
                    + '.json';

            // do a "synchronous" AJAX call
            jQuery.ajax({
                    type:'GET',
                    async: false,
                    cache: false,
                    url: ajaxUrl,
                    success: function(response) {
                        // remove all options from the allowed metres per bobbin
                        allowedLengthsCombo.find('option').remove();

                        // add all returned values
                        for (var i = 0; i < response.length; i++) {
                            var currentKey = response[i];
                            var currentKeyDescr = response[i] + ' metres';
                            var optionText = '<option value="'
                                    + currentKey
                                    + '">'
                                    + currentKeyDescr
                                    + '</option>';
                            allowedLengthsCombo.append(optionText);
                        }
                    }
            });
        });
    });
</script>

As a last statement The code for manipulating the select input options comes from the very consise post from StackOverflow.com

Thursday, 23 May 2013

CakePHP and AJAX submitting a form with jQuery

A couple of years back I wrote an article about how to handle the dependent drop down lists problem using CakePHP's Ajax facilities. Today i will put down a trivial example of how to submit a CakePHP created form using jQuery as a small reference that can be easily pasted.

Let me remind you of the CakePHP ajax way. You start by creating a controller method that will "return" the ajax content. For our trivial example, the following controller will be more than enough.

class AjaxController extends AppController {
   var $uses = NULL;

   public function helloAjax()
   {
       $this->layout='ajax';
       // result can be anything coming from $this->data
       $result =  'Hello Dolly!';
       $this->set("result", $result);
   }
}

The corresponding view file view/ajax/hello_ajax.ctp should contain just the following:

<?php echo $result; ?>

Setting up our Ajax call is now as easy as, creating a link or a button that will invoke the asynchronous call and then setting the id of the field that will receive the result. A typical setup would be that the link looks something like this :

<a href="#" id="performAjaxLink">Do Ajax </a>

And then the target field can be created using:

<?php echo $this->Form->input('your_field', array('id' => 'resultField')); ?>

Finally a little script at the end of the file ...

<script>
    jQuery("#performAjaxLink").click(
            function()
            {                
                jQuery.ajax({
                    type:'POST',
                    async: true,
                    cache: false,
                    url: '<?= Router::Url(['controller' => 'ajax','admin' => FALSE, 'action' => 'helloAjax'], TRUE); ?>',
                    success: function(response) {
                        jQuery('#resultField').val(response);
                    },
                    data:jQuery('form').serialize()
                });
                return false;
            }
    );
</script>

The jQuery Ajax API is available here.

Thursday, 7 January 2010

CakePHP: The dependent listboxes problem

Note: This example is outdated. I have a newer blog post that addresses the same issue using more up to date methods and functionality.

When creating dynamic web-sites, sooner or later you are going to face the problem of providing dependent list boxes. Imagine the case where one wishes to select a city from a list grouped by region, or a user from a group, an invoice from an order etc. Grouping things into categories is very common in real life and as far as I am concerned, it is almost always a must for your application to be able to utilize such groupings.
In CakePHP -- as far as I know -- there are two ways you can help your users pick up a value from a list of grouped items. One is to create a select box whose items are organized in selection groups sorted in some logical way. the other way in to use dependent AJAX triggered combo or list boxes where selection on the first will filter the items displayed on the second.
In this posting I will provide code that handles both cases.

The sample data

As an exercise for learning Cake, I developed a small application that manages the IT department books. Each Book belongs to a BookCategory and each BookCategory belongs to a BookCategoryGroup. Reversely, each BookCategoryGroup has many BookCategory and each BookCategory has many Book. The corresponding tables have foreign keys adhering to the CakePHP conventions, so I am not going to waste any more time explaining the data structure.
The goal here is to help our users, when adding or editing book records, to find the right category for each book given the organization of book categories in book category groups. Like I said in the introduction there are two ways we can accomplish this

One combo box organized in selection groups

The way is very easy to implement and may become particularly handy whenever the total number of list items is relatively small. Cake's Form::imput method will create option groups if the array containing the options for a select box is organized into sub arrays so if we add the following function in our BooksController ....
    private function prepareCategoriesCombo()
    {
        // gain access to the BookCategoryGroups model class
        $this->loadModel('BookCategoryGroup');
        // prepare a list of all book category groups
        $this->BookCategoryGroup->recursive = 0;
        $bookCategoryGroups = $this->BookCategoryGroup->find(
                                'all',
                                array(
                                    'conditions' => array(),
                                    'order' => array('BookCategoryGroup.name')
                                )
                            );

        // create an empty array to hold the combo box options
        $bookCategories = array();
        foreach( $bookCategoryGroups as $bookCategoryGroup) {
            $groupId = $bookCategoryGroup['BookCategoryGroup']['id'];
            $groupName = $bookCategoryGroup['BookCategoryGroup']['name'];
            // create a sub array for each group category
            $bookCategories[] = $groupName;
            // fill the array with the categories corresponding to the group
            $bookCategories[$groupName] = $this->Book->BookCategory->find(
                                'list',
                                array (
                                    'conditions' => array(
                                        'BookCategory.book_category_group_id' => $groupId
                                        ),
                                    'order' => array(
                                        'BookCategory.name'
                                        )
                                )
                                    );
        }
        return $bookCategories;
    }
Supposing that you have baked your original controller and view code with the cake script, your add() or edit() controller actions need to have the following in order to use the option grouped combo:
   ...
   $bookCategories = $this->prepareCategoriesCombo();
   ...
while the view template will require no change at all (i.e. a simple echo $form->input('book_category_id'); will suffice). As I said earlier on, this method is simple enough and unless you intent to let your users pick a US zip code organized by state, this may be the preferred solution for many cases. If however you have lots of data and an untamable desire for ajax, read on; fear not however CakePHP's approach to AJAX makes this look also like a piece of cake.

The AJAX way: Two combos with one auto filtering the other

The basic idea behind AJAX is the following: You start by defining an area in you web page identifiable via the the HTML id attribute. Then when the user clicks on a button or changes the value of some control (edit, list or combo), you make an asynchronous call to the web server -- that is without having to reload the page -- and the server returns HTML code that is ready to be placed inside that area. The actual way you implement this depends on the libraries and the AJAX framework you use. Cake does that using the prototype and the Scriptaculus frameworks.
So to get things started, download prototype and scriptaculus and place the following files in your APP/webroot/js folder:
builder.js   dragdrop.js  prototype.js      slider.js  unittest.js
controls.js  effects.js   scriptaculous.js  sound.js
Having done that, modify you application layout in order to include them. Open APP/view/layouts/default.ctp and change the HTML head part so it looks like this :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>
      <?php echo $title_for_layout; ?>
    </title>
    <?php echo $html->css('it-library'); ?>
    <meta name="Generator" content="Quanta Plus" />
    <meta name="Author" content="Thanassis Bakalidis" />
    <?php if (isset($javascript)) : ?>
      <?php echo $javascript->link('prototype.js'); ?>
      <?php echo $javascript->link('scriptaculous.js'); ?>;
    <?php endif; ?>
    <?php echo $scripts_for_layout; ?>
  </head>
The next thing to do is to modify our controller in order to provide Javascript and AJAX support: Our books controller now looks like the following:
class BooksController extends AppController {

    var $name = 'Books';
    var $helpers = array('Html', 'Form', 'Javascript', 'Ajax');
    var $components = array('RequestHandler');

    ...
}
Next go to your view template -- may that be the add or edit.ctp -- and change the initial echo $form->input('Book.book_category_id'); line in order to look like the following:
        // here are the two list boxes displaying groups and categories
        // aim is to create an auto-filter effect with AJAX
        echo $form->label( 'BookCategory.book_category_group_id',
                            'Category Group');
        echo $form->select('BookCategory.book_category_group_id',
                            $bookCategoryGroups,
                            $bookCategoryGroupId,
                            array(
                                'id' => 'bookCategoryGroups'
                            ),
                            FALSE);
        echo $form->input('Book.book_category_id', array('id' => 'bookCategories' ));

        // each time the bookCategoryGroups element changes we are to
        // asynchronously call the updateSelect action of the current 
        // controller and insert whatever the action produces inside the 
        // html DOM element identified by bookCategories
        $ajaxOptions = array('url' => 'updateSelect','update' => 'bookCategories');
        echo $ajax->observeField('bookCategoryGroups',$ajaxOptions);
I believe that the code is self explanatory. Now let us add the updateSelect method of the BooksController class
    function updateSelect()
    {
        $groupId = $this->data['BookCategory']['book_category_group_id'];
        if (!empty( $groupId )) {
            $options = $this->getBookCategoriesForGroup( $groupId);
            // these are the combo box options to be used in the view file
            $this->set('options',$options);
        }
    }
There is one thing to mention here: the AJAX code produced by observeField() serializes the entire field that is supposed to observe, so this will be available in the controller action as $this->data['Model']['field'].
Next we need to create the actual view code. Create a file named update_select.ctp inside your APP/views/books directory and place the following code inside (Thanks HerbCSO):
<?php
    // create  tags coming from a $options variable
    // This is to be used by AJAX in order to fill the contents of a combo
    // box
    if(!empty($options)) {
        foreach($options as $key => $value) {
             echo "<option value=\"$key\">$value</option>";
        }
    }
?>
Now we have everything in place. The only thing left to do is to initialize the two combo boxes so that they contain the correct data, i.e. all the group categories for the top combo and the categories for the selected records category group on the second, during initial page load. To achieve this I have created two additional functions in the BooksController class:
    private function getBookCategoriesGroups()
    {
        $this->loadModel('BookCategoryGroup');
        $this->BookCategoryGroup->recursive = 0;
        return $this->BookCategoryGroup->find('list',
                                                array(
                                                    'conditions' => array(),
                                                    'order' => array(
                                                        'BookCategoryGroup.name'
                                                        )
                                                )
                                            );
    }

    private function getBookCategoriesForGroup( $groupId)
    {
        return $this->Book->BookCategory->find('list',
                                array(
                                    'conditions' => array (
                                        'book_category_group_id' => $groupId
                                        ),
                                    'order' => array(
                                            'BookCategory.name'
                                        )
                                )
                            );
    }
Now my controller's edit action -- which as I mentioned earlier, was baked by cake -- looks like the following.
    function edit($id = null)
    {
        if (!$id && empty($this->data)) {
            $this->Session->setFlash(__('Invalid Book', true));
            $this->redirect(array('action'=>'index'));
        }
        if (!empty($this->data)) {
            if ($this->Book->save($this->data)) {
                $this->Session->setFlash(__('The Book has been saved', true));
                $this->redirect(array('action'=>'index'));
            } else {
                $this->Session->setFlash(__('The Book could not be saved. Please, try again.', true));
            }
        }

        if (empty($this->data)) {
            $this->data = $this->Book->read(null, $id);
        }

        // set up additional book record parameters
        $sites = $this->Book->Site->find('list');
        $bookTypes = $this->Book->BookType->find('list');
        $languages = $this->Book->Language->find('list');

        // setup the two AJAX operated combo boxes
        $bookCategoryGroups = $this->getBookCategoriesGroups();                
        $bookCategoryGroupId = $this->data['BookCategory']['book_category_group_id'];
        $bookCategories = $this->getBookCategoriesForGroup( $bookCategoryGroupId);

        $ratings = $this->Book->Rating->find('list');
        $publishers = $this->Book->Publisher->find('list');

        $this->set( compact( 'sites','bookTypes','languages',
                             'bookCategories', 'bookCategoryGroups', 'bookCategoryGroupId',
                             'ratings','publishers'));
    }
Needless to say that when adding a record, the initial $bookCategoryGroupId can be set to an initial value say 1 and then let your uses change to whatever seems appropriate.
I have tested this with CakePHP 1.2.5 on both Firefox (versions 3 and 3.5) and IE (version 8).
As a last statement, I would like to point out that I am by no means an expert on AJAX or CakePHP. I got my info from an earlier posting by DEVMOZ and the CakePHP AJAXHelper class info page. I have put this down as a working reference to a real problem, that anybody can copy -- hopefully -- easy to modify code.