Контекстно-зависимая форма в Yii

в 7:31, , рубрики: Bootstrap, form, framework, yii, Песочница, метки: , , ,

При работе с фреймворками всегда приходится создавать основной функционал самим, желательно, используя при этом возможности фреймворка (зачем тогда он нам нужен). Как понятно по заголовку речь пойдет про контекстно-зависимые формы в Yii. В статье описана реализация подобной формы, используя модальное окошко. Надеюсь, что кому то будет полезен именно такой вариант.

Забегая вперёд скажу, что в результате должно получиться вот такое поле, выпадающее меню и кнопка справа для выбора нужных данных.

Контекстно зависимая форма в Yii

Контекстно-зависимое окошко выбора:

Контекстно зависимая форма в Yii

После выбора:

Контекстно зависимая форма в Yii

Почему был выбран вариант с модальным окном? Это позволяет упростить выбор нужного элемента. В модальном окне можно выводить CGridView, при этом можно сортировать, искать и фильтровать данные, что очень удобно, если выбирать приходится из большого количества элементов.
Итак, ингредиенты для приготовления этой Yiiшницы:

Теперь, наберемся терпения и начнем приготовление.

Допустим, у нас есть контроллер menuController.php, view файл _form.php и модель Menu.php. Не буду останавливаться на том, кто есть кто, всё это стандартный, автоматически сгенерированный код Gii, Гы.

Модель (Menu.php)

Сначала уточним, как работает модель. Будем считать, что у нас есть связка из типа данных и ID, которое привязано к типу данных. В модели (так же и в БД) должны присутствовать поля type и data_id, оба целочисленные. Для удобства при загрузке модели, будем загружать наименование объекта, на который он ссылается (понадобится при редактировании объекта). Допустим в поле data_name. Провернём нужные махинации в функции afterFind(). Так же сделаем список типов данных более динамичным. Определим статический массив в классе, таким образом, если нам нужно будет расширять или же сужать варианты типов, мы будем редактировать только класс модели.
Т.к. мы используем ActiveRecord то, по сути, необходимо знать только название модели и вьюшки для работы с конкретно взятым типом данных.

    static $types = array(
        1 => array("name" => "Страница",    "model" => "Pages", "view" => "pages_grid"),
        2 => array("name" => "Документ",    "model" => "Docs",  "view" => "docs_grid"),
        3 => array("name" => "Папка",       "model" => "Cats",  "view" => "cats_grid"),
    );

    public static function getSimpleTypes() { // возвращает список типов. Нужен для DropDownList
        $st = array();
        foreach (Menu::$types as $key => $value)
            $st[$key] = $value["name"];
        return $st;
    }
    
    var $data_name;
    public function afterFind() {
        $dataModel = Menu::$types[$this->type]['model']::model()->findByPk($this->data_id);
        $this->data_name = $dataModel->title; // Присваиваем переменной $data_name название выбранного элемента
        parent::afterFind();
    }

На этом рассмотрение модели закончим.

View формы (_form.php)

Смотрим view файл нашей формы _form.php

<!-- Создаем стандартную форму --> 
<?php $form=$this->beginWidget('bootstrap.widgets.TbActiveForm',array(
        'id'=>'menu-form',
        'enableAjaxValidation'=>false,
        'type' => 'horizontal',
)); ?>
<!-- Здесь могут быть Ваши поля формы -->         
    <?php echo $form->errorSummary($model);?>
    <?php echo $form->textFieldRow($model,'name');?>

<!-- Скрытое поле, в котором лежит data_id -->  
    <?php echo $form->textField($model,'data_id',array('class'=>'hide')); ?>

<!-- Сами пишем обертку будущего поля согласно правилам bootstrap-->  
    <div class="control-group">
        <div class="control-label">
            <?=$form->labelEx($model,'type')?>
        </div>
        <div class="controls">
            <div class="input-append">
                <?php echo $form->dropDownList($model,'type',Menu::getSimpleTypes());?>
                <button class="btn" id="data-select-btn" data-loading-text="..." type="button"><i class="icon-list"></i></button>
            </div>
        </div>
    </div>

<!-- Уведомление о том, что элемент был выбран. Крайне необходим для действия Update, чтобы было видно, что выбрано-->          
    <div id="data-info" class="alert alert-success controls <?if($model->isNewRecord):?>hide<?endif;?>">
        <i class="icon-file"></i>
        <span class="info">
            <?if(!$model->isNewRecord) echo $model->data_name?>
        </span>
    </div>

<!-- Кнопка создания/сохранения-->      
    <div class="form-actions">
        <?php $this->widget('bootstrap.widgets.TbButton', array(
                'buttonType'=>'submit',
                'type'=>'primary',
                'label'=>$model->isNewRecord ? 'Create' : 'Save',
        )); ?>
    </div>

<?php $this->endWidget(); ?>

<!-- Модальное окошко для выбора нужного материала-->      
<?php $this->beginWidget('bootstrap.widgets.TbModal', array('id'=>'dataModal')); ?>
    <div class="modal-header">
        <a class="close" data-dismiss="modal">×</a>
        <h4><?=Yii::t("menu", "Выберите материал")?></h4>
    </div>
    <div class="modal-body"></div>
    <div class="modal-footer">
        <?php $this->widget('bootstrap.widgets.TbButton', array(
            'label'=>Yii::t("menu", "Отмена"),
            'url'=>'#',
            'htmlOptions'=>array('data-dismiss'=>'modal'),
        )); ?>
    </div>
<?php $this->endWidget(); ?>
        
<script>
// Функция для вызова из модального окошка
    function insertPageUrl(id, name) {
        $("#Menu_data_id").val(id);
        $("#data-info .info").html(name);
        $("#data-info").show();
        $('#dataModal').modal("hide");
    }

// Обнуляем data_id если меняем тип данных    
    $('#Menu_type').change(function(){
        $("#Menu_data_id").val("");
        $("#data-info").hide();
    })

// Функция которая показывает модальное окно с данными для выбора, полученными через AJAX
    $('#data-select-btn').click(function(){
        var buttn = this; 
        $(buttn).button('loading');
        $.ajax({
          url: "<?php echo $this->createAbsoluteUrl('menu/loadData') ?>",
          cache: false,
          data: "type="+$("#Menu_type").val(),
          success: function(html){
            $(".modal-body").html(html);       
            $(buttn).button('reset');
            $('#dataModal').modal().css({
                width: 'auto',
                'margin-left': function () {
                    return -($(this).width() / 2);
                },
            });
          }
          
        });
    })
</script>

Собственно это весь файл формы. Основные элементы тут, это наше поле и функция для AJAX запроса данных.
Как вы можете видеть, функция получения данных, обращается к экшну menu/loadData. Посмотрим, что он делает:

Контроллер (menuController.php)

    public function actionLoadData($type)
    {
        $model_name = Menu::$types[$type]['model']; 
        $model = new $model_name('search'); // создаем модель данных нужного типа

        if(isset($_GET[$model_name])) // чтобы работали функции поиска нужно передать параметры в модель
                $model->attributes=$_GET[$model_name];

        $this->renderPartial(Menu::$types[$type]['view'],array(
                'model'=>$model,
        ), false, true); // обязательно ставим $processOutput = true, чтобы работали скрипты в модальном окошке.
    }

Стоить отметить, что нужно установить для функции renderPartial параметр $processOutput = true, иначе не будут загружены скрипты, подключаемые виджетами в view файле.
Еще нужно уточнить очень важный момент, при установке параметра $processOutput, будут подгружены все файлы, включая и те, что уже были подключены на главной странице, что очень критично в случае, например, JQuery. Поэтому, советую установить расширение NLSClientScript, оно проконтролирует, чтобы все файлы подключались единожды.

View файл для модального окошка (docs_grid.php)

View файлы надо хранить в предназначенной для этого папке контроллера, т.е. в нашем случае это будет /protected/views/menu
Теперь разберем один из view файлов для модального окошка. Можно использовать любой удобный вид для выбора данных, главное чтобы он работал через объект модели. Мне нравится CGridView, т.к. в нём есть все необходимое: поиск, пагинация и сортировка.

$this->widget('bootstrap.widgets.TbGridView',array(
	'id'=>'docs-grid',
	'dataProvider'=>$model->search(),
	'filter'=>$model,
	'columns'=>array(
            'id',
            'title',
            'updated',
            array(
                'class'=>'CButtonColumn',
                'template' => "{insert}",
                'buttons' => array(
                    "insert" => array(
                        'label' => "выбрать",
                        'options' => array(
                            "class" => "btn btn-mini btn-success",
                            "onclick" => 'insertPageUrl($(this).parent().parent().children(":nth-child(1)").text(),$(this).parent().parent().children(":nth-child(2)").text());',
                        )
                    ),
                )
            ),
	),
));

Тут ничего лишнего, один единственный грид с кнопками выбора элемента. В примере использован грид от bootstrap, но можно использовать стандартный CGridView.
Но тут есть одна хитрость. Как вы заметили, на кнопке выбора элемента висит невзрачный скрипт, выдирающий из таблицы id и title элемента. К сожалению, виджет GridView не позволяет оперировать данными отдельно взятой записи. Поэтому приходится таким вот образом получать нужные id и title, которые передаются функции, описанной в view файле _form.php.

Вот собственно и всё. Для того чтобы добавить новый тип данных достаточно дописать еще одну строчку в массиве созданном в модели Menu.php и написать или сгенерировать, при помощи Gii, модель ActiveRecord для нового типа.

Автор: A3a

Источник

Поделиться

* - обязательные к заполнению поля