diff --git a/.gitignore b/.gitignore index bbbe0ef..5009586 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,8 @@ composer.phar #PhpStorm .idea + +#Visual Studio +.vs +*.phpproj +*.sln diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 07fea7a..0873825 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -6,7 +6,7 @@ use Symfony\Component\Config\Definition\ConfigurationInterface; /** - * This is the class that validates and merges configuration from your app/config files + * This is the class that validates and merges configuration from your config/packages files * * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class} */ @@ -18,37 +18,31 @@ class Configuration implements ConfigurationInterface public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder('tetranz_select2_entity'); - $rootNode = \method_exists($treeBuilder, 'getRootNode') ? $treeBuilder->getRootNode() : $treeBuilder->root('tetranz_select2_entity'); + $rootNode = $treeBuilder->getRootNode(); $rootNode ->children() - ->scalarNode('minimum_input_length')->defaultValue(1)->end() - ->scalarNode('scroll')->defaultFalse()->end() - ->scalarNode('page_limit')->defaultValue(10)->end() - ->scalarNode('allow_clear')->defaultFalse()->end() + ->integerNode('minimum_input_length')->min(0)->defaultValue(1)->end() + ->booleanNode('scroll')->defaultFalse()->end() + ->integerNode('page_limit')->defaultValue(10)->end() + ->booleanNode('allow_clear')->defaultFalse()->end() ->arrayNode('allow_add')->addDefaultsIfNotSet() ->children() - ->scalarNode('enabled')->defaultFalse()->end() + ->booleanNode('enabled')->defaultFalse()->end() ->scalarNode('new_tag_text')->defaultValue(' (NEW)')->end() ->scalarNode('new_tag_prefix')->defaultValue('__')->end() ->scalarNode('tag_separators')->defaultValue('[",", " "]')->end() ->end() ->end() - ->scalarNode('delay')->defaultValue(250)->end() + ->integerNode('delay')->defaultValue(250)->min(0)->end() ->scalarNode('language')->defaultValue('en')->end() - ->scalarNode('cache')->defaultTrue()->end() - // default to 1ms for backwards compatibility for older versions where 'cache' is true but the - // user is not aware of the updated caching feature. This way the cache will, by default, not - // be very effective. Realistically this should be like 60000ms (60 seconds). - ->scalarNode('cache_timeout')->defaultValue(1)->end() + ->booleanNode('cache')->defaultTrue()->end() + ->integerNode('cache_timeout')->defaultValue(60000)->min(0)->end() ->scalarNode('width')->defaultNull()->end() - ->scalarNode('object_manager')->defaultValue(1)->end() + ->scalarNode('object_manager')->defaultNull()->end() + ->booleanNode('render_html')->defaultFalse()->end() ->end(); - // Here you should define the parameters that are allowed to - // configure your bundle. See the documentation linked above for - // more information on that topic. - return $treeBuilder; } } diff --git a/Form/Type/Select2EntityType.php b/Form/Type/Select2EntityType.php index f0b55b9..e2ca2f5 100644 --- a/Form/Type/Select2EntityType.php +++ b/Form/Type/Select2EntityType.php @@ -2,17 +2,18 @@ namespace Tetranz\Select2EntityBundle\Form\Type; +use Tetranz\Select2EntityBundle\Form\DataTransformer\EntitiesToPropertyTransformer; +use Tetranz\Select2EntityBundle\Form\DataTransformer\EntityToPropertyTransformer; + use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Common\Persistence\ManagerRegistry; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\Routing\RouterInterface; -use Tetranz\Select2EntityBundle\Form\DataTransformer\EntitiesToPropertyTransformer; -use Tetranz\Select2EntityBundle\Form\DataTransformer\EntityToPropertyTransformer; use Symfony\Component\PropertyAccess\PropertyAccess; /** @@ -22,21 +23,24 @@ */ class Select2EntityType extends AbstractType { + /** @var ManagerRegistry */ + protected $registry; /** @var ObjectManager */ protected $em; /** @var RouterInterface */ protected $router; - /** @var array */ + /** @var array */ protected $config; /** - * @param ObjectManager $em - * @param RouterInterface $router - * @param array $config + * @param ManagerRegistry $registry + * @param RouterInterface $router + * @param array $config */ - public function __construct(ObjectManager $em, RouterInterface $router, $config) + public function __construct(ManagerRegistry $registry, RouterInterface $router, $config) { - $this->em = $em; + $this->registry = $registry; + $this->em = $registry->getManager(); $this->router = $router; $this->config = $config; } @@ -44,13 +48,25 @@ public function __construct(ObjectManager $em, RouterInterface $router, $config) public function buildForm(FormBuilderInterface $builder, array $options) { // custom object manager for this entity, override the default entity manager ? - if(isset($options['object_manager'])) { + if (isset($options['object_manager'])) { $em = $options['object_manager']; - if(!$em instanceof ObjectManager) { + if (!$em instanceof ObjectManager) { throw new \Exception('The entity manager \'em\' must be an ObjectManager instance'); } // Use the custom manager instead. $this->em = $em; + } else if (isset($this->config['object_manager'])) { + $em = $this->registry->getManager($this->config['object_manager']); + if (!$em instanceof ObjectManager) { + throw new \Exception('The entity manager \'em\' must be an ObjectManager instance'); + } + $this->em = $em; + } + else { + $manager = $this->registry->getManagerForClass($options['class']); + if ($manager instanceof ObjectManager) { + $this->em = $manager; + } } // add custom data transformer @@ -70,9 +86,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) // add the default data transformer } else { - - $newTagPrefix = isset($options['allow_add']['new_tag_prefix']) ? $options['allow_add']['new_tag_prefix'] : $this->config['allow_add']['new_tag_prefix']; - $newTagText = isset($options['allow_add']['new_tag_text']) ? $options['allow_add']['new_tag_text'] : $this->config['allow_add']['new_tag_text']; + $newTagPrefix = $options['allow_add']['new_tag_prefix'] ?? $this->config['allow_add']['new_tag_prefix']; + $newTagText = $options['allow_add']['new_tag_text'] ?? $this->config['allow_add']['new_tag_text']; $transformer = $options['multiple'] ? new EntitiesToPropertyTransformer($this->em, $options['class'], $options['text_property'], $options['primary_key'], $newTagPrefix, $newTagText) @@ -87,10 +102,10 @@ public function finishView(FormView $view, FormInterface $form, array $options) parent::finishView($view, $form, $options); // make variables available to the view $view->vars['remote_path'] = $options['remote_path'] - ?: $this->router->generate($options['remote_route'], array_merge($options['remote_params'], [ 'page_limit' => $options['page_limit'] ])); + ?: $this->router->generate($options['remote_route'], array_merge($options['remote_params'], ['page_limit' => $options['page_limit'] ])); // merge variable names which are only set per instance with those from yml config - $varNames = array_merge(array('multiple', 'placeholder', 'primary_key', 'autostart'), array_keys($this->config)); + $varNames = array_merge(['multiple', 'placeholder', 'primary_key', 'autostart', 'query_parameters'], array_keys($this->config)); foreach ($varNames as $varName) { $view->vars[$varName] = $options[$varName]; } @@ -109,11 +124,7 @@ public function finishView(FormView $view, FormInterface $form, array $options) //tags options $varNames = array_keys($this->config['allow_add']); foreach ($varNames as $varName) { - if (isset($options['allow_add'][$varName])) { - $view->vars['allow_add'][$varName] = $options['allow_add'][$varName]; - } else { - $view->vars['allow_add'][$varName] = $this->config['allow_add'][$varName]; - } + $view->vars['allow_add'][$varName] = $options['allow_add'][$varName] ?? $this->config['allow_add'][$varName]; } if ($options['multiple']) { @@ -123,73 +134,52 @@ public function finishView(FormView $view, FormInterface $form, array $options) $view->vars['class_type'] = $options['class_type']; } - /** - * Added for pre Symfony 2.7 compatibility - * - * @param OptionsResolverInterface $resolver - */ - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $this->configureOptions($resolver); - } - /** * @param OptionsResolver $resolver */ public function configureOptions(OptionsResolver $resolver) { - $resolver->setDefaults( - array( - 'object_manager'=> null, + $resolver->setDefaults([ + 'object_manager' => null, 'class' => null, 'data_class' => null, 'primary_key' => 'id', 'remote_path' => null, 'remote_route' => null, - 'remote_params' => array(), + 'remote_params' => [], 'multiple' => false, 'compound' => false, 'minimum_input_length' => $this->config['minimum_input_length'], 'page_limit' => $this->config['page_limit'], 'scroll' => $this->config['scroll'], 'allow_clear' => $this->config['allow_clear'], - 'allow_add' => array( + 'allow_add' => [ 'enabled' => $this->config['allow_add']['enabled'], 'new_tag_text' => $this->config['allow_add']['new_tag_text'], 'new_tag_prefix' => $this->config['allow_add']['new_tag_prefix'], 'tag_separators' => $this->config['allow_add']['tag_separators'] - ), + ], 'delay' => $this->config['delay'], 'text_property' => null, - 'placeholder' => '', + 'placeholder' => false, 'language' => $this->config['language'], 'required' => false, 'cache' => $this->config['cache'], 'cache_timeout' => $this->config['cache_timeout'], 'transformer' => null, 'autostart' => true, - 'width' => isset($this->config['width']) ? $this->config['width'] : null, - 'req_params' => array(), + 'width' => $this->config['width'] ?? null, + 'req_params' => [], 'property' => null, 'callback' => null, 'class_type' => null, - ) + 'query_parameters' => [], + 'render_html' => $this->config['render_html'] ?? false + ] ); } /** - * pre Symfony 3 compatibility - * - * @return string - */ - public function getName() - { - return $this->getBlockPrefix(); - } - - /** - * Symfony 2.8+ - * * @return string */ public function getBlockPrefix() diff --git a/README.md b/README.md index fe2748d..0647f7f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -[Note from project maintainer: June 17th 2019](https://github.com/tetranz/select2entity-bundle/issues/144) - select2entity-bundle ==================== @@ -7,7 +5,8 @@ select2entity-bundle This is a Symfony bundle which enables the popular [Select2](https://select2.github.io) component to be used as a drop-in replacement for a standard entity field on a Symfony form. -It works with Symfony 2, 3 and 4. +It works with Symfony 4 and 5. For Symfony 2 and 3, please use version or 2.x of the bundle. +For Select2 4.0 and above. For older versions, use version 1.x of the bundle (not compatible with Symfony 5). The main feature that this bundle provides compared with the standard Symfony entity field (rendered with a html select) is that the list is retrieved via a remote ajax call. This means that the list can be of almost unlimited size. The only limitation is the performance of the database query or whatever that retrieves the data in the remote web service. @@ -53,32 +52,32 @@ Alternatively, minified versions of select2.js and select2.css can be loaded fro Note that this only works with Select2 version 4. If you are using Select2 version 3.X please use `"tetranz/select2entity-bundle": "1.*"` in `composer.json` * Run `php composer.phar update tetranz/select2entity-bundle` in your project root. -* Update your project `app/AppKernel.php` file and add this bundle to the $bundles array: +* Update your project `config/bundles.php` file and add this bundle to the $bundles array: ```php -$bundles = array( +$bundles = [ // ... - new Tetranz\Select2EntityBundle\TetranzSelect2EntityBundle(), -); + Tetranz\Select2EntityBundle\TetranzSelect2EntityBundle::class => ['all' => true] +]; ``` -* Update your project `app/config.yml` file to provide global twig form templates: +* Update your project `config/packages/twig.yaml` file to provide global twig form templates: ```yaml twig: form_themes: - - 'TetranzSelect2EntityBundle:Form:fields.html.twig' - -``` -On Symfony 4, use `@TetranzSelect2Entity/Form/fields.html.twig` instead of `TetranzSelect2EntityBundle:Form:fields.html.twig` + - '@TetranzSelect2Entity/Form/fields.html.twig' + * Load the Javascript on the page. The simplest way is to add the following to your layout file. Don't forget to run console assets:install. Alternatively, do something more sophisticated with Assetic. +``` + ``` ``` ## How to use -The following is for Symfony 3. The latest version works on both Symfony 2 and Symfony 3 but see https://github.com/tetranz/select2entity-bundle/tree/v2.1 for Symfony 2 configuration and use. +The following is for Symfony 4. See https://github.com/tetranz/select2entity-bundle/tree/v2.1 for Symfony 2/3 configuration and use. Select2Entity is simple to use. In the buildForm method of a form type class, specify `Select2EntityType::class` as the type where you would otherwise use `entity:class`. @@ -89,6 +88,7 @@ $builder ->add('country', Select2EntityType::class, [ 'multiple' => true, 'remote_route' => 'tetranz_test_default_countryquery', + 'remote_params' => [] // static route parameters for request->query 'class' => '\Tetranz\TestBundle\Entity\Country', 'primary_key' => 'id', 'text_property' => 'name', @@ -100,6 +100,11 @@ $builder 'cache_timeout' => 60000, // if 'cache' is true 'language' => 'en', 'placeholder' => 'Select a country', + 'query_parameters' => [ + 'start' => new \DateTime() + 'end' => (new \DateTime())->modify('+5d') + // any other parameters you want your ajax route request->query to get, that you might want to modify dynamically + ], // 'object_manager' => $objectManager, // inject a custom object / entity manager ]) ``` @@ -133,12 +138,15 @@ If text_property is omitted then the entity is cast to a string. This requires i * `autostart` Determines whether or not the select2 jQuery code is called automatically on document ready. Defaults to true which provides normal operation. * `width` Sets a data-width attribute if not null. Defaults to null. * `class_type` Optional value that will be added to the ajax request as a query string parameter. +* `render_html` This will render your results returned under ['html']. The url of the remote query can be given by either of two ways: `remote_route` is the Symfony route. `remote_params` can be optionally specified to provide parameters. Alternatively, `remote_path` can be used to specify the url directly. -The defaults can be changed in your app/config.yml file with the following format. +You may use `query_parameters` for when those remote_params have to be changeable dynamically. You may change them using $('#elem).data('query-parameters', { /* new params */ }); + +The defaults can be changed in your config/packages/tetranzselect2entity.yaml file with the following format. ```yaml tetranz_select2_entity: @@ -146,10 +154,12 @@ tetranz_select2_entity: page_limit: 8 allow_clear: true delay: 500 - language: fr + language: 'fr' cache: false cache_timeout: 0 scroll: true + object_manager: 'manager_alias' + render_html: true ``` ## AJAX Response @@ -332,6 +342,15 @@ Because the handling of requests is usually very similar you can use a service w ### Templating +General templating has now been added to the bundle. If you need to render html code inside your selection results, set the `render_html` option to true and in your controller return data like this: +```javascript +[ + { id: 1, text: 'United Kingdom (Europe)', html: '' }, + { id: 2, text: 'China (Asia)', html: '' } +] +``` + +
If you need further templating, you'll need to override the .select2entity() method as follows. If you need [Templating](https://select2.org/dropdown#templating) in Select2, you could consider the following example that shows the country flag next to each option. Your custom transformer should return data like this: @@ -387,7 +406,7 @@ You also will need to override the following block in your template: {% endblock %} ``` -This block adds all additional data needed to the JavaScript function `select2entityAjax`, like data attribute. In this case we are passing `data-img`. +This block adds all additional data needed to the JavaScript function `select2entityAjax`, like data attribute. In this case we are passing `data-img`.
## Embed Collection Forms If you use [Embedded Collection Forms](http://symfony.com/doc/current/cookbook/form/form_collections.html) and [data-prototype](http://symfony.com/doc/current/cookbook/form/form_collections.html#allowing-new-tags-with-the-prototype) to add new elements in your form, you will need the following JavaScript that will listen for adding an element `.select2entity`: diff --git a/Resources/config/services.xml b/Resources/config/services.xml index f27223d..4cd57eb 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -7,7 +7,7 @@ - + %tetranz_select2_entity.config% diff --git a/Resources/public/js/select2entity.js b/Resources/public/js/select2entity.js index 0c2d801..6bc1b97 100644 --- a/Resources/public/js/select2entity.js +++ b/Resources/public/js/select2entity.js @@ -1,20 +1,22 @@ -(function( $ ) { +(function ($) { $.fn.select2entity = function (options) { this.each(function () { - var request; + let request; // Keep a reference to the element so we can keep the cache local to this instance and so we can // fetch config settings since select2 doesn't expose its options to the transport method. - var $s2 = $(this), + let $s2 = $(this), limit = $s2.data('page-limit') || 0, scroll = $s2.data('scroll'), prefix = Date.now(), + query_parameters = $s2.data('query-parameters'), + render_html = $s2.data('render-html'), cache = []; - var reqParams = $s2.data('req_params'); + let reqParams = $s2.data('req_params'); if (reqParams) { $.each(reqParams, function (key, value) { - $('*[name="'+value+'"]').on('change', function () { + $('*[name="' + value + '"]').on('change', function () { $s2.val(null); $s2.trigger('change'); }); @@ -22,12 +24,12 @@ } // Deep-merge the options - $s2.select2($.extend(true, { + let mergedOptions = $.extend(true, { // Tags support createTag: function (data) { if ($s2.data('tags') && data.term.length > 0) { - var text = data.term + $s2.data('tags-text'); - return {id: $s2.data('new-tag-prefix') + data.term, text: text}; + let text = data.term + $s2.data('tags-text'); + return { id: $s2.data('new-tag-prefix') + data.term, text: text }; } }, ajax: { @@ -39,7 +41,7 @@ var key = prefix + ' page:' + (params.data.page || 1) + ' ' + params.data.q, cacheTimeout = $s2.data('ajax--cacheTimeout'); // no cache entry for 'term' or the cache has timed out? - if (typeof cache[key] == 'undefined' || (cacheTimeout && Date.now() >= cache[key].time)) { + if (typeof cache[key] === 'undefined' || (cacheTimeout && Date.now() >= cache[key].time)) { return $.ajax(params).fail(failure).done(function (data) { cache[key] = { data: data, @@ -59,21 +61,21 @@ request = $.ajax(params).fail(failure).done(success).always(function () { request = undefined; }); - + return request; } }, data: function (params) { - var ret = { + let ret = { 'q': params.term, 'field_name': $s2.data('name'), 'class_type': $s2.data('classtype') }; - var reqParams = $s2.data('req_params'); + let reqParams = $s2.data('req_params'); if (reqParams) { $.each(reqParams, function (key, value) { - ret[key] = $('*[name="'+value+'"]').val() + ret[key] = $('*[name="' + value + '"]').val(); }); } @@ -82,15 +84,26 @@ ret['page'] = params.page || 1; } + if (Array.isArray(query_parameters) || + typeof (query_parameters) === 'object') { + for (var key in query_parameters) { + // prevent overriding required parameters + if (!ret[key]) { + ret[key] = query_parameters[key]; + } + } + } + return ret; }, processResults: function (data, params) { - var results, more = false, response = {}; + let results, more = false, + response = {}; params.page = params.page || 1; if ($.isArray(data)) { results = data; - } else if (typeof data == 'object') { + } else if (typeof data === 'object') { // assume remote result was proper object results = data.results; more = data.more; @@ -100,21 +113,36 @@ } if (scroll) { - response.pagination = {more: more}; + response.pagination = { more: more }; } response.results = results; return response; } } - }, options || {})); + }, options || {}); + if (render_html) { + mergedOptions = $.extend({ + escapeMarkup: function (text) { + return text; + }, + templateResult: function (option) { + return option.html ? option.html : option.text; + }, + templateSelection: function (option) { + return option.text; + } + }, mergedOptions); + } + + $s2.select2(mergedOptions); }); return this; }; -})( jQuery ); +})(jQuery); -(function( $ ) { +(function ($) { $(document).ready(function () { $('.select2entity[data-autostart="true"]').select2entity(); }); -})( jQuery ); +})(jQuery); diff --git a/Resources/views/Form/fields.html.twig b/Resources/views/Form/fields.html.twig index b88da2d..c6b524a 100644 --- a/Resources/views/Form/fields.html.twig +++ b/Resources/views/Form/fields.html.twig @@ -1,31 +1,37 @@ {% block tetranz_select2entity_widget %} {% set attr = attr|merge({ - 'data-ajax--url':remote_path, + 'data-ajax--url': remote_path, 'data-ajax--cache': cache ? 'true' : 'false', 'data-ajax--cache-timeout': cache_timeout|default(0), - 'data-ajax--delay':delay, - 'data-ajax--data-type':"json", - 'data-language':language, - 'data-minimum-input-length':minimum_input_length, - 'data-placeholder':placeholder|trans({}, translation_domain), - 'data-page-limit':page_limit, - 'data-scroll':scroll ? 'true' : 'false', - 'data-autostart':autostart ? 'true' : 'false', - 'class' : (attr.class|default('') ~ ' select2entity form-control')|trim, - 'data-name' : name|e('html_attr') + 'data-ajax--delay': delay, + 'data-ajax--data-type': "json", + 'data-language' :language, + 'data-minimum-input-length': minimum_input_length, + 'data-placeholder': placeholder|trans({}, translation_domain), + 'data-page-limit': page_limit, + 'data-scroll': scroll ? 'true' : 'false', + 'data-autostart': autostart ? 'true' : 'false', + 'class': (attr.class|default('') ~ ' select2entity form-control')|trim, + 'data-name': name|e('html_attr') }) %} {% if allow_add.enabled %} {% set attr = attr|merge({ - 'data-tags':'true', - 'data-tags-text':allow_add.new_tag_text|trans({}, translation_domain), - 'data-new-tag-prefix':allow_add.new_tag_prefix|trans({}, translation_domain), - 'data-token-separators':allow_add.tag_separators, + 'data-tags': 'true', + 'data-tags-text': allow_add.new_tag_text|trans({}, translation_domain), + 'data-new-tag-prefix': allow_add.new_tag_prefix|trans({}, translation_domain), + 'data-token-separators': allow_add.tag_separators, }) %} {% endif %} {% if multiple %} - {% set attr = attr|merge({'multiple':'multiple'}) %} + {% set attr = attr|merge({'multiple': 'multiple'}) %} + {% endif %} + + {% if query_parameters %} + {% set attr = attr|merge({ + 'data-query-parameters': query_parameters|json_encode + }) %} {% endif %} {% if allow_clear %} @@ -36,6 +42,10 @@ {% set attr = attr|merge({'data-width': width}) %} {% endif %} + {% if render_html %} + {% set attr = attr|merge({'data-render-html': 'true'}) %} + {% endif %} + {% if class_type %} {% set attr = attr|merge({'data-classtype': class_type}) %} {% endif %} diff --git a/composer.json b/composer.json index c6e14a0..7a23ddd 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,15 @@ } ], "require": { - "php": ">=5.4.0", + "php": ">=7.1.3", "doctrine/orm": ">=2.4", - "twig/twig": ">=2.9" + "twig/twig": ">=2.9", + "symfony/property-access": ">=4.0", + "symfony/dependency-injection": ">=4.0", + "symfony/http-kernel": ">=4.0", + "symfony/form": ">=4.0", + "symfony/config": ">=4.0", + "symfony/routing": ">=4.0" }, "autoload": { "psr-4": { "Tetranz\\Select2EntityBundle\\": "" }