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

2 comments :

Martin Winstrand said...

Tnax Thanassis!!!
You saved my day. This helped me ALLOT!!

Gian Vallejos said...

Thank you so much !!!