В этой маленькой заметке я расскажу о том, как подключить удобный 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/
Добавить комментарий