Полная документация Yii2 Dynamic Form Widget
Версия 1.0.0 для Bootstrap 5
Содержание
Введение

Установка

Требования

Быстрый старт

Типы связей

API Reference

Примеры использования

JavaScript события

Часто задаваемые вопросы

Лицензия

Введение
DynamicForm - это мощный виджет для Yii2, который позволяет создавать динамические формы с поддержкой связей один-ко-многим и многие-ко-многим. Виджет позволяет пользователям динамически добавлять и удалять формы связанных моделей с автоматическим управлением индексами и стилизацией Bootstrap 5.

Основные возможности
✅ Полная интеграция с Bootstrap 5

✅ Поддержка связей один-ко-многим (One-to-Many)

✅ Поддержка связей многие-ко-многим (Many-to-Many)

✅ Поддержка связей has-one

✅ Динамическое добавление/удаление элементов формы

✅ Автоматическое управление индексами полей

✅ Поддержка валидации форм

✅ Отображение ошибок валидации

✅ Система JavaScript событий

✅ Гибкая настройка внешнего вида

Установка
Через Composer
bash
composer require garmayev/yii2-dynamic-form
Вручную
Добавьте в файл composer.json:

json
{
"require": {
"garmayev/yii2-dynamic-form": "^1.0"
}
}
Затем выполните:

bash
composer update
Требования
PHP 8.2 или выше

Yii2 Framework 2.0+

Yii2 Bootstrap 5 Extension (yiisoft/yii2-bootstrap5)

jQuery

Быстрый старт
Шаг 1: Подготовка моделей
php
// app/models/Order.php
namespace app\models;

use yii\db\ActiveRecord;
use garmayev\dynamicform\DynamicForm;

class Order extends ActiveRecord
{
public static function tableName()
{
return 'orders';
}

    public function rules()
    {
        return [
            [['order_number', 'customer_name'], 'required'],
            [['order_number'], 'string', 'max' => 50],
            [['customer_name'], 'string', 'max' => 255],
        ];
    }

    // Определяем связь один-ко-многим
    public function getItems()
    {
        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
    }

    // Сохраняем связанные модели после сохранения основной
    public function afterSave($insert, $changedAttributes)
    {
        parent::afterSave($insert, $changedAttributes);
        
        $dynamicForm = Yii::createObject([
            'class' => DynamicForm::class,
            'model' => $this,
            'relation' => 'items',
            'relationType' => DynamicForm::RELATION_ONE_TO_MANY,
        ]);
        $dynamicForm->saveRelations();
    }
}

// app/models/OrderItem.php
namespace app\models;

use yii\db\ActiveRecord;

class OrderItem extends ActiveRecord
{
public static function tableName()
{
return 'order_items';
}

    public function rules()
    {
        return [
            [['product_name', 'quantity', 'price'], 'required'],
            [['quantity'], 'integer', 'min' => 1],
            [['price'], 'number', 'min' => 0],
            [['product_name'], 'string', 'max' => 255],
            [['order_id'], 'integer'],
        ];
    }

    public function attributeLabels()
    {
        return [
            'product_name' => 'Название товара',
            'quantity' => 'Количество',
            'price' => 'Цена',
        ];
    }
}
Шаг 2: Создание формы
php
// app/views/order/_form.php
<?php

use yii\bootstrap5\ActiveForm;
use yii\bootstrap5\Html;
use garmayev\dynamicform\DynamicForm;

/* @var $this yii\web\View */
/* @var $model app\models\Order */
/* @var $form yii\bootstrap5\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>

    <div class="row">
        <div class="col-md-6">
            <?= $form->field($model, 'order_number')->textInput(['maxlength' => true]) ?>
        </div>
        <div class="col-md-6">
            <?= $form->field($model, 'customer_name')->textInput(['maxlength' => true]) ?>
        </div>
    </div>

    <h3>Товары в заказе</h3>

    <?php DynamicForm::begin([
        'form' => $form,
        'model' => $model,
        'relation' => 'items',
        'relationType' => DynamicForm::RELATION_ONE_TO_MANY,
        'itemHeaderText' => 'Товар',
        'addButtonOptions' => [
            'label' => '<i class="fas fa-plus"></i> Добавить товар',
            'class' => 'btn btn-primary',
        ],
    ]); ?>

        <div class="row">
            <div class="col-md-6">
                <?= $dynamicForm->textField('product_name', 'Название товара', [
                    'placeholder' => 'Введите название товара'
                ]) ?>
            </div>
            <div class="col-md-3">
                <?= $dynamicForm->textField('quantity', 'Количество', [
                    'type' => 'number',
                    'placeholder' => 'Количество'
                ]) ?>
            </div>
            <div class="col-md-3">
                <?= $dynamicForm->textField('price', 'Цена', [
                    'type' => 'number',
                    'step' => '0.01',
                    'placeholder' => 'Цена'
                ]) ?>
            </div>
        </div>

    <?php DynamicForm::end(); ?>

    <div class="form-group mt-3">
        <?= Html::submitButton('Сохранить заказ', ['class' => 'btn btn-success']) ?>
    </div>

<?php ActiveForm::end(); ?>
Шаг 3: Контроллер
php
// app/controllers/OrderController.php
namespace app\controllers;

use Yii;
use app\models\Order;
use yii\web\Controller;
use yii\web\NotFoundHttpException;

class OrderController extends Controller
{
public function actionCreate()
{
$model = new Order();

        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            Yii::$app->session->setFlash('success', 'Заказ успешно создан');
            return $this->redirect(['view', 'id' => $model->id]);
        }
        
        return $this->render('create', [
            'model' => $model,
        ]);
    }

    public function actionUpdate($id)
    {
        $model = $this->findModel($id);
        
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            Yii::$app->session->setFlash('success', 'Заказ успешно обновлен');
            return $this->redirect(['view', 'id' => $model->id]);
        }
        
        return $this->render('update', [
            'model' => $model,
        ]);
    }

    protected function findModel($id)
    {
        if (($model = Order::findOne($id)) !== null) {
            return $model;
        }
        
        throw new NotFoundHttpException('Страница не найдена');
    }
}
Типы связей
1. Связь один-ко-многим (One-to-Many)
   php
   // Модель
   public function getItems()
   {
   return $this->hasMany(Item::class, ['parent_id' => 'id']);
   }

// В форме
DynamicForm::begin([
'form' => $form,
'model' => $model,
'relation' => 'items',
'relationType' => DynamicForm::RELATION_ONE_TO_MANY,
]);
2. Связь многие-ко-многим (Many-to-Many)
   php
   // Модель
   public function getServices()
   {
   return $this->hasMany(Service::class, ['id' => 'service_id'])
   ->viaTable('appointment_service', ['appointment_id' => 'id']);
   }

// В форме
DynamicForm::begin([
'form' => $form,
'model' => $model,
'relation' => 'services',
'relationType' => DynamicForm::RELATION_MANY_TO_MANY,
]);

echo $dynamicForm->dropDownListField('id', $servicesList, 'Услуга');
3. Связь has-one
   php
   // Модель
   public function getProfile()
   {
   return $this->hasOne(Profile::class, ['user_id' => 'id']);
   }

// В форме
DynamicForm::begin([
'form' => $form,
'model' => $model,
'relation' => 'profile',
'relationType' => DynamicForm::RELATION_HAS_ONE,
'showAddButton' => false,
'showRemoveButton' => false,
]);
API Reference
Параметры виджета
Параметр	Тип	По умолчанию	Описание
form	ActiveForm	обязательный	Экземпляр ActiveForm
model	ActiveRecord	обязательный	Основная модель
relation	string	обязательный	Имя связи
relationType	string	RELATION_MANY_TO_MANY	Тип связи
relationModelClass	string	null	Класс связанной модели
options	array	[]	HTML опции контейнера
addButtonOptions	array	см. ниже	Опции кнопки добавления
removeButtonOptions	array	см. ниже	Опции кнопки удаления
itemsContainerClass	string	'dynamic-items-container'	CSS класс контейнера
itemClass	string	'dynamic-form-item'	CSS класс элемента
showAddButton	bool	true	Показывать кнопку добавления
showRemoveButton	bool	true	Показывать кнопку удаления
allowRemoveLast	bool	false	Разрешить удаление последнего
itemHeaderText	string	'Item'	Текст заголовка элемента
showItemHeader	bool	true	Показывать заголовок
Методы для создания полей
Метод	Описание
textField($attribute, $label, $options)	Текстовое поле с лейблом
dropDownListField($attribute, $items, $label, $options)	Выпадающий список с лейблом
textareaField($attribute, $label, $options)	Текстовая область с лейблом
checkboxField($attribute, $label, $options)	Чекбокс с лейблом
textInput($attribute, $options)	Текстовое поле
passwordInput($attribute, $options)	Поле пароля
dropDownList($attribute, $items, $options)	Выпадающий список
textarea($attribute, $options)	Текстовая область
checkbox($attribute, $options)	Чекбокс
hiddenInput($attribute, $options)	Скрытое поле
Константы типов связей
php
DynamicForm::RELATION_MANY_TO_MANY  // Многие-ко-многим
DynamicForm::RELATION_ONE_TO_MANY   // Один-ко-многим
DynamicForm::RELATION_HAS_MANY      // Has-many
DynamicForm::RELATION_HAS_ONE       // Has-one
Примеры использования
Пример 1: Простая форма с одним полем
php
<?php DynamicForm::begin([
    'form' => $form,
    'model' => $model,
    'relation' => 'tags',
    'relationType' => DynamicForm::RELATION_ONE_TO_MANY,
    'itemHeaderText' => 'Тег',
    'showItemHeader' => false,
]); ?>

    <?= $dynamicForm->textField('name', 'Название тега', [
        'placeholder' => 'Введите название тега'
    ]) ?>

<?php DynamicForm::end(); ?>
Пример 2: Форма с различными типами полей
php
<?php DynamicForm::begin([
    'form' => $form,
    'model' => $model,
    'relation' => 'products',
    'addButtonOptions' => [
        'label' => '<i class="fas fa-plus"></i> Добавить товар',
        'class' => 'btn btn-primary',
    ],
]); ?>

    <div class="row">
        <div class="col-md-6">
            <?= $dynamicForm->textField('name', 'Название товара') ?>
        </div>
        <div class="col-md-3">
            <?= $dynamicForm->textField('price', 'Цена', [
                'type' => 'number',
                'step' => '0.01'
            ]) ?>
        </div>
        <div class="col-md-3">
            <?= $dynamicForm->textField('quantity', 'Количество', [
                'type' => 'number'
            ]) ?>
        </div>
    </div>
    
    <?= $dynamicForm->textareaField('description', 'Описание', [
        'rows' => 3
    ]) ?>
    
    <?= $dynamicForm->checkboxField('is_active', 'Активен') ?>

<?php DynamicForm::end(); ?>
Пример 3: Связь многие-ко-многим с выпадающим списком
php
<?php
$servicesList = \yii\helpers\ArrayHelper::map(Service::find()->all(), 'id', 'name');

DynamicForm::begin([
    'form' => $form,
    'model' => $appointment,
    'relation' => 'services',
    'relationType' => DynamicForm::RELATION_MANY_TO_MANY,
    'itemHeaderText' => 'Услуга',
]); ?>

    <?= $dynamicForm->dropDownListField('id', $servicesList, 'Услуга', [
        'prompt' => 'Выберите услугу'
    ]) ?>

<?php DynamicForm::end(); ?>
Пример 4: Вложенные динамические формы
php
<?php DynamicForm::begin([
    'form' => $form,
    'model' => $order,
    'relation' => 'items',
    'relationType' => DynamicForm::RELATION_ONE_TO_MANY,
]); ?>

    <?= $dynamicForm->textField('product_name', 'Товар') ?>
    <?= $dynamicForm->textField('quantity', 'Количество') ?>
    
    <!-- Вложенная форма для опций товара -->
    <?php DynamicForm::begin([
        'form' => $form,
        'model' => $itemModel,
        'relation' => 'options',
        'relationType' => DynamicForm::RELATION_ONE_TO_MANY,
        'itemHeaderText' => 'Опция',
    ]); ?>
    
        <?= $dynamicForm->textField('name', 'Название опции') ?>
        <?= $dynamicForm->textField('value', 'Значение') ?>
        
    <?php DynamicForm::end(); ?>

<?php DynamicForm::end(); ?>
Пример 5: С пользовательскими стилями
php
<?php DynamicForm::begin([
    'form' => $form,
    'model' => $model,
    'relation' => 'items',
    'options' => [
        'class' => 'dynamic-form-widget custom-style',
        'data-custom-attribute' => 'value',
    ],
    'addButtonOptions' => [
        'label' => '<i class="fas fa-plus-circle"></i> Добавить элемент',
        'class' => 'btn btn-outline-primary btn-lg',
    ],
    'removeButtonOptions' => [
        'label' => '<i class="fas fa-times-circle"></i>',
        'class' => 'btn btn-outline-danger btn-sm',
    ],
    'itemHeaderText' => 'Позиция',
]); ?>
Пример 6: Обработка AJAX
javascript
$('#dynamic-form-items').on('itemAdded', function(event, $item, index) {
// Загружаем данные через AJAX после добавления
$.ajax({
url: '/items/get-default-data',
method: 'GET',
success: function(data) {
$item.find('input[name*="product_name"]').val(data.default_name);
$item.find('input[name*="price"]').val(data.default_price);
}
});
});
JavaScript события
Виджет генерирует следующие JavaScript события:

Событие	Описание	Параметры
itemAdded	Срабатывает после добавления элемента	event, $item, index
itemRemoved	Срабатывает после удаления элемента	event, index
Пример использования событий
javascript
// Обработчик добавления элемента
$('#dynamic-form-items').on('itemAdded', function(event, $item, index) {
console.log('Добавлен элемент #' + (index + 1));

    // Добавляем анимацию
    $item.hide().fadeIn(300);
    
    // Подсвечиваем новый элемент
    $item.css('background-color', '#d4edda');
    setTimeout(function() {
        $item.css('background-color', '');
    }, 1000);
});

// Обработчик удаления элемента
$('#dynamic-form-items').on('itemRemoved', function(event, index) {
console.log('Удален элемент #' + (index + 1));

    // Обновляем счетчик
    var count = $('.dynamic-form-item').length;
    $('#items-count').text(count);
});

// Кастомная валидация при добавлении
$('#dynamic-form-items').on('itemAdded', function(event, $item, index) {
var quantity = $item.find('input[name*="quantity"]').val();
if (quantity && quantity > 100) {
alert('Количество не может превышать 100');
$item.remove();
}
});
Часто задаваемые вопросы
Вопрос 1: Как изменить максимальное количество элементов?
javascript
var maxItems = 10;

$('#dynamic-form-items').on('itemAdded', function(event, $item, index) {
var currentCount = $('.dynamic-form-item').length;

    if (currentCount >= maxItems) {
        $('.btn-add-dynamic-item').prop('disabled', true);
        alert('Достигнуто максимальное количество элементов');
    }
});

$('#dynamic-form-items').on('itemRemoved', function() {
$('.btn-add-dynamic-item').prop('disabled', false);
});
Вопрос 2: Как добавить подтверждение при удалении?
javascript
// Переопределяем обработчик удаления
$(document).on('click', '.btn-remove-dynamic-item', function(e) {
if (!confirm('Вы уверены, что хотите удалить этот элемент?')) {
e.preventDefault();
e.stopPropagation();
return false;
}
});
Вопрос 3: Как предзаполнить форму данными?
php
// В контроллере
$model = new Order();
$model->items = [
new OrderItem(['product_name' => 'Товар 1', 'quantity' => 2, 'price' => 100]),
new OrderItem(['product_name' => 'Товар 2', 'quantity' => 1, 'price' => 200]),
];
Вопрос 4: Как использовать с AJAX валидацией?
php
// В форме
<?php $form = ActiveForm::begin([
    'enableAjaxValidation' => true,
    'validationUrl' => ['validate-order'],
]); ?>

// В контроллере
public function actionValidateOrder()
{
$model = new Order();
$model->load(Yii::$app->request->post());

    // Загружаем связанные модели
    $itemsData = Yii::$app->request->post('items');
    foreach ($itemsData as $itemData) {
        $item = new OrderItem();
        $item->load($itemData, '');
        $model->items[] = $item;
    }
    
    Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
    return \yii\widgets\ActiveForm::validate($model);
}
Вопрос 5: Как добавить кастомную кнопку добавления?
php
<?php DynamicForm::begin([
    'form' => $form,
    'model' => $model,
    'relation' => 'items',
    'showAddButton' => false, // Отключаем стандартную кнопку
]); ?>

    <!-- Поля формы -->

<?php DynamicForm::end(); ?>

<!-- Своя кнопка -->
<button type="button" class="btn btn-success my-custom-add-btn">
    <i class="fas fa-plus"></i> Мой текст кнопки
</button>

<script>
$(document).on('click', '.my-custom-add-btn', function() {
    $('.btn-add-dynamic-item').trigger('click');
});
</script>
Вопрос 6: Как обработать сохранение на сервере?
php
// В модели
public function afterSave($insert, $changedAttributes)
{
parent::afterSave($insert, $changedAttributes);

    $dynamicForm = Yii::createObject([
        'class' => DynamicForm::class,
        'model' => $this,
        'relation' => 'items',
        'relationType' => DynamicForm::RELATION_ONE_TO_MANY,
    ]);
    $dynamicForm->saveRelations();
}
Вопрос 7: Как использовать с файловыми загрузками?
php
// В форме
<?php DynamicForm::begin([
    'form' => $form,
    'model' => $model,
    'relation' => 'images',
]); ?>

    <?= $dynamicForm->fileInput('image_file', ['accept' => 'image/*']) ?>
    <?= $dynamicForm->textField('caption', 'Подпись') ?>

<?php DynamicForm::end(); ?>

// В контроллере
public function actionCreate()
{
$model = new Gallery();

    if ($model->load(Yii::$app->request->post())) {
        $transaction = Yii::$app->db->beginTransaction();
        try {
            $model->save();
            
            $imagesData = Yii::$app->request->post('images');
            foreach ($imagesData as $index => $imageData) {
                $image = new GalleryImage();
                $image->load($imageData, '');
                $image->gallery_id = $model->id;
                
                $image->image_file = UploadedFile::getInstanceByName("images[{$index}][image_file]");
                if ($image->image_file) {
                    $image->save();
                    $image->image_file->saveAs('uploads/' . $image->image_file->name);
                }
            }
            
            $transaction->commit();
        } catch (\Exception $e) {
            $transaction->rollBack();
            throw $e;
        }
    }
}
Вопрос 8: Как изменить CSS стили?
css
/* Основной контейнер */
.dynamic-form-widget {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
}

/* Карточка элемента */
.dynamic-form-widget .card {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

/* Заголовок карточки */
.dynamic-form-widget .card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}

/* Кнопка удаления */
.dynamic-form-widget .btn-remove-dynamic-item {
background: #dc3545;
border: none;
border-radius: 20px;
padding: 5px 15px;
}

/* Кнопка добавления */
.dynamic-form-widget .btn-add-dynamic-item {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 25px;
padding: 10px 25px;
}

/* Поля ввода */
.dynamic-form-widget .form-control,
.dynamic-form-widget .form-select {
border-radius: 8px;
border: 1px solid #e0e0e0;
transition: all 0.3s ease;
}

.dynamic-form-widget .form-control:focus,
.dynamic-form-widget .form-select:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102,126,234,0.25);
}
Лицензия
Это расширение распространяется под лицензией MIT. Вы можете свободно использовать, модифицировать и распространять его.

Поддержка
Если у вас возникли вопросы или проблемы:

Создайте Issue на GitHub

Отправьте email на garmayev@yandex.ru

Посмотрите демо-приложение

Расширение разработано Garmayev для сообщества Yii2

Скачать документацию
Вы можете сохранить этот документ как файл DYNAMIC_FORM_DOCS.md и использовать его в любом Markdown редакторе.