Explorar o código

Merge pull request #2454 from pulzarraider/autocomplete_filter

Added support for model autocomplete filter
Andrej Hudec %!s(int64=10) %!d(string=hai) anos
pai
achega
97dfc2f1ef

+ 1 - 5
Admin/Admin.php

@@ -1914,11 +1914,7 @@ abstract class Admin implements AdminInterface, DomainObjectInterface
     }
 
     /**
-     * Returns a filter FieldDescription
-     *
-     * @param string $name
-     *
-     * @return array|null
+     * {@inheritdoc}
      */
     public function getFilterFieldDescription($name)
     {

+ 9 - 0
Admin/AdminInterface.php

@@ -388,6 +388,15 @@ interface AdminInterface
      */
     public function getFilterFieldDescriptions();
 
+    /**
+     * Returns a filter FieldDescription
+     *
+     * @param string $name
+     *
+     * @return array|null
+     */
+    public function getFilterFieldDescription($name);
+
     /**
      * Returns a list depend on the given $object
      *

+ 8 - 0
CHANGELOG.md

@@ -1,6 +1,14 @@
 CHANGELOG
 =========
 
+### 2015-02-08
+ * [BC BREAK] added ``getFieldOption``, ``setFieldOption`` methods to the FilterInterface
+ * [BC BREAK] added the ``getFilterFieldDescription`` method to the AdminInterface
+ * [BC BREAK] added the ``getMaxPageLinks``, ``setMaxPageLinks`` methods to the PagerInterface
+ * [BC BREAK] CSS class ``sonata-autocomplete-dropdown-item`` is not automatically added to dropdown
+   autocomplete item in ``sonata_type_model_autocmplete``, use option ``dropdown_item_css_class``
+   to set the CSS class of dropdown item.
+
 ## 2015-01-05
  * [BC BREAK] #2665 - text from Admin's toString method is escaped for html output before adding in flash message to prevent possible XSS vulnerability.
 

+ 56 - 14
Controller/HelperController.php

@@ -308,8 +308,9 @@ class HelperController
      */
     public function retrieveAutocompleteItemsAction(Request $request)
     {
-        $admin = $this->pool->getInstance($request->get('code'));
+        $admin = $this->pool->getInstance($request->get('admin_code'));
         $admin->setRequest($request);
+        $context = $request->get('_context', '');
 
         if (false === $admin->isGranted('CREATE') && false === $admin->isGranted('EDIT')) {
             throw new AccessDeniedException();
@@ -318,19 +319,33 @@ class HelperController
         // subject will be empty to avoid unnecessary database requests and keep autocomplete function fast
         $admin->setSubject($admin->getNewInstance());
 
-        $fieldDescription = $this->retrieveFieldDescription($admin, $request->get('field'));
-        $formAutocomplete = $admin->getForm()->get($fieldDescription->getName());
+        if ($context == 'filter') {
+            // filter
+            $fieldDescription = $this->retrieveFilterFieldDescription($admin, $request->get('field'));
+            $filterAutocomplete = $admin->getDatagrid()->getFilter($fieldDescription->getName());
+
+            $property           = $filterAutocomplete->getFieldOption('property');
+            $callback           = $filterAutocomplete->getFieldOption('callback');
+            $minimumInputLength = $filterAutocomplete->getFieldOption('minimum_input_length', 3);
+            $itemsPerPage       = $filterAutocomplete->getFieldOption('items_per_page', 10);
+            $reqParamPageNumber = $filterAutocomplete->getFieldOption('req_param_name_page_number', '_page');
+            $toStringCallback   = $filterAutocomplete->getFieldOption('to_string_callback');
+        } else {
+            // create/edit form
+            $fieldDescription = $this->retrieveFormFieldDescription($admin, $request->get('field'));
+            $formAutocomplete = $admin->getForm()->get($fieldDescription->getName());
 
-        if ($formAutocomplete->getConfig()->getAttribute('disabled')) {
-            throw new AccessDeniedException('Autocomplete list can`t be retrieved because the form element is disabled or read_only.');
-        }
+            if ($formAutocomplete->getConfig()->getAttribute('disabled')) {
+                throw new AccessDeniedException('Autocomplete list can`t be retrieved because the form element is disabled or read_only.');
+            }
 
-        $property           = $formAutocomplete->getConfig()->getAttribute('property');
-        $callback           = $formAutocomplete->getConfig()->getAttribute('callback');
-        $minimumInputLength = $formAutocomplete->getConfig()->getAttribute('minimum_input_length');
-        $itemsPerPage       = $formAutocomplete->getConfig()->getAttribute('items_per_page');
-        $reqParamPageNumber = $formAutocomplete->getConfig()->getAttribute('req_param_name_page_number');
-        $toStringCallback   = $formAutocomplete->getConfig()->getAttribute('to_string_callback');
+            $property           = $formAutocomplete->getConfig()->getAttribute('property');
+            $callback           = $formAutocomplete->getConfig()->getAttribute('callback');
+            $minimumInputLength = $formAutocomplete->getConfig()->getAttribute('minimum_input_length');
+            $itemsPerPage       = $formAutocomplete->getConfig()->getAttribute('items_per_page');
+            $reqParamPageNumber = $formAutocomplete->getConfig()->getAttribute('req_param_name_page_number');
+            $toStringCallback   = $formAutocomplete->getConfig()->getAttribute('to_string_callback');
+        }
 
         $searchText = $request->get('q');
 
@@ -410,7 +425,7 @@ class HelperController
     }
 
     /**
-     * Retrieve the field description given by field name.
+     * Retrieve the form field description given by field name.
      *
      * @param AdminInterface $admin
      * @param string         $field
@@ -419,7 +434,7 @@ class HelperController
      *
      * @throws \RuntimeException
      */
-    private function retrieveFieldDescription(AdminInterface $admin, $field)
+    private function retrieveFormFieldDescription(AdminInterface $admin, $field)
     {
         $admin->getFormFieldDescriptions();
 
@@ -439,4 +454,31 @@ class HelperController
 
         return $fieldDescription;
     }
+
+    /**
+     * Retrieve the filter field description given by field name.
+     *
+     * @param AdminInterface $admin
+     * @param string         $field
+     *
+     * @return \Symfony\Component\Form\FormInterface
+     *
+     * @throws \RuntimeException
+     */
+    private function retrieveFilterFieldDescription(AdminInterface $admin, $field)
+    {
+        $admin->getFilterFieldDescriptions();
+
+        $fieldDescription = $admin->getFilterFieldDescription($field);
+
+        if (!$fieldDescription) {
+            throw new \RuntimeException(sprintf('The field "%s" does not exist.', $field));
+        }
+
+        if (null === $fieldDescription->getTargetEntity()) {
+            throw new \RuntimeException(sprintf('No associated entity with field "%s".', $field));
+        }
+
+        return $fieldDescription;
+    }
 }

+ 2 - 6
Datagrid/Pager.php

@@ -355,9 +355,7 @@ abstract class Pager implements \Iterator, \Countable, \Serializable, PagerInter
     }
 
     /**
-     * Returns the maximum number of page numbers.
-     *
-     * @return integer
+     * {@inheritdoc}
      */
     public function getMaxPageLinks()
     {
@@ -365,9 +363,7 @@ abstract class Pager implements \Iterator, \Countable, \Serializable, PagerInter
     }
 
     /**
-     * Sets the maximum number of page numbers.
-     *
-     * @param integer $maxPageLinks
+     * {@inheritdoc}
      */
     public function setMaxPageLinks($maxPageLinks)
     {

+ 14 - 0
Datagrid/PagerInterface.php

@@ -53,4 +53,18 @@ interface PagerInterface
      * @return array
      */
     public function getResults();
+
+    /**
+     * Sets the maximum number of page numbers.
+     *
+     * @param int $maxPageLinks
+     */
+    public function setMaxPageLinks($maxPageLinks);
+
+    /**
+     * Returns the maximum number of page numbers.
+     *
+     * @return int
+     */
+    public function getMaxPageLinks();
 }

+ 20 - 0
Filter/Filter.php

@@ -89,6 +89,26 @@ abstract class Filter implements FilterInterface
         return $this->getOption('field_options', array('required' => false));
     }
 
+    /**
+     * {@inheritdoc}
+     */
+    public function getFieldOption($name, $default = null)
+    {
+        if (isset($this->options['field_options'][$name]) && is_array($this->options['field_options'])) {
+            return $this->options['field_options'][$name];
+        }
+
+        return $default;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setFieldOption($name, $value)
+    {
+        $this->options['field_options'][$name] = $value;
+    }
+
     /**
      * {@inheritdoc}
      */

+ 15 - 0
Filter/FilterInterface.php

@@ -115,6 +115,21 @@ interface FilterInterface
      */
     public function getFieldOptions();
 
+    /**
+     * Get field option
+     *
+     * @return mixed
+     */
+    public function getFieldOption($name, $default = null);
+
+    /**
+     * Set field option
+     *
+     * @param string $name
+     * @param mixed  $value
+     */
+    public function setFieldOption($name, $value);
+
     /**
      * @return string
      */

+ 21 - 13
Form/DataTransformer/ModelToIdPropertyTransformer.php

@@ -15,7 +15,6 @@ namespace Sonata\AdminBundle\Form\DataTransformer;
 use Symfony\Component\Form\DataTransformerInterface;
 use Sonata\AdminBundle\Model\ModelManagerInterface;
 use Doctrine\Common\Util\ClassUtils;
-use RuntimeException;
 
 /**
  * Transform object to ID and property label
@@ -55,19 +54,27 @@ class ModelToIdPropertyTransformer implements DataTransformerInterface
     {
         $collection = $this->modelManager->getModelCollectionInstance($this->className);
 
-        if (empty($value) || empty($value['identifiers'])) {
-            if (!$this->multiple) {
-                return null;
-            } else {
+        if (empty($value)) {
+            if ($this->multiple) {
                 return $collection;
             }
+
+            return null;
         }
 
         if (!$this->multiple) {
-             return $this->modelManager->find($this->className, current($value['identifiers']));
+             return $this->modelManager->find($this->className, $value);
+        }
+
+        if (!is_array($value)) {
+            throw new \UnexpectedValueException(sprintf('Value should be array, %s given.', gettype($value)));
         }
 
-        foreach ($value['identifiers'] as $id) {
+        foreach ($value as $key => $id) {
+            if ($key === '_labels') {
+                continue;
+            }
+
             $collection->add($this->modelManager->find($this->className, $id));
         }
 
@@ -79,11 +86,12 @@ class ModelToIdPropertyTransformer implements DataTransformerInterface
      */
     public function transform($entityOrCollection)
     {
-        $result = array('identifiers' => array(), 'labels' => array());
+        $result = array();
 
         if (!$entityOrCollection) {
             return $result;
         }
+
         if ($this->multiple) {
             if (substr(get_class($entityOrCollection), -1 * strlen($this->className)) == $this->className) {
                 throw new \InvalidArgumentException('A multiple selection must be passed a collection not a single value. Make sure that form option "multiple=false" is set for many-to-one relation and "multiple=true" is set for many-to-many or one-to-many relations.');
@@ -103,7 +111,7 @@ class ModelToIdPropertyTransformer implements DataTransformerInterface
         }
 
         if (empty($this->property)) {
-            throw new RuntimeException('Please define "property" parameter.');
+            throw new \RuntimeException('Please define "property" parameter.');
         }
 
         foreach ($collection as $entity) {
@@ -111,7 +119,7 @@ class ModelToIdPropertyTransformer implements DataTransformerInterface
 
             if ($this->toStringCallback !== null) {
                 if (!is_callable($this->toStringCallback)) {
-                    throw new RuntimeException('Callback in "to_string_callback" option doesn`t contain callable function.');
+                    throw new \RuntimeException('Callback in "to_string_callback" option doesn`t contain callable function.');
                 }
 
                 $label = call_user_func($this->toStringCallback, $entity, $this->property);
@@ -119,12 +127,12 @@ class ModelToIdPropertyTransformer implements DataTransformerInterface
                 try {
                     $label = (string) $entity;
                 } catch (\Exception $e) {
-                    throw new RuntimeException(sprintf("Unable to convert the entity %s to String, entity must have a '__toString()' method defined", ClassUtils::getClass($entity)), 0, $e);
+                    throw new \RuntimeException(sprintf("Unable to convert the entity %s to String, entity must have a '__toString()' method defined", ClassUtils::getClass($entity)), 0, $e);
                 }
             }
 
-            $result['identifiers'][] = $id;
-            $result['labels'][] = $label;
+            $result[] = $id;
+            $result['_labels'][] = $label;
         }
 
         return $result;

+ 39 - 7
Form/Type/ModelAutocompleteType.php

@@ -18,6 +18,8 @@ use Symfony\Component\OptionsResolver\OptionsResolverInterface;
 use Symfony\Component\Form\FormView;
 use Symfony\Component\Form\FormInterface;
 use Sonata\AdminBundle\Form\DataTransformer\ModelToIdPropertyTransformer;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
 
 /**
  * This type defines a standard text field with autocomplete feature.
@@ -34,9 +36,6 @@ class ModelAutocompleteType extends AbstractType
     {
         $builder->addViewTransformer(new ModelToIdPropertyTransformer($options['model_manager'], $options['class'], $options['property'], $options['multiple'], $options['to_string_callback']), true);
 
-        $builder->add('title', 'text', array('attr'=>$options['attr'], 'property_path' => '[labels][0]'));
-        $builder->add('identifiers', 'collection', array('type'=>'hidden', 'allow_add' => true, 'allow_delete' => true));
-
         $builder->setAttribute('property', $options['property']);
         $builder->setAttribute('callback', $options['callback']);
         $builder->setAttribute('minimum_input_length', $options['minimum_input_length']);
@@ -44,6 +43,14 @@ class ModelAutocompleteType extends AbstractType
         $builder->setAttribute('req_param_name_page_number', $options['req_param_name_page_number']);
         $builder->setAttribute('disabled', $options['disabled'] || $options['read_only']);
         $builder->setAttribute('to_string_callback', $options['to_string_callback']);
+
+        if ($options['multiple']) {
+            $resizeListener = new ResizeFormListener(
+                'hidden', array(), true, true, true
+            );
+
+            $builder->addEventSubscriber($resizeListener);
+        }
     }
 
     /**
@@ -51,10 +58,13 @@ class ModelAutocompleteType extends AbstractType
      */
     public function buildView(FormView $view, FormInterface $form, array $options)
     {
+        $view->vars['admin_code'] = $options['admin_code'];
+
         $view->vars['placeholder'] = $options['placeholder'];
         $view->vars['multiple'] = $options['multiple'];
         $view->vars['minimum_input_length'] = $options['minimum_input_length'];
         $view->vars['items_per_page'] = $options['items_per_page'];
+        $view->vars['width'] = $options['width'];
 
         // ajax parameters
         $view->vars['url'] = $options['url'];
@@ -64,8 +74,17 @@ class ModelAutocompleteType extends AbstractType
         $view->vars['req_param_name_page_number'] = $options['req_param_name_page_number'];
         $view->vars['req_param_name_items_per_page'] = $options['req_param_name_items_per_page'];
 
-        // dropdown list css class
+        // CSS classes
+        $view->vars['container_css_class'] = $options['container_css_class'];
         $view->vars['dropdown_css_class'] = $options['dropdown_css_class'];
+        $view->vars['dropdown_item_css_class'] = $options['dropdown_item_css_class'];
+
+        $view->vars['dropdown_auto_width'] = $options['dropdown_auto_width'];
+
+        // template
+        $view->vars['template'] = $options['template'];
+
+        $view->vars['context'] = $options['context'];
     }
 
     /**
@@ -73,13 +92,20 @@ class ModelAutocompleteType extends AbstractType
      */
     public function setDefaultOptions(OptionsResolverInterface $resolver)
     {
+        $compound = function (Options $options) {
+            return $options['multiple'];
+        };
+
         $resolver->setDefaults(array(
             'attr'                            => array(),
-            'compound'                        => true,
+            'compound'                        => $compound,
             'model_manager'                   => null,
             'class'                           => null,
+            'admin_code'                      => null,
             'callback'                        => null,
             'multiple'                        => false,
+            'width'                           => '',
+            'context'                         => '',
 
             'placeholder'                     => '',
             'minimum_input_length'            => 3, //minimum 3 chars should be typed to load ajax data
@@ -95,8 +121,14 @@ class ModelAutocompleteType extends AbstractType
             'req_param_name_page_number'      => '_page',
             'req_param_name_items_per_page'   => '_per_page',
 
-            // dropdown list css class
-            'dropdown_css_class'              => 'sonata-autocomplete-dropdown',
+            // CSS classes
+            'container_css_class'            => '',
+            'dropdown_css_class'             => '',
+            'dropdown_item_css_class'        => '',
+
+            'dropdown_auto_width'            => false,
+
+            'template'                        => 'SonataAdminBundle:Form/Type:sonata_type_model_autocomplete.html.twig'
         ));
 
         $resolver->setRequired(array('property'));

+ 44 - 3
Resources/doc/reference/form_types.rst

@@ -232,8 +232,21 @@ route
   The route ``name`` with ``parameters`` that is used as target url for ajax
   requests.
 
+width
+  defaults to "". Controls the width style attribute of the Select2 container div.
+
+dropdown_auto_width
+  defaults to false. Set to true to enable the `dropdownAutoWidth` Select2 option,
+  which allows the drop downs to be wider than the parent input, sized according to their content.
+
+container_css_class
+  defaults to "". Css class that will be added to select2's container tag.
+
 dropdown_css_class
-  defaults to "sonata-autocomplete-dropdown". CSS class of dropdown list.
+  defaults to "". CSS class of dropdown list.
+
+dropdown_item_css_class
+  defaults to "". CSS class of dropdown item.
 
 req_param_name_search
   defaults to "q". Ajax request parameter name which contains the searched text.
@@ -245,6 +258,34 @@ req_param_name_items_per_page
   defaults to "_per_page".  Ajax request parameter name which contains the limit of
   items per page.
 
+template
+  defaults to ``SonataAdminBundle:Form/Type:sonata_type_model_autocomplete.html.twig``.
+  Use this option if you want to override the default template of this form type.
+
+.. code-block:: php
+
+    class ArticleAdmin extends Admin
+    {
+        protected function configureFormFields(FormMapper $formMapper)
+        {
+            $formMapper
+                ->add('category', 'sonata_type_model_autocomplete', array(
+                    'property'=>'title',
+                    'template'=>'AcmeAdminBundle:Form/Type:sonata_type_model_autocomplete.html.twig'
+                ))
+            ;
+        }
+    }
+
+.. code-block:: jinja
+
+    {# in Acme/AdminBundle/Resources/views/Form/Type/sonata_type_model_autocomplete.html.twig #}
+
+    {% extends 'SonataAdminBundle:Form/Type:sonata_type_model_autocomplete.html.twig' %}
+
+    {# change the default selection format #}
+    {% block sonata_type_model_autocomplete_selection_format %}'<b>'+item.label+'</b>'{% endblock %}
+
 sonata_choice_field_mask
 ^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -278,8 +319,8 @@ According the choice made only associated fields are displayed. The others field
 
 map
   Associative array. Describes the fields that are displayed for each choice.
-    
-    
+
+
 sonata_type_admin
 ^^^^^^^^^^^^^^^^^
 

+ 168 - 0
Resources/views/Form/Type/sonata_type_model_autocomplete.html.twig

@@ -0,0 +1,168 @@
+{#
+
+This file is part of the Sonata package.
+
+(c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
+
+For the full copyright and license information, please view the LICENSE
+file that was distributed with this source code.
+
+#}
+{% spaceless %}
+
+    <input type="text" name="{{ full_name }}_autocomplete_input" id="{{ id }}_autocomplete_input" value=""
+        {%- if read_only %} readonly="readonly"{% endif -%}
+        {%- if disabled %} disabled="disabled"{% endif -%}
+        {%- if required %} required="required"{% endif %}
+    />
+
+    <div id="{{ id }}_hidden_inputs_wrap">
+        {% if multiple -%}
+            {%- for idx, val in value if idx~'' != '_labels' -%}
+                <input type="hidden" name="{{ full_name }}[]" {%- if disabled %} disabled="disabled"{% endif %} value="{{ val }}">
+            {%- endfor -%}
+        {% else -%}
+            <input type="hidden" name="{{ full_name }}" {%- if disabled %} disabled="disabled"{% endif %} value="{{ value[0]|default('') }}">
+        {% endif -%}
+    </div>
+
+    <script>
+        (function ($) {
+            var autocompleteInput = $('#{{ id }}_autocomplete_input');
+            autocompleteInput.select2({
+                {%- set allowClearPlaceholder = (not multiple and not required) ? ' ' : '' -%}
+                placeholder: '{{ placeholder ?: allowClearPlaceholder }}', // allowClear needs placeholder to work properly
+                allowClear: {{ required ? 'false' : 'true' }},
+                enable: {{ disabled ? 'false' : 'true' }},
+                readonly: {{ read_only ? 'true' : 'false' }},
+                minimumInputLength: {{ minimum_input_length }},
+                multiple: {{ multiple ? 'true' : 'false' }},
+                width: '{{ width }}',
+                dropdownAutoWidth: {{ dropdown_auto_width ? 'true' : 'false' }},
+                containerCssClass: '{{ container_css_class ~ ' form-control' }}',
+                dropdownCssClass: '{{ dropdown_css_class }}',
+                ajax: {
+                    url:  '{{ url ?: path(route.name, route.parameters|default([])) }}',
+                    dataType: 'json',
+                    quietMillis: 100,
+                    data: function (term, page) { // page is the one-based page number tracked by Select2
+                        {% block sonata_type_model_autocomplete_ajax_request_parameters %}
+                        return {
+                                //search term
+                                '{{ req_param_name_search }}': term,
+
+                                // page size
+                                '{{ req_param_name_items_per_page }}': {{ items_per_page }},
+
+                                // page number
+                                '{{ req_param_name_page_number }}': page,
+
+                                // admin
+                                {% if sonata_admin.admin is not null %}
+                                    'uniqid': '{{ sonata_admin.admin.uniqid }}',
+                                    'admin_code': '{{ sonata_admin.admin.code }}',
+                                {% elseif admin_code %}
+                                    'admin_code':  '{{ admin_code }}',
+                                {% endif %}
+
+                                {% if context == 'filter' %}
+                                    'field':  '{{ full_name|replace({'filter[': '', '][value]': ''}) }}',
+                                    '_context': 'filter'
+                                {% else %}
+                                    'field':  '{{ name }}'
+                                {% endif %}
+
+                                // other parameters
+                                {% if req_params is not empty %},
+                                    {%- for key, value in req_params -%}
+                                        '{{- key|e('js') -}}': '{{- value|e('js') -}}'
+                                        {%- if not loop.last -%}, {% endif -%}
+                                    {%- endfor -%}
+                                {% endif %}
+                        };
+                        {% endblock %}
+                    },
+                    results: function (data, page) {
+                        // notice we return the value of more so Select2 knows if more results can be loaded
+                        return {results: data.items, more: data.more};
+                    }
+                },
+                formatResult: function (item) {
+                    return {% block sonata_type_model_autocomplete_dropdown_item_format %}'<div class="{{ dropdown_item_css_class }}">'+item.label+'</div>'{% endblock %};// format of one dropdown item
+                },
+                formatSelection: function (item) {
+                    return {% block sonata_type_model_autocomplete_selection_format %}item.label{% endblock %};// format selected item '<b>'+item.label+'</b>';
+                },
+                escapeMarkup: function (m) { return m; } // we do not want to escape markup since we are displaying html in results
+            });
+
+            autocompleteInput.on('change', function(e) {
+
+                // console.log('change '+JSON.stringify({val:e.val, added:e.added, removed:e.removed}));
+
+                // remove input
+                if (undefined !== e.removed && null !== e.removed) {
+                    var removedItems = e.removed;
+
+                    {% if multiple %}
+                        if(!$.isArray(removedItems)) {
+                            removedItems = [removedItems];
+                        }
+
+                        var length = removedItems.length;
+                        for (var i = 0; i < length; i++) {
+                            el = removedItems[i];
+                            $('#{{ id }}_hidden_inputs_wrap input:hidden[value="'+el.id+'"]').remove();
+                        }
+                    {%- else -%}
+                        $('#{{ id }}_hidden_inputs_wrap input:hidden').val('');
+                    {%- endif %}
+                }
+
+                // add new input
+                var el = null;
+                if (undefined !== e.added) {
+
+                    var addedItems = e.added;
+
+                    {% if multiple %}
+                        if(!$.isArray(addedItems)) {
+                            addedItems = [addedItems];
+                        }
+
+                        var length = addedItems.length;
+                        for (var i = 0; i < length; i++) {
+                            el = addedItems[i];
+                            $('#{{ id }}_hidden_inputs_wrap').append('<input type="hidden" name="{{ full_name }}[]" value="'+el.id+'" />');
+                        }
+                    {%- else -%}
+                        $('#{{ id }}_hidden_inputs_wrap input:hidden').val(addedItems.id);
+                    {%- endif %}
+                }
+            });
+
+            // Initialise the autocomplete
+            var data = [];
+
+            {%- if value is not empty -%}
+                data = {%- if multiple -%}[ {%- endif -%}
+                {%- for idx, val  in value if idx~'' != '_labels' -%}
+                    {%- if not loop.first -%}, {% endif -%}
+                    {id: '{{ val|e('js') }}', label:'{{ value['_labels'][idx]|e('js') }}'}
+                {%- endfor -%}
+                {%- if multiple -%} ] {%- endif -%};
+            {% endif -%}
+
+            if (undefined==data.length || 0<data.length) { // Leave placeholder if no data set
+                autocompleteInput.select2('data', data);
+            }
+
+            // remove unneeded autocomplete text input before form submit
+            $('#{{ id }}_autocomplete_input').closest('form').submit(function()
+            {
+                $('#{{ id }}_autocomplete_input').remove();
+                return true;
+            });
+        })(jQuery);
+    </script>
+{% endspaceless %}

+ 4 - 0
Resources/views/Form/filter_admin_fields.html.twig

@@ -83,3 +83,7 @@ file that was distributed with this source code.
         {% endif %}
     {% endspaceless %}
 {% endblock checkbox_widget %}
+
+{% block sonata_type_model_autocomplete_widget %}
+    {% include template %}
+{% endblock sonata_type_model_autocomplete_widget %}

+ 1 - 119
Resources/views/Form/form_admin_fields.html.twig

@@ -274,125 +274,7 @@ file that was distributed with this source code.
 {% endblock %}
 
 {% block sonata_type_model_autocomplete_widget %}
-{% spaceless %}
-
-    {{ form_widget(form.title) }}
-
-    {% for child in form %}
-        {% if not child.rendered %}
-            {{ form_widget(child) }}
-        {% endif %}
-    {% endfor %}
-
-    <script>
-        (function ($) {
-            var autocompleteInput = $("#{{ form.title.vars.id }}");
-            autocompleteInput.select2({
-                placeholder: "{{ placeholder }}",
-                allowClear: {{ required ? 'false' : 'true' }},
-                enable: {{ disabled ? 'false' : 'true' }},
-                readonly: {{ read_only ? 'true' : 'false' }},
-                minimumInputLength: {{ minimum_input_length }},
-                multiple: {{ multiple ? 'true' : 'false' }},
-                ajax: {
-                    url:  "{{ url ?: path(route.name, route.parameters|default([])) }}",
-                    dataType: 'json',
-                    quietMillis: 100,
-                    data: function (term, page) { // page is the one-based page number tracked by Select2
-                        return {
-                                //search term
-                                "{{ req_param_name_search }}": term,
-
-                                // page size
-                                "{{ req_param_name_items_per_page }}": {{ items_per_page }},
-
-                                // page number
-                                "{{ req_param_name_page_number }}": page,
-
-                                // admin
-                                'uniqid': "{{ sonata_admin.admin.uniqid }}",
-                                'code':   "{{ sonata_admin.admin.code }}",
-                                'field':  "{{ name }}"
-
-                                // other parameters
-                                {% if req_params is not empty %},
-                                    {%- for key, value in req_params -%}
-                                        "{{- key|e('js') -}}": "{{- value|e('js') -}}"
-                                        {%- if not loop.last -%}, {% endif -%}
-                                    {%- endfor -%}
-                                {% endif %}
-                            };
-                    },
-                    results: function (data, page) {
-                        // notice we return the value of more so Select2 knows if more results can be loaded
-                        return {results: data.items, more: data.more};
-                    }
-                },
-                formatResult: function (item) {
-                    return {% block sonata_type_model_autocomplete_dropdown_item_format %}'<span class="sonata-autocomplete-dropdown-item">'+item.label+'</span>'{% endblock %};// format of one dropdown item
-                },
-                formatSelection: function (item) {
-                    return {% block sonata_type_model_autocomplete_selection_format %}item.label{% endblock %};// format selected item '<b>'+item.label+'</b>';
-                },
-                dropdownCssClass: "{{ dropdown_css_class }}",
-                escapeMarkup: function (m) { return m; } // we do not want to escape markup since we are displaying html in results
-            });
-
-            autocompleteInput.on("change", function(e) {
-
-                // console.log("change "+JSON.stringify({val:e.val, added:e.added, removed:e.removed}));
-
-                // add new input
-                var el = null;
-                if (undefined !== e.added) {
-
-                    var addedItems = e.added;
-
-                    if(!$.isArray(addedItems)) {
-                        addedItems = [addedItems];
-                    }
-
-                    var length = addedItems.length;
-                    for (var i = 0; i < length; i++) {
-                        el = addedItems[i];
-                        $("#{{ form.identifiers.vars.id }}").append('<input type="hidden" name="{{ form.identifiers.vars.full_name }}[]" value="'+el.id+'" />');
-                    }
-                }
-
-                // remove input
-                if (undefined !== e.removed && null !== e.removed) {
-                    var removedItems = e.removed;
-
-                    if(!$.isArray(removedItems)) {
-                        removedItems = [removedItems];
-                    }
-
-                    var length = removedItems.length;
-                    for (var i = 0; i < length; i++) {
-                        el = removedItems[i];
-                        $('#{{ form.identifiers.vars.id }} input:hidden[value="'+el.id+'"]').remove();
-                    }
-                }
-            });
-
-            // Initialise the autocomplete
-            var data = [];
-            {% if multiple -%}
-                data = [
-                {%- for key, label_text in value.labels -%}
-                    {id: '{{ value.identifiers[key]|e('js') }}', label:'{{ label_text|e('js') }}'}
-                    {%- if not loop.last -%}, {% endif -%}
-                {%- endfor -%}
-                ];
-            {%- elseif value.labels[0] is defined -%}
-                data = {id: '{{ value.identifiers[0]|e('js') }}', label:'{{ value.labels[0]|e('js') }}'};
-            {%- endif  %}
-            if (undefined==data.length || 0<data.length) { // Leave placeholder if no data set
-                autocompleteInput.select2('data', data);
-            }
-        })(jQuery);
-    </script>
-{% endspaceless %}
+    {% include template %}
 {% endblock sonata_type_model_autocomplete_widget %}
 
 {% block sonata_type_choice_field_mask_widget %}

+ 71 - 0
Tests/Admin/AdminTest.php

@@ -22,6 +22,8 @@ use Sonata\AdminBundle\Tests\Fixtures\Admin\PostAdmin;
 use Sonata\AdminBundle\Tests\Fixtures\Admin\CommentAdmin;
 use Symfony\Component\HttpFoundation\Request;
 use Sonata\AdminBundle\Admin\AdminInterface;
+use Sonata\AdminBundle\Tests\Fixtures\Admin\ModelAdmin;
+use Sonata\AdminBundle\Tests\Fixtures\Admin\FieldDescription;
 
 class AdminTest extends \PHPUnit_Framework_TestCase
 {
@@ -1500,6 +1502,75 @@ class AdminTest extends \PHPUnit_Framework_TestCase
         $this->assertTrue(isset($parameters['post__author']));
         $this->assertEquals(array('value' => $authorId), $parameters['post__author']);
     }
+
+    public function testGetFilterFieldDescription()
+    {
+        $modelAdmin = new ModelAdmin('sonata.post.admin.model', 'Application\Sonata\FooBundle\Entity\Model', 'SonataFooBundle:ModelAdmin');
+
+        $fooFieldDescription = new FieldDescription();
+        $barFieldDescription = new FieldDescription();
+        $bazFieldDescription = new FieldDescription();
+
+        $modelManager = $this->getMock('Sonata\AdminBundle\Model\ModelManagerInterface');
+        $modelManager->expects($this->exactly(3))
+            ->method('getNewFieldDescriptionInstance')
+            ->will($this->returnCallback(function ($adminClass, $name, $filterOptions) use ($fooFieldDescription, $barFieldDescription, $bazFieldDescription) {
+                switch ($name) {
+                    case 'foo':
+                        $fieldDescription = $fooFieldDescription;
+                        break;
+
+                    case 'bar':
+                        $fieldDescription = $barFieldDescription;
+                        break;
+
+                    case 'baz':
+                        $fieldDescription = $bazFieldDescription;
+                        break;
+
+                    default:
+                        throw new \RuntiemException(sprintf('Unknown filter name "%s"', $name));
+                        break;
+                }
+
+                $fieldDescription->setName($name);
+
+                return $fieldDescription;
+            }));
+
+        $modelAdmin->setModelManager($modelManager);
+
+        $pager = $this->getMock('Sonata\AdminBundle\Datagrid\PagerInterface');
+
+        $datagrid = $this->getMock('Sonata\AdminBundle\Datagrid\DatagridInterface');
+        $datagrid->expects($this->once())
+            ->method('getPager')
+            ->will($this->returnValue($pager));
+
+        $datagridBuilder = $this->getMock('Sonata\AdminBundle\Builder\DatagridBuilderInterface');
+        $datagridBuilder->expects($this->once())
+            ->method('getBaseDatagrid')
+            ->with($this->identicalTo($modelAdmin), array())
+            ->will($this->returnValue($datagrid));
+
+        $datagridBuilder->expects($this->exactly(3))
+            ->method('addFilter')
+            ->will($this->returnCallback(function ($datagrid, $type = null, $fieldDescription, AdminInterface $admin) {
+                $admin->addFilterFieldDescription($fieldDescription->getName(), $fieldDescription);
+                $fieldDescription->mergeOption('field_options', array('required' => false));
+            }));
+
+        $modelAdmin->setDatagridBuilder($datagridBuilder);
+
+        $this->assertSame(array('foo'=>$fooFieldDescription, 'bar'=>$barFieldDescription, 'baz'=>$bazFieldDescription), $modelAdmin->getFilterFieldDescriptions());
+        $this->assertFalse($modelAdmin->hasFilterFieldDescription('fooBar'));
+        $this->assertTrue($modelAdmin->hasFilterFieldDescription('foo'));
+        $this->assertTrue($modelAdmin->hasFilterFieldDescription('bar'));
+        $this->assertTrue($modelAdmin->hasFilterFieldDescription('baz'));
+        $this->assertSame($fooFieldDescription, $modelAdmin->getFilterFieldDescription('foo'));
+        $this->assertSame($barFieldDescription, $modelAdmin->getFilterFieldDescription('bar'));
+        $this->assertSame($bazFieldDescription, $modelAdmin->getFilterFieldDescription('baz'));
+    }
 }
 
 class DummySubject

+ 1 - 45
Tests/Admin/BaseFieldDescriptionTest.php

@@ -13,6 +13,7 @@ namespace Sonata\AdminBundle\Tests\Admin;
 
 use Sonata\AdminBundle\Admin\BaseFieldDescription;
 use Sonata\AdminBundle\Admin\AdminInterface;
+use Sonata\AdminBundle\Tests\Fixtures\Admin\FieldDescription;
 
 class BaseFieldDescriptionTest extends \PHPUnit_Framework_TestCase
 {
@@ -190,48 +191,3 @@ class BaseFieldDescriptionTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals('FOoBar', BaseFieldDescription::camelize('fOo bar'));
     }
 }
-
-class FieldDescription extends BaseFieldDescription
-{
-    public function setAssociationMapping($associationMapping)
-    {
-        // TODO: Implement setAssociationMapping() method.
-    }
-
-    public function getTargetEntity()
-    {
-        // TODO: Implement getTargetEntity() method.
-    }
-
-    public function setFieldMapping($fieldMapping)
-    {
-        // TODO: Implement setFieldMapping() method.
-    }
-
-    public function isIdentifier()
-    {
-        // TODO: Implement isIdentifier() method.
-    }
-
-    /**
-     * set the parent association mappings information
-     *
-     * @param  array $parentAssociationMappings
-     * @return void
-     */
-    public function setParentAssociationMappings(array $parentAssociationMappings)
-    {
-        // TODO: Implement setParentAssociationMappings() method.
-    }
-
-    /**
-     * return the value linked to the description
-     *
-     * @param  $object
-     * @return bool|mixed
-     */
-    public function getValue($object)
-    {
-        // TODO: Implement getValue() method.
-    }
-}

+ 3 - 3
Tests/Controller/HelperControllerTest.php

@@ -501,7 +501,7 @@ class HelperControllerTest extends \PHPUnit_Framework_TestCase
             }));
 
         $request = new Request(array(
-            'code'     => 'foo.admin',
+            'admin_code'     => 'foo.admin',
         ), array(), array(), array(), array(), array('REQUEST_METHOD' => 'GET', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'));
 
         $this->controller->retrieveAutocompleteItemsAction($request);
@@ -574,7 +574,7 @@ class HelperControllerTest extends \PHPUnit_Framework_TestCase
             ->will($this->returnValue(true));
 
         $request = new Request(array(
-            'code'  => 'foo.admin',
+            'admin_code'  => 'foo.admin',
             'field' => 'barField'
         ), array(), array(), array(), array(), array('REQUEST_METHOD' => 'GET', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'));
 
@@ -676,7 +676,7 @@ class HelperControllerTest extends \PHPUnit_Framework_TestCase
             }));
 
         $request = new Request(array(
-            'code'  => 'foo.admin',
+            'admin_code'  => 'foo.admin',
             'field' => 'barField'
         ), array(), array(), array(), array(), array('REQUEST_METHOD' => 'GET', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'));
 

+ 25 - 0
Tests/Filter/FilterTest.php

@@ -52,6 +52,31 @@ class FilterTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals('>', $filter->getCondition());
     }
 
+    public function testGetFieldOption()
+    {
+        $filter = new FooFilter();
+        $filter->initialize('name', array(
+            'field_options' => array('foo'=>'bar', 'baz'=>12345)
+        ));
+
+        $this->assertSame(array('foo'=>'bar', 'baz'=>12345), $filter->getFieldOptions());
+        $this->assertSame('bar', $filter->getFieldOption('foo'));
+        $this->assertSame(12345, $filter->getFieldOption('baz'));
+    }
+
+    public function testSetFieldOption()
+    {
+        $filter = new FooFilter();
+        $this->assertEquals(array('required'=>false), $filter->getFieldOptions());
+
+        $filter->setFieldOption('foo', 'bar');
+        $filter->setFieldOption('baz', 12345);
+
+        $this->assertSame(array('foo'=>'bar', 'baz'=>12345), $filter->getFieldOptions());
+        $this->assertSame('bar', $filter->getFieldOption('foo'));
+        $this->assertSame(12345, $filter->getFieldOption('baz'));
+    }
+
     public function testInitialize()
     {
         $filter = new FooFilter();

+ 59 - 0
Tests/Fixtures/Admin/FieldDescription.php

@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * This file is part of the Sonata package.
+ *
+ * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ */
+namespace Sonata\AdminBundle\Tests\Fixtures\Admin;
+
+use Sonata\AdminBundle\Admin\BaseFieldDescription;
+
+class FieldDescription extends BaseFieldDescription
+{
+    public function setAssociationMapping($associationMapping)
+    {
+        // TODO: Implement setAssociationMapping() method.
+    }
+
+    public function getTargetEntity()
+    {
+        // TODO: Implement getTargetEntity() method.
+    }
+
+    public function setFieldMapping($fieldMapping)
+    {
+        // TODO: Implement setFieldMapping() method.
+    }
+
+    public function isIdentifier()
+    {
+        // TODO: Implement isIdentifier() method.
+    }
+
+    /**
+     * set the parent association mappings information
+     *
+     * @param  array $parentAssociationMappings
+     * @return void
+     */
+    public function setParentAssociationMappings(array $parentAssociationMappings)
+    {
+        // TODO: Implement setParentAssociationMappings() method.
+    }
+
+    /**
+     * return the value linked to the description
+     *
+     * @param  $object
+     * @return bool|mixed
+     */
+    public function getValue($object)
+    {
+        // TODO: Implement getValue() method.
+    }
+}

+ 61 - 19
Tests/Form/DataTransformer/ModelToIdPropertyTransformerTest.php

@@ -35,13 +35,21 @@ class ModelToIdPropertyTransformerTest extends \PHPUnit_Framework_TestCase
         $this->modelManager
             ->expects($this->any())
             ->method('find')
-            ->with($this->equalTo('Sonata\AdminBundle\Tests\Fixtures\Entity\Foo'), $this->equalTo(123))
-            ->will($this->returnValue($entity));
+            ->will($this->returnCallback(function ($class, $id) use ($entity) {
+                if ($class === 'Sonata\AdminBundle\Tests\Fixtures\Entity\Foo' && $id === 123) {
+                    return $entity;
+                }
+
+                return null;
+            }));
 
         $this->assertNull($transformer->reverseTransform(null));
         $this->assertNull($transformer->reverseTransform(false));
+        $this->assertNull($transformer->reverseTransform(''));
         $this->assertNull($transformer->reverseTransform(12));
-        $this->assertEquals($entity, $transformer->reverseTransform(array('identifiers' => array(123), 'titles' => array('example'))));
+        $this->assertNull($transformer->reverseTransform(array(123)));
+        $this->assertNull($transformer->reverseTransform(array(123, 456, 789)));
+        $this->assertEquals($entity, $transformer->reverseTransform(123));
     }
 
     /**
@@ -103,10 +111,42 @@ class ModelToIdPropertyTransformerTest extends \PHPUnit_Framework_TestCase
         return array(
             array(array(), null, $entity1, $entity2, $entity3),
             array(array(), false, $entity1, $entity2, $entity3),
-            array(array(), true, $entity1, $entity2, $entity3),
-            array(array(), 12, $entity1, $entity2, $entity3),
-            array(array($entity1), array('identifiers' => array(123), 'titles' => array('example')), $entity1, $entity2, $entity3),
-            array(array($entity1, $entity2, $entity3), array('identifiers' => array(123, 456, 789), 'titles' => array('example', 'example2', 'example3')), $entity1, $entity2, $entity3),
+            array(array($entity1), array(123, '_labels' => array('example')), $entity1, $entity2, $entity3),
+            array(array($entity1, $entity2, $entity3), array(123, 456, 789, '_labels' => array('example', 'example2', 'example3')), $entity1, $entity2, $entity3),
+        );
+    }
+
+    /**
+     * @dataProvider getReverseTransformMultipleInvalidTypeTests
+     */
+    public function testReverseTransformMultipleInvalidTypeTests($expected, $params, $type)
+    {
+        $this->setExpectedException(
+          'UnexpectedValueException', sprintf('Value should be array, %s given.', $type)
+        );
+
+        $transformer = new ModelToIdPropertyTransformer($this->modelManager, 'Sonata\AdminBundle\Tests\Fixtures\Entity\Foo', 'bar', true);
+
+        $collection = new ArrayCollection();
+        $this->modelManager
+            ->expects($this->any())
+            ->method('getModelCollectionInstance')
+            ->with($this->equalTo('Sonata\AdminBundle\Tests\Fixtures\Entity\Foo'))
+            ->will($this->returnValue($collection));
+
+        $result = $transformer->reverseTransform($params);
+        $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $result);
+        $this->assertEquals($expected, $result->getValues());
+    }
+
+    public function getReverseTransformMultipleInvalidTypeTests()
+    {
+        return array(
+            array(array(), true, 'boolean'),
+            array(array(), 12, 'integer'),
+            array(array(), 12.9, 'double'),
+            array(array(), '_labels', 'string'),
+            array(array(), new \stdClass(), 'object'),
         );
     }
 
@@ -121,12 +161,13 @@ class ModelToIdPropertyTransformerTest extends \PHPUnit_Framework_TestCase
 
         $transformer = new ModelToIdPropertyTransformer($this->modelManager, 'Sonata\AdminBundle\Tests\Fixtures\Entity\Foo', 'bar', false);
 
-        $this->assertEquals(array('identifiers' => array(), 'labels' => array()), $transformer->transform(null));
-        $this->assertEquals(array('identifiers' => array(), 'labels' => array()), $transformer->transform(false));
-        $this->assertEquals(array('identifiers' => array(), 'labels' => array()), $transformer->transform(0));
-        $this->assertEquals(array('identifiers' => array(), 'labels' => array()), $transformer->transform('0'));
+        $this->assertEquals(array(), $transformer->transform(null));
+        $this->assertEquals(array(), $transformer->transform(false));
+        $this->assertEquals(array(), $transformer->transform(''));
+        $this->assertEquals(array(), $transformer->transform(0));
+        $this->assertEquals(array(), $transformer->transform('0'));
 
-        $this->assertEquals(array('identifiers' => array(123), 'labels' => array('example')), $transformer->transform($entity));
+        $this->assertEquals(array(123, '_labels' => array('example')), $transformer->transform($entity));
     }
 
     public function testTransformWorksWithArrayAccessEntity()
@@ -140,7 +181,7 @@ class ModelToIdPropertyTransformerTest extends \PHPUnit_Framework_TestCase
 
         $transformer = new ModelToIdPropertyTransformer($this->modelManager, 'Sonata\AdminBundle\Tests\Fixtures\Entity\FooArrayAccess', 'bar', false);
 
-        $this->assertEquals(array('identifiers' => array(123), 'labels' => array('example')), $transformer->transform($entity));
+        $this->assertEquals(array(123, '_labels' => array('example')), $transformer->transform($entity));
     }
 
     public function testTransformToStringCallback()
@@ -157,7 +198,7 @@ class ModelToIdPropertyTransformerTest extends \PHPUnit_Framework_TestCase
             return $entity->getBaz();
         });
 
-        $this->assertEquals(array('identifiers' => array(123), 'labels' => array('bazz')), $transformer->transform($entity));
+        $this->assertEquals(array(123, '_labels' => array('bazz')), $transformer->transform($entity));
     }
 
     /**
@@ -215,12 +256,13 @@ class ModelToIdPropertyTransformerTest extends \PHPUnit_Framework_TestCase
 
         $transformer = new ModelToIdPropertyTransformer($this->modelManager, 'Sonata\AdminBundle\Tests\Fixtures\Entity\Foo', 'bar', true);
 
-        $this->assertEquals(array('identifiers' => array(), 'labels' => array()), $transformer->transform(null));
-        $this->assertEquals(array('identifiers' => array(), 'labels' => array()), $transformer->transform(false));
-        $this->assertEquals(array('identifiers' => array(), 'labels' => array()), $transformer->transform(0));
-        $this->assertEquals(array('identifiers' => array(), 'labels' => array()), $transformer->transform('0'));
+        $this->assertEquals(array(), $transformer->transform(null));
+        $this->assertEquals(array(), $transformer->transform(false));
+        $this->assertEquals(array(), $transformer->transform(''));
+        $this->assertEquals(array(), $transformer->transform(0));
+        $this->assertEquals(array(), $transformer->transform('0'));
 
-        $this->assertEquals(array('identifiers' => array(123, 456, 789), 'labels' => array('foo', 'bar', 'baz')), $transformer->transform($collection));
+        $this->assertEquals(array(123, 456, 789, '_labels' => array('foo', 'bar', 'baz')), $transformer->transform($collection));
     }
 
     /**

+ 10 - 2
Tests/Form/Type/ModelAutocompleteTypeTest.php

@@ -29,7 +29,7 @@ class ModelAutocompleteTypeTest extends TypeTestCase
         $options = $optionResolver->resolve(array('model_manager' => $modelManager, 'class' => 'Foo', 'property'=>'bar'));
 
         $this->assertEquals(array(), $options['attr']);
-        $this->assertTrue($options['compound']);
+        $this->assertFalse($options['compound']);
         $this->assertInstanceOf('Sonata\AdminBundle\Model\ModelManagerInterface', $options['model_manager']);
         $this->assertEquals($modelManager, $options['model_manager']);
         $this->assertEquals('Foo', $options['class']);
@@ -39,6 +39,8 @@ class ModelAutocompleteTypeTest extends TypeTestCase
         $this->assertEquals('', $options['placeholder']);
         $this->assertEquals(3, $options['minimum_input_length']);
         $this->assertEquals(10, $options['items_per_page']);
+        $this->assertEquals('', $options['width']);
+        $this->assertFalse($options['dropdown_auto_width']);
 
         $this->assertEquals('', $options['url']);
         $this->assertEquals(array('name'=>'sonata_admin_retrieve_autocomplete_items', 'parameters'=>array()), $options['route']);
@@ -46,6 +48,12 @@ class ModelAutocompleteTypeTest extends TypeTestCase
         $this->assertEquals('q', $options['req_param_name_search']);
         $this->assertEquals('_page', $options['req_param_name_page_number']);
         $this->assertEquals('_per_page', $options['req_param_name_items_per_page']);
-        $this->assertEquals('sonata-autocomplete-dropdown', $options['dropdown_css_class']);
+        $this->assertEquals('', $options['container_css_class']);
+        $this->assertEquals('', $options['dropdown_css_class']);
+        $this->assertEquals('', $options['dropdown_item_css_class']);
+
+        $this->assertEquals('SonataAdminBundle:Form/Type:sonata_type_model_autocomplete.html.twig', $options['template']);
+
+        $this->assertEquals('', $options['context']);
     }
 }

+ 6 - 0
UPGRADE-2.4.md

@@ -9,3 +9,9 @@ You will need to follow the dependencies upgrade instructions.
 
 If you have implemented a custom datagrid builder, you must adapt the signature
 of its `addFilter` method to match the one in `DatagridBuilderInterface` again.
+
+## sonata_type_model_autocomplete
+CSS class ``sonata-autocomplete-dropdown-item`` is not automatically added to 
+dropdown autocomplete item in ``sonata_type_model_autocmplete``, use option 
+``dropdown_item_css_class`` to set the CSS class of dropdown item.
+