Внедряем Bootstrap 3 Datepicker в SonataAdminBundle

от автора

В этой маленькой заметке я расскажу о том, как подключить удобный datepicker в админку Symfony. По умолчанию datepicker в SonataAdminBundle выглядит так:

А мы его превратим в удобные и красивые контролы:

Те, кто еще мучаются с неудобным datepicker-ом, добро пожаловать под кат.

Я не буду рассказывать о том, как установить SonataAdminBundle, об этом можно прочитать в этой статье. Я предполагаю, что у вас уже установлено приложение и админка. Ну чтож, приступим.

Тип поля datetime

Первое с чего стоит начать это создание нового поля формы как описано в документации. Его нужно обязательно создавать в namespace <vendor_name>\<bundle_name>\Form\Type\ иначе будет ругаться SensioLabsInsight при тестировании.

namespace Acme\Bundle\DemoBundle\Form\Type\Field;  use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\DataTransformerInterface;  // Symfony >=2.8 //use Symfony\Component\Form\Extension\Core\Type\TextType;  class DateTime extends AbstractType implements DataTransformerInterface {     public function buildForm(FormBuilderInterface $builder, array $options)     {         // результатом заполнения форму является строка и ее необходимо конвертировать в \DateTime         $builder->addModelTransformer($this);     }      public function transform($value)     {         return $value; // нужно для интерфейса DataTransformerInterface     }      public function reverseTransform($value)     {         // собственно конвертирование значения в \DateTime         return $value instanceof \DateTime ? $value : new \DateTime($value);     }      public function buildView(FormView $view, FormInterface $form, array $options)     {         // объект даты нужно преобразовать в строку         if ($form->getData() instanceof \DateTime) {             $view->vars['value'] = $form->getData()->format('Y-m-d H:i');         }         // css класс для bootstrap форм         $view->vars['attr']['class'] = 'form-control';     }      public function configureOptions(OptionsResolver $resolver)     {         // без указания data_class не работает DataTransformer         $resolver->setDefaults([              'data_class' => \DateTime::class         ]);     }      public function getParent()     {         // Symfony >=2.8         //return TextType::class;         // Symfony <2.8         return 'text';     }      public function getName()     {         return 'datetime'; // мы потом перегрузим стандартный datetime     } } 

Я не делал перенос стандартных опций datetime в новый класс за ненадобностью, но вы можете это сделать если вам это необходимо. В разделе Тип поля Time я опишу как это сделать на примере опции with_seconds.

Следующим пунктом нашей программы будет создание общего шаблон форм (темы для форм). В нем мы наследуемся от темы Sonata и переопределим шаблон даты. Шаблон сохраняем в файд Resources/views/Form/fields.html.twig . Можно выбрать и другой путь, но мне так привычней.

{% extends 'SonataAdminBundle:Form:form_admin_fields.html.twig' %}  {% block datetime_widget %} {% spaceless %}     <div class="input-group date form-field-datetime">         <input type="text" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>         <span class="input-group-addon">             <span class="glyphicon glyphicon-calendar"></span>         </span>     </div> {% endspaceless %} {% endblock datetime_widget %} 

Класс form-field-datetime нам потом будет нужен для навешивания JavaScript. Теперь укажем Sonata что ей необходимо использовать другую тему для форм прописав в конфиге app/config/config.yml следующие строчки:

sonata_doctrine_orm_admin:     templates:         form: [ AcmeDemoBundle:Form:fields.html.twig ] 

Не забываем создать сервис для нового поля формы:

    acme.demo.form.type.datetime:         class: Acme\Bundle\DemoBundle\Form\Type\Field\DateTime         public: false 

Мы не создаем метку для сервиса как описано тут потому что мы не создаем новое поля и будем перегружать старое. По той же причине он нам не нужен в публичном доступе. Теперь приступим к перезаписи стандартных полей формы. Создадим компилятор для DI контейнера:

namespace Acme\Bundle\DemoBundle\DependencyInjection\Compiler;  use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder;  class FormTypePass implements CompilerPassInterface {     public function process(ContainerBuilder $container)     {         $container->setAlias('form.type.datetime', 'acme.demo.form.type.datetime');     } } 

Здесь мы указываем что form.type.datetime является псевдонимом для нашего, вновь созданного сериса acme.demo.form.type.datetime. Таким образом когда в формах мы будем создавать поле типа datetime будет использоваться наш сервис. Так мы меняем контрол не меняя код проекта. Теперь подключим компилятор в бандл:

namespace Acme\Bundle\DemoBundle  use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; use Acme\BundleDemoBundle\DependencyInjection\Compiler\FormTypePass;  class AcmeDemoBundle extends Bundle {     public function build(ContainerBuilder $container)     {         parent::build($container);         $container->addCompilerPass(new FormTypePass());     } } 

Сейчас datepicker уже имеет красивую и удобную форму, осталось только навесить JavaScript для открытия выпадающего окна с выбором даты.

Устанавливать мы будем Bootstrap 3 Datepicker который есть на packagist.org, за что им большое спасибо. Пропишем зависимость в composer.json:

{     "require": {         …         "eonasdan/bootstrap-datetimepicker": "~4.17.37",         …     } } 

При такой установки пакета его удобней подключать через assetic что мы и сделаем. Прописываем в app/config/config.yml следующие строчки:

assetic:     assets:         admin-js:             inputs:                 - '%kernel.root_dir%/../vendor/eonasdan/bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js'                 - '@AcmeDemoBundle/Resources/public/js/admin.js'             output: js/admin.js  sonata_admin:     templates:         layout: AcmeDemoBundle:Admin:standard_layout.html.twig 

Мы определили файл js/admin.js в который будет билдиться наш datepicker и JavaScript код который его инициализирует и навешивает на соответствующие поля формы. Этот файл будет лежать по адресу web/js/admin.js. Так же мы переопределили лайаут Sonata для того что бы подключить наш JavaScript. Давайте этим и займемся:

{% extends 'SonataAdminBundle::standard_layout.html.twig' %}  {% block javascripts %}     {{ parent() }}     <script src="{{ asset('js/admin.js') }}" type="text/javascript"></script> {% endblock %} 

Теперь мы создадим файл Resources/public/js/admin.js в котором обвяжем наши поля формы JavaScript-ом.

$(function(){     $('.form-field-datetime').datetimepicker({         format: 'YYYY-MM-DD HH:mm',         locale: 'ru'     }); }); 

Вот собственно и все. Выполняем сбору assetic и радуемся жизни:

app/console assetic:dump web --no-debug 

Тип поля date

По аналогии создаем поле date с небольшими отличиями. Класс для поля формы:

namespace Acme\Bundle\DemoBundle\Form\Type\Field;  use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\DataTransformerInterface;  // Symfony >=2.8 //use Symfony\Component\Form\Extension\Core\Type\TextType;  class Date extends AbstractType implements DataTransformerInterface {     public function buildForm(FormBuilderInterface $builder, array $options)     {         $builder->addModelTransformer($this);     }      public function transform($value)     {         return $value;     }      public function reverseTransform($value)     {         return $value instanceof \DateTime ? $value : new \DateTime($value);     }      public function buildView(FormView $view, FormInterface $form, array $options)     {         if ($form->getData() instanceof \DateTime) {             $view->vars['value'] = $form->getData()->format('Y-m-d');         }          $view->vars['attr']['class'] = 'form-control';     }      public function configureOptions(OptionsResolver $resolver)     {         $resolver->setDefaults([              'data_class' => \DateTime::class         ]);     }      public function getParent()     {         // Symfony >=2.8         //return TextType::class;         // Symfony <2.8         return 'text';     }      public function getName()     {         return 'date';     } } 

Шаблон:

{% block date_widget %} {% spaceless %}     <div class="input-group date form-field-date">         <input type="text" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>         <span class="input-group-addon">             <span class="glyphicon glyphicon-calendar"></span>         </span>     </div> {% endspaceless %} {% endblock date_widget %} 

Сервис:

    acme.demo.form.type.date:         class: Acme\Bundle\DemoBundle\Form\Type\Field\Date         public: false 

Добавляем псевдоним:

// .. class FormTypePass implements CompilerPassInterface {     public function process(ContainerBuilder $container)     {         // ..         $container->setAlias('form.type.date', 'acme.demo.form.type.date');     } } 

Ну и JavaScript:

$(function(){     // ..      $('.form-field-date').datetimepicker({         format: 'YYYY-MM-DD',         locale: 'ru'     }); }); 

Тип поля time

По аналогии с предыдущими, но с небольшими отличиями. В нашем проекте публикуются видео ролики и необходимо в админке указывать их продолжительность. Для этого мы используем поле time и выставляем ему опцию with_seconds в true. В новом поле формы нужно было сохранить эту функциональность.

namespace Acme\Bundle\DemoBundle\Form\Type\Field;  use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\DataTransformerInterface;  // Symfony >=2.8 //use Symfony\Component\Form\Extension\Core\Type\TextType;  class Time extends AbstractType implements DataTransformerInterface {     public function buildForm(FormBuilderInterface $builder, array $options)     {         $builder->addModelTransformer($this);     }      public function transform($value)     {         return $value;     }      public function reverseTransform($value)     {         return $value instanceof \DateTime ? $value : new \DateTime($value);     }      public function buildView(FormView $view, FormInterface $form, array $options)     {         if ($form->getData() instanceof \DateTime) {             // формат даты соответственно различается             $view->vars['value'] = $form->getData()->format($options['with_seconds'] ? 'H:i:s' : 'H:i');         }         $view->vars['attr']['class'] = 'form-control';          // сохраняем переменную для шаблона         $view->vars['with_seconds'] = $options['with_seconds'];     }      public function configureOptions(OptionsResolver $resolver)     {         $resolver->setDefaults([             'data_class' => \DateTime::class,             'with_seconds' => false // по умолчанию опция выключена         ]);     }      public function getParent()     {         // Symfony >=2.8         //return TextType::class;         // Symfony <2.8         return 'text';     }       public function getName()     {         return 'time';     } } 

Шаблон:

{% block time_widget %} {% spaceless %}     <div class="input-group date form-field-time" data-with-seconds="{{ with_seconds == true ? 1 : 0 }}">         <input type="text" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>         <span class="input-group-addon">             <span class="glyphicon glyphicon-time"></span>         </span>     </div> {% endspaceless %} {% endblock time_widget %} 

Сервис:

    acme.demo.form.type.time:         class: Acme\Bundle\DemoBundle\Form\Type\Field\Time         public: false 

Добавляем псевдоним:

// .. class FormTypePass implements CompilerPassInterface {     public function process(ContainerBuilder $container)     {         // ..         $container->setAlias('form.type.time', 'acme.demo.form.type.time');     } } 

JavaScript будет немного отличатся:

$(function(){     // ..      $('.form-field-time') .each(function () {         var el = $(this),             options = {locale: 'ru'};         if (el.data('with-seconds') == 1) {             options.format = 'HH:mm:ss';         } else {             options.format = 'HH:mm';         }         el.datetimepicker(options); }); 

Заключение

В место заключения скажу, что есть очень интересная библиотека ClockPicker для выбора времени. Если она вас заинтересует, то вы с легкостью сможете подключить ее по моим примерам.

ссылка на оригинал статьи http://habrahabr.ru/post/272513/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *