Updating Ajax selects with symfony

Updating Ajax selects with symfony

February 17, 2012 by Cris

This tutorial shows how to make an AJAX script that dynamically changes some selects with Symfony.

For example, let’s suppose that a user wants to create a new product, inserting the name, the category and one or more subcategories.

The Schema and Data

The database is structured with Product, Category, Subcategory e SubcategoryPerProduct tables.

# config/doctrine/schema.yml

Product
:
  columns
:
    name
:          { type: string(255), notnull: true }
    category_id
:   { type: integer, notnull: true }
  relations
:
    Category
:      { local: category_id, foreignAlias: Products, onDelete: cascade}
   
Category
:
  columns
:
    name
:          { type: string(255), notnull: true }

Subcategory
:
  columns
:
    name
:          { type: string(255), notnull: true }
    description
:   { type: string(255) }
    category_id
:   { type: integer }
  relations
:
    Category
:      { local: category_id, foreignAlias: Subcategories, onDelete: cascade}

SubcategoryPerProduct
:
  columns
:
    subcategory_id
: { type: integer, notnull: true }
    product_id
:     { type: integer, notnull: true }
  relations
:
    Subcategory
:    { local: subcategory_id, foreignAlias: SubcategoriesPerProduct, onDelete: CASCADE }
    Product
:        { local: product_id, foreignAlias: SubcategoriesPerProduct, onDelete: CASCADE }

To bring the project to life, insert the following test data.

# data/fixture/fixture.yml

Category
:
  restaurant
:
    id
:          1
    name
:        Restaurant
  clothing
:
    id
:          2
    name
:        Clothing
   
Subcategory
:
  condition_1
:
    Category
:    clothing
    name
:        Shoes
    description
: Shoes donec sollicitudin, tortor at porttitor sollicitudin.
  condition_2
:
    Category
:    clothing
    name
:        Clothes
    description
: Clothes morbi eu turpis ac sapien auctor consequat.
  condition_3
:
    Category
:    clothing
    name
:        trousers
    description
: Trousers cras purus nulla, venenatis vel sodales vel, viverra nec diam.
  condition_4
:
    Category
:    restaurant
    name
:        pizzeria
    description
: Pizzeria phasellus vel ligula quis odio.
  condition_5
:
    Category
:    restaurant
    name
:        Sandwich shop
    description
: Sandwich shop morbi eu turpis ac sapien auctor consequat.

ProductForm

Analyzing the Product form, you will notice that subcategory selection checkboxes do not appear. To do this, we need to create a sfWidgetFormDoctrineChoice widget.

# lib/form/doctrine/ProductForm.class.php

class ProductForm extends BaseProductForm
{
  public function configure()
  {    
    $this->widgetSchema['subcategory'] = new sfWidgetFormDoctrineChoice(array(
      'model' => 'Subcategory',
      'add_empty' => false,
      'expanded' => true,
      'multiple' => true,
      'query' => SubcategoryTable::getInstance()->createQuery()
    ));
   
    $this->validatorSchema['subcategory'] = new sfValidatorDoctrineChoice(array(
      'required' => true,
      'model' => 'Subcategory',
      'multiple' => true,
      'query' => SubcategoryTable::getInstance()->createQuery(),
    ));    
  }
}

With this new widget, all subcategories are shown. To show only those relative to the selected category, let’s add a query in SubcategoryTable and modify the Product form.

# lib/model/doctrine/SubcategoryTable.class.php

public function findByCategoryQuery(Category $category)
{
    $q = $this->createQuery()
      ->where('category_id = ?', $category->getId());

    return $q;
}

# lib/form/doctrine/ProductForm.class.php

class ProductForm extends BaseProductForm
{
  public function configure()
  {    
    // Verify if category exists
    $category = $this->getObject()->getCategory();
    if (!$category->exists()) {
      $request = sfContext::getInstance()->getRequest();
      $params = $request->getParameter($this->getName());

      if ($params['category_id'])
        $category = CategoryTable::getInstance()->findOneById($params['category_id']);
      else
        $category = CategoryTable::getInstance()->findAll()->getFirst();
    }

    $this->widgetSchema['subcategory'] = new sfWidgetFormDoctrineChoice(array(
      'model' => 'Subcategory',
      'add_empty' => false,
      'expanded' => true,
      'multiple' => true,
      'query' => SubcategoryTable::getInstance()->findByCategoryQuery($category)
      ));
   
    $this->validatorSchema['subcategory'] = new sfValidatorDoctrineChoice(array(
      'required' => true,
      'model' => 'Subcategory',
      'multiple' => true,
      'query' => SubcategoryTable::getInstance()->findByCategoryQuery($category),
      ));
  }

  // This method save the subcategories
  protected function doUpdateObject($values)
  {
    $subcategories = $values['subcategory'];
    unset($values['subcategory']);
    parent::doUpdateObject($values);

    $product = $this->getObject();
    foreach ($subcategories as $key => $subcategoryId) {
      $subcategoryPerProduct = new SubcategoryPerProduct();
      $subcategoryPerProduct->setSubcategoryId($subcategoryId);
      $product->SubcategoriesPerProduct[$key] = $subcategoryPerProduct;
    }
  }
}

To complete the Product form, we can assign a class attribute to the category widget, and an id to the subcategories widget. We will need this for the jQuery script.

# lib/form/doctrine/ProductForm.class.php

class ProductForm extends BaseProductForm
{
  public function configure()
  {
      // ...

      $this->getWidget('category_id')->setAttribute('class', 'category');

      $this->widgetSchema['subcategory'] = new sfWidgetFormDoctrineChoice(array(
        'model' => 'Subcategory',
        'add_empty' => false,
        'expanded' => true,
        'multiple' => true,
        'query' => SubcategoryTable::getInstance()->findByCategoryQuery($category),
        'renderer_class' => 'myWidgetFormSelectCheckbox'
      ));

      // ...
  }
}

To assign the id to the subcategories widget, let’s create a class that inherits from sfWidgetFormSelectCheckbox, implementing the formatter method.

# lib/widget/myWidgetFormSelectCheckbox.class.php

class myWidgetFormSelectCheckbox extends sfWidgetFormSelectCheckbox
{
  public function formatter($widget, $inputs)
  {
    $rows = array();
    foreach ($inputs as $input) {
      $rows[] = $this->renderContentTag('li', $input['input'] . $this->getOption('label_separator') . $input['label']);
    }
    return !$rows ? '' : $this->renderContentTag('ul', implode($this->getOption('separator'), $rows), array('id' => 'subcategory', 'class' => $this->getOption('class')));
  }
}

Creating a “subcategory” module

This module defines an action to recover the list of subcategories, filtered by category, and that returns a JSON template. The JSON answer will be read by the AJAX script, in order to update the subcategory checkboxes.

# apps/frontend/config/routing.yml

list_by_category
:
  url
: /category/:id/subcategory.:sf_format
  param
: { module: subcategory, action: listByCategory }
  requirements
:
    sf_format
: (?:json)
# apps/frontend/modules/subcategory/actions/actions.class.php

class subcategoryActions extends sfActions
{
  public function executeListByCategory(sfWebRequest $request)
  {
    $this->subcategories = SubcategoryTable::getInstance()->findByCategoryId($request->getParameter('id'));
  }
}
# apps/frontend/modules/subcategory/templates/listByCategorySuccess.json.php
{
<?php $count = $subcategories->count(); $i = 0; ?>
<?php foreach($subcategories as $subcategory) : $i++ ?>
  "<?php echo $subcategory->getId()?>": { "title": "<?php echo $subcategory->getName()?>" }<?php echo $i == $count ? '' : ','?>  
<?php endforeach ?>
}

Retrieve subcategories with AJAX and jQuery

To complete our tutorial, we need to add a jQuery script that, when a category is selected, updates the subcategory checkboxes using AJAX.

# apps/frontend/modules/product/templates/newSuccess.php

// ...
<script type="text/javascript">
  $(function() {

    $('.category').change(function() {
      var chosenoption = this.options[this.selectedIndex];
      var conditionUrl = "/category/"+ chosenoption.value +"/subcategory.json";
     
      $.ajax({
        url: conditionUrl,
        dataType: "json",        
        success: function(choices) {
          if(choices) {
            $("#subcategory").hide();
            $("#subcategory").html('');
            var $subcategories = '';
            $.each(choices, function(index) {  
              $subcategories = $subcategories +
                "<li>\n\
                 <input id=\""
+ chosenoption.value + "_" + index + "\" type=\"checkbox\" multiple=\"multiple\" value=\"" + index + "\" name=\"product[subcategory][]\">\n\
                 <label for=\"product_subcategory_"
+ index + "\">" + this.title + "</label>\n\
               </li>"
;
            });
            $("#subcategory").html($subcategories);
            $("#subcategory").show();
          }
        }
      });
    });
  });  
</script>

Links

Symfony
jQuery

Related Posts

jQuery Text Slider Plugin

Portfolio effects in jQuery

This entry was posted in Software Development, Web Solutions and tagged , , . Bookmark the permalink.

Leave a Reply

7 + = eight