Search code examples
ajaxselectzend-frameworkzend-framework2zend-form

ZF2 - Populate Select with Ajax


I have my in ProductsForm.php:


    namespace Products\Form;

    use Zend\Form\Form;
    use Zend\Db\Adapter\AdapterInterface;
    use Zend\Db\TableGateway\TableGateway;
    use Zend\Db\Sql\Select;

    class ProductsForm extends Form {

        public function __construct(AdapterInterface $dbAdapter) {

            $this->adapter = $dbAdapter;

            parent::__construct('products');

            $this->add(array(
                'name' => 'state',
                'type' => 'select',
                'attributes' => array(
                    'class' => 'form-control',
                ),
                'options' => array(
                    'empty_option' => 'Select...',
                    'value_options' => $this->getStatesForSelect()
                ),
            ));

            $this->add(array(
                'name' => 'city',
                'type' => 'select',
                'attributes' => array(
                    'class' => 'form-control',
                ),
                'options' => array(
                    'empty_option' => 'Select...',
                    'value_options' => $this->getCitiesForSelect()
                ),
            ));

        }

        public function getStatesForSelect() {
            $dbAdapter = $this->adapter;

            $table = new TableGateway('states', $dbAdapter);
            $result = $table->select(function (Select $select) {
                $select->order('name DESC');
            });

            foreach ($result as $res) {
                $selectData[$res['id']] = $res['name'];
            }
            return $selectData;
        }

        public function getCitiesForSelect($state_id) {
            ??? :(
            select from cities where state_id = $state_id ?
        }

    }

I want to execute the getCitiesForSelect() only when the user select the "state"... and then fill it with values from database based on the state.id value

How can I do that?


Solution

  • First of all, do not put the methods getStatesForSelect and getCitiesForSelect in your form. Forms are part of the controller layer, every database request belongs in the model layer: http://framework.zend.com/manual/current/en/modules/zend.mvc.intro.html

    Second: If you did this, you can create an action that will return a json with the requested states and call it via ajax. To load everything properly and nice looking, you will have to make some changes to your form as well:

    $this->add(array(
        'name' => 'state',
        'type' => 'select',
        'attributes' => array(
            'class' => 'form-control',
            'id'    => 'select-state',
        ),
        'options' => array(
            'class' => 'state-option',
            'empty_option' => 'Select...',
        ),
    ));
    
    $this->add(array(
        'name' => 'city',
        'type' => 'select',
        'attributes' => array(
            'class' => 'form-control',
            'id'    => 'select-city',
            'style' => 'display:none',
        ),
        'options' => array(
            'empty_option' => 'Select...',
        ),
    ));
    

    I removed the methods getting your options from the database because, as mentioned, this should be the job of your model table. You set those options in the controller as NandaKumar already wrote:

    $states = $modelTable->getStatesForSelect();
    $form->get('state')->setValueOptions($states);
    

    But this will only fill the states, we need something to fill the cities as well! To do so, we will define an ajax action to get those:

    public function stateCitiesAction()
    {
        if(!empty($_SERVER['HTTP_X_REQUESTED_WITH']) 
             && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
            $stateId = $this->params()->fromRoute("id");
            // get your cities table
            // getCitiesForSelect() needs to return an array!
            $cities = $citiesTable->getCitiesForSelect();
            return new JsonModel($cities);
        } else {
            // return a 404 if this action is not called via ajax
           $this->getResponse()->setStatusCode(404);
            return NULL;
        }
    }
    

    The if statement is to make sure, this action is only accessible via ajax. If it is not, it will return a 404.

    I just assumed in this example, that the model actions will return the same arrays as your methods in the form did. Although you could argue about which level should transform the database result, the model itself or the controller. But to keep the example simple I did it this way.

    To resolve a JsonModel properly, you need to include the ViewJsonStrategy in your module.config.php. Otherwise you'd get an error because Zend would try to find a view script.

    'view_manager'    => array(
        //...
        'strategies' => array(
            'ViewJsonStrategy',
        ),
    ),
    

    Plus, we need to pass the id as well. A route param is the most common way to do this and we need to include this in our route definition. This code is not a final solution and should only show you, how you could do this in your config:

    'poducts' => array(
        'type'    => 'Segment',
        'options' => array(
            'route'       => '/products/[:controller][/:action][/:id]',
            'constraints' => array(
                'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
                'action'     => '[a-zA-Z][a-zA-Z0-9_-]*',
                'id'         => '[0-9]*',
            ),
            'defaults'    => array(
                '__NAMESPACE__' => 'Application\Controller',
                'controller'    => 'Index',
                'action'        => 'index',
            ),
        ),
    ),
    

    For more information about routing see: http://framework.zend.com/manual/current/en/modules/zend.mvc.routing.html

    Phew, we are almost done! The only thing left is the ajax call filling the city options. I did this via jQuery:

    $(document).ready(function () {
    
        $("#select-state").change(function () {
            var stateId = $(this).val();
            if(stateId !== "") {
                // you might want to change this
                var url = "products/index/state-cities/"+stateId;
                $.getJSON( url, function( data ) {
                    var options = "";
                    $.each(data, function(id, city) {
                        options += "<option value='" + id + "'>" + city + "</option>";
                    });
                    $("#select-city").html(options).fadeIn();
                });
            }
        });
    
    });
    

    Not that I don't know your routing and you probably need to change the url.