Пишем виджет для field ActiveForm
  • 1818

Yii2 Создание виджета переключателя вместо стандартного input ActiveForm

Автор: admin | 19 мая (Сб.) 2018г. в 22ч.21м.

Что такое switch control для формы.

Для работы с формами в yii2 есть замечательный виждет ActiveForm, который оснащен валидацией с выводом
сообщений об ошибках и прочими полезностями.
ActiveForm имеет множество настроек и поддерживает всенеобходимые поля, такие как textarea, checkbox, radio, input и т.д. Но иногда возможностей, которые есть в виджете не достаточно и нужно как-то расширить функционал или изменить внешний вид полей ввода.
Я хочу поделиться опытом создания расширения или точнее виджета для ActiveForm на примере создания переключателя на основе checkbox. Данный переключатель может применяться где угодно. Займемся созданием switch переключателя...

В моем примере switch перключатель служит для установки статуса публикации статьи в форме создания/редактирования в административной части сайта.

Ново испеченый виджет для поля формы yii2

Для реализации задуманного нужен jquery плагин, которых множество в интернете. Я выбрал простой и
симпатичный плагин от Dario Montalbano под названием jQuery mSwitch v.1, который я нашел на просторах
интернета. Но плагин может быть любым, не в этом суть.

Итак, для назначения статуса у меня в базе данных есть поле 'flagActive' которое может ровняться
или 0 или если статься активирована - 1.

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

Виджет красивее чем стандартное поле

Создание switch виджета для ActiveForm. 

Теперь можно приступать к созданию Switch виджета. Создаем в папке widgets папку с
именем IosStyleToggleSwitch. В данной дирректории и будет расположен виджет.

Структура директорий  виджета

Создаем файл виджета и IosStyleToggleSwitchWidget.php , файл IosStyleToggleSwitchAsset.php

Стили и скрипты будут храниться в ../assets/css и ../assets/js соответственно.
Как я уже говорил, я выбрал готовый плагин, скачал его и скопировал только css и js в
указанные папки.

Теперь пишем код в файл ресурсов IosStyleToggleSwitchAsset.php. Тут будут подключаться
css и js файлы, добавленные из плагина.

<?php

namespace common\widgets\IosStyleToggleSwitch;

use yii\web\AssetBundle;

/**
 * Main frontend application asset bundle.
 */
class IosStyleToggleSwitchAsset extends AssetBundle
{
    public $sourcePath = (__DIR__ . '/assets');
    
    public $css = ["css/jquery.mswitch.css"];
    public $js = ["js/jquery.mswitch.js"];
    
    public $jsOptions = [
        'position' => \yii\web\View::POS_END,
    ];
    
    public $depends = [
        "backend\assets\AppAsset"
    ];
    
    public $publishOptions = [
        'forceCopy'=>true,
    ];
    
    public function init()
    {
        
        parent::init();
    }
 
    
    
}
​

Устанавливаем 'position' => \yii\web\View::POS_END расположение скрипта в конце html файла перед  </body>

Ставим зависимости в

public $depends = [
"backend\assets\AppAsset"
]; ​

В данном случае AppAsset это мой главный файл ресурсов, где установлены все основные
скрипты и стили сайта, в том числе и подключена jquery, которая нужна для нашего
плагина.

Далее я думаю код ясен.

Теперь приступим к виджету IosStyleToggleSwitchWidget.php .Выкладываю сразу весь код
виджета, а далее буду разьяснять особенности:

<?php
namespace common\widgets\IosStyleToggleSwitch;

use Yii;
use yii\base\InvalidConfigException;
use yii\base\InvalidArgumentException;
use yii\base\Widget;
use yii\helpers\Html;
use yii\helpers\Url;
use yii\web\View;
use yii\widgets\InputWidget;

/**
 * Виджет 
 */

class IosStyleToggleSwitchWidget extends InputWidget
{
    const CHECKBOX = "checkbox";
//    const RADIO = "radio";
    
    /*
     * Это поле означает тип input ,который будет использован. В данной версии 
     * используется только checkbox, однако в будущем может прибавиться еще и radio
     */
    public $type;
    
    
    /*
     * Этот класс назначается input checkbox для того, чтобы можно было
     * с ним работать через javascript
     */
    public $class = 'm_switch_check';

    /*
     * Если переключатель включен
     */
    public $turnOnValue = 1;

    
    /*
     * Если переключатель выключен
     */
    public $turnOffValue = 0;
    
    /*
     * Просто массив возможных типов полей для проверки поддержки в виджете
     */
    public $typesList = [
        self::CHECKBOX, 
//        self::RADIO
            ];
    
    public function init()
    {
        parent::init();

        if ($this->type == null) {
            $this->type = self::CHECKBOX;
            
        }
        
        if (empty($this->type) || !in_array($this->type, $this->typesList)) {
            throw new InvalidConfigException("Type not set. Type must be either checkbox or radio.");
        }
        
        
        
    }

    public function run()
    {
        parent::run();
        
        if($this->type === self::CHECKBOX){
            $input = $this->getCheckbox($this->type);//строим input
        }
       
        $this->registerAssets();//превращаем input checkbox в красивый переключатель
 
        echo $input;//выводим переключатель
    }

    //Этот метод можно было бы не создавать, но тогда 
    //метод run() был бы менее читаем
    protected function getCheckbox($type)
    {
        
        if (empty($this->options['label'])) {
                $this->options['label'] = null;
        }
        
        $options = \yii\helpers\ArrayHelper::merge(
                $this->options, ['class' => $this->class]
        );
        
//        var_dump($this->value);
        
        if ($this->hasModel()) {
            $input = 'active' . ucfirst($type);
            return Html::$input($this->model, $this->attribute, $options);
            
        } else {
            if ($type == self::CHECKBOX) {
                $input = $type;
                $checked = false;
                return Html::$input($this->name, $checked, $options);
            }else{
                throw new InvalidArgumentException("Field type not supported");
            }
            
        }
    }
    
    /**
     * Register assets.
     */
    protected function registerAssets()
    {
        /*
         * Регистрируем стили и скрипты плагина
         */
        $view = $this->getView();
        IosStyleToggleSwitchAsset::register($view);
       
        /*
         * Регистрируем скрипт, который применится к чекбоксу и видоизменит его
         * в красивый switch. Также меняем value input checkbox при событиях 
         * переключения свича. Скрипт отработает, когда html документ будет построен
         * 
         */
        $js = <<<JS
        
        $(".{$this->class}").mSwitch({
            onRendered: function(){},
            onRender: function(elem){},
            onTurnOn: function(elem){
//              console.log(elem);
                elem.val({$this->turnOnValue});
            },
            onTurnOff: function(elem){
                elem.val({$this->turnOffValue});
            }
        });

        
JS;

        $view->registerJs($js, \yii\web\View::POS_READY);
        
    }
}
​

Во первых хочу отметить, что не зависимо от графической оболочки в виджете используется
обычный input как поле ввода информации в форме.
Итак наш виджет наследует yii\widgets\InputWidget . Так удобнее потому, что отнаследовав
InputWidget мы получаем доступ к модели и данным поля.

В файле вида подключаем виджет так :
        <?php $form = ActiveForm::begin(); ?>
        
        <?php //тут какие-то еще поля ?>

        <?= $form->field($model, 'metaTitle')->textInput(['maxlength' => true]) ?>

        <?= $form->field($model, 'metaDesc')->textarea(['rows' => 6]) ?>

        <?php //НАШ ВИДЖЕТ ?>
        <?php 
            echo $form->field($model, 'flagActive')->widget(IosStyleToggleSwitchWidget::classname(), [
                'type' => IosStyleToggleSwitchWidget::CHECKBOX
            ]); 
        ?>

        <?php //СТАРЫЙ INPUT ?>
        <?php 
         //echo $form->field($model, 'flagActive')->dropDownList(
         //                  BlogArticles::$statusesName, 
         //                  ['prompt'=>'Назначить статус']); 
        ?>

        <div class="form-group">
        <?= Html::submitButton(Yii::t('app/admin', 'Save'), ['class' => 'btn btn-success']) ?>
        </div>

        <?php ActiveForm::end(); ?>​

Весь код активной формы обрамлен статическими методами виджета ActiveForm::begin() и ActiveForm::end() .
В общем это стандартное устройство виджета в yii2, когда весь контент между  begin()
сохраняется в буфер и выводится при вызове end() . В общем почитать про виджеты можно и в
документации к yii2.

Перейдем к формированию полей ввода. Итак в $form = ActiveForm::begin() попадает
объект класса yii\widgets\ActiveForm . Далее исходя из конструкции $form->field($model, 'flagActive')
у объекта активной формы есть метод field .Взглянем на него, перейдя в файл ActiveForm.php.
public function field($model, $attribute, $options = [])
    {
        $config = $this->fieldConfig;
        if ($config instanceof \Closure) {
            $config = call_user_func($config, $model, $attribute);
        }
        if (!isset($config['class'])) {
            $config['class'] = $this->fieldClass;
        }

        return Yii::createObject(ArrayHelper::merge($config, $options, [
            'model' => $model,
            'attribute' => $attribute,
            'form' => $this,
        ]));
    }​

Тут видно что метод возвращает объект класса ActiveField
котрый установлен в свойстве public $fieldClass = 'yii\widgets\ActiveField';

Если взглянуть на класс yii\widgets\ActiveField ,то тут можно встретить
те самые методы, которые формируют поля формы, такие как ->textInput(['maxlength' => true])
и ->textarea(['rows' => 6]) и все остальные. И среди прочих есть тот самый метод widget,
который и позволяет кастомизировать формирование полей.
public function widget($class, $config = [])
    {
        /* @var $class \yii\base\Widget */
        $config['model'] = $this->model;
        $config['attribute'] = $this->attribute;
        $config['view'] = $this->form->getView();
        if (is_subclass_of($class, 'yii\widgets\InputWidget')) {
            $config['field'] = $this;
            if (isset($config['options'])) {
                if ($this->form->validationStateOn === ActiveForm::VALIDATION_STATE_ON_INPUT) {
                    $this->addErrorClassIfNeeded($config['options']);
                }

                $this->addAriaAttributes($config['options']);
                $this->adjustLabelFor($config['options']);
            }
        }

        $this->parts['{input}'] = $class::widget($config);

        return $this;
    }​

Еще раз обращаю внимание на вызов вновь созданного переключателя в файле вида _form.php
<?php 
    echo $form->field($model, 'flagActive')->widget(IosStyleToggleSwitchWidget::classname(), [
        'type' => IosStyleToggleSwitchWidget::CHECKBOX
    ]); 
?>​

Видно, что в качестве первого аргумента метода widget передается имя класса (не забываем
про пространство имен). Второй - массив конфигурации $config = []. Те параметры,
которые попадают в виджет, в данном случае в IosStyleToggleSwitchWidget и заполняют
публичные свойства виджета.

Из этой строчки видно if (is_subclass_of($class, 'yii\widgets\InputWidget')) { ,что
если наш виджет наследуется (как я и сделал) от класса yii\widgets\InputWidget ,то
в виджете будет доступен и объект класса yii\widgets\ActiveField со всеми публичными
свойствами и методами.

Вот сам класс yii\widgets\InputWidget от которого нужно наследоваться.
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\widgets;

use Yii;
use yii\base\InvalidConfigException;
use yii\base\Model;
use yii\base\Widget;
use yii\helpers\Html;

/**
 * InputWidget is the base class for widgets that collect user inputs.
 *
 * An input widget can be associated with a data [[model]] and an [[attribute]],
 * or a [[name]] and a [[value]]. If the former, the name and the value will
 * be generated automatically (subclasses may call [[renderInputHtml()]] to follow this behavior).
 *
 * Classes extending from this widget can be used in an [[\yii\widgets\ActiveForm|ActiveForm]]
 * using the [[\yii\widgets\ActiveField::widget()|widget()]] method, for example like this:
 *
 * ```php
 * <?= $form->field($model, 'from_date')->widget('WidgetClassName', [
 *     // configure additional widget properties here
 * ]) ?>
 * ```
 *
 * For more details and usage information on InputWidget, see the [guide article on forms](guide:input-forms).
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class InputWidget extends Widget
{
    /**
     * @var \yii\widgets\ActiveField active input field, which triggers this widget rendering.
     * This field will be automatically filled up in case widget instance is created via [[\yii\widgets\ActiveField::widget()]].
     * @since 2.0.11
     */
    public $field;
    /**
     * @var Model the data model that this widget is associated with.
     */
    public $model;
    /**
     * @var string the model attribute that this widget is associated with.
     */
    public $attribute;
    /**
     * @var string the input name. This must be set if [[model]] and [[attribute]] are not set.
     */
    public $name;
    /**
     * @var string the input value.
     */
    public $value;
    /**
     * @var array the HTML attributes for the input tag.
     * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
     */
    public $options = [];


    /**
     * Initializes the widget.
     * If you override this method, make sure you call the parent implementation first.
     */
    public function init()
    {
        if ($this->name === null && !$this->hasModel()) {
            throw new InvalidConfigException("Either 'name', or 'model' and 'attribute' properties must be specified.");
        }
        if (!isset($this->options['id'])) {
            $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId();
        }
        parent::init();
    }

    /**
     * @return bool whether this widget is associated with a data model.
     */
    protected function hasModel()
    {
        return $this->model instanceof Model && $this->attribute !== null;
    }

    /**
     * Render a HTML input tag.
     *
     * This will call [[Html::activeInput()]] if the input widget is [[hasModel()|tied to a model]],
     * or [[Html::input()]] if not.
     *
     * @param string $type the type of the input to create.
     * @return string the HTML of the input field.
     * @since 2.0.13
     * @see Html::activeInput()
     * @see Html::input()
     */
    protected function renderInputHtml($type)
    {
        if ($this->hasModel()) {
            return Html::activeInput($type, $this->model, $this->attribute, $this->options);
        }
        return Html::input($type, $this->name, $this->value, $this->options);
    }
}
​

Тут есть доступ и к value поля и к options, который использовался для отключения label
чтобы не дублировать label -  $this->options['label'] = null;

Виджет должен возвращать поле формы, которое формируется с помощью Html хелперов для
построения тегов с которыми можно ознакомиться в клвссе yii\helpers\Html .Ну а сам input
оформляется как душе угодно, главное, чтобы нужное значение попадало в поле формы.

Когда в форме выводится объект:
<?php 
    echo $form->field($model, 'flagActive')->widget(IosStyleToggleSwitchWidget::classname(), [
        'type' => IosStyleToggleSwitchWidget::CHECKBOX
    ]); 
?>​

то в классе ActiveField сробатывает метод. Вот тут содержится магия появления поля
на экран. Вы далее можете посмотреть самостоятельно что здесь вызывается и как
происходит вывод формы.
public function __toString()
{
    // __toString cannot throw exception
    // use trigger_error to bypass this limitation
    try {
        return $this->render();
    } catch (\Exception $e) {
        ErrorHandler::convertExceptionToError($e);
        return '';
    }
}​

Финал :

Ну на этом все. Думаю в дальнейшем снять видео, где будет все более наглядно,
так как по-моему такие вещи более понятны когда строится код перед глазами, по крайней
мере для меня.

Приветствую!

Меня зовут Сергей. Я - автор этого блога.

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