i-avatar777/yii2-service-form-ajax

用于yii2的表单验证和执行AJAX的服务。代替提交并刷新页面,使用AJAX请求和调用JS方法success

0.0.26 2024-05-28 15:52 UTC

README

用于yii2的表单验证和执行AJAX的服务。代替提交并刷新页面,使用AJAX请求和调用JS方法 success

概念

为了通过AJAX传递表单。

返回后显示错误。

例外情况是,无法通过AJAX传递文件,或者比较困难,因此应用在线上传小部件,其中仅通过AJAX传递文件。

\iAvatar777\services\FormAjax\ActiveRecord::attributeWidgets - 这里指定了小部件及其设置,用于在表单中输出

这样在页面上指定表单

<?php $model = new \avatar\models\validate\CabinetSchoolFilesCloudSave(); ?>
<?php $form = \iAvatar777\services\FormAjax\ActiveForm::begin([
    'model'   => $model,
    'formUrl' => '/cabinet-school-files/add',
    'success' => <<<JS
function (ret) {
    $('#modalInfo').on('hidden.bs.modal', function() {
        
    }).modal();
}
JS

]) ?>
    <?= $form->field($model, 'url') ?>
    <?= $form->field($model, 'key')->passwordInput() ?>
<?php \iAvatar777\services\FormAjax\ActiveForm::end(['label' => 'Сохранить']) ?>

如果您使用这个 ActiveForm,则必须使用这个模型 ActiveRecord 或这个 Model。在其中定义了返回验证错误的函数 getErrors102。或者您可以使用自己的,但请将此函数复制到您的模型中。

作为表单处理程序,主要考虑的是 DefaultFormAjax,它在控制器中定义如下

class CabinetBlogController extends CabinetBaseController
{
    public function actions()
    {
        return [
            'add' => [
                'class'    => '\iAvatar777\services\FormAjax\DefaultFormAjax',
                'model'    => '\avatar\models\forms\BlogItem',
            ],
        ];
    }
}

其中 model 是表单模型,而 add 是操作标识符 (Action::id)。

为了使表单发送检查此处理程序,必须在表单中指定

与YII2标准AJAX处理的区别

安装

显示小部件

在函数 attributeWidgets 中指定字段和小部件的对应关系。如果表单中指定了,则此小部件将自动绘制

<?= $form->field($model, 'image') ?>

class ProductImage extends \iAvatar777\services\FormAjax\ActiveRecord
{
    
    public function attributeWidgets()
    {
        return [
            'image' => [
                'class'    => '\iAvatar777\widgets\FileUpload7\FileUpload',
                'update'   => \avatar\controllers\CabinetSchoolPagesConstructorController::getUpdate(),
                'settings' => \avatar\controllers\CabinetSchoolPagesConstructorController::getSettingsLibrary($this->_school_id, $this->_type_id),
                'events'   => [
                    'onDelete' => function ($item) {
                        $r = new \cs\services\Url($item->image);
                        $d = pathinfo($r->path);
                        $start = $d['dirname'] . '/' . $d['filename'];

                        File::deleteAll(['like', 'file', $start]);
                    },
                ],
            ],
        ];
    }

}

禁用按钮

如果需要不显示按钮并执行子处理,则可以这样

<?php \iAvatar777\services\FormAjax\ActiveForm::end(['isHide' => true]) ?>

ajaxJson({
    url: '/...',
    data: $('{$formSelector}').serializeArray(),
    success: function(ret) {
        //
    },
    errorScript: function(ret) {
        if (ret.id == 102) {
            for (var key in ret.data.errors) {
                if (ret.data.errors.hasOwnProperty(key)) {
                    var name = key;
                    var value = ret.data.errors[key];
                    var id;
                    for (var key2 in ret.data.fields) {
                        if (ret.data.fields.hasOwnProperty(key2)) {
                            var name2 = key2;
                            var value2 = ret.data.fields[key2];
                            if (name == name2) {
                                id = 'field-' + value2;
                            }
                        }
                    }
                    var g = $('.' + id);
                    g.addClass('has-error');
                    g.find('p.help-block-error').html(value.join('<br>')).show();
                }
            }
        }
    }
});

特点

提交事件在Enter键上被触发两次,但实际上只触发一次。没有找到原因。因此,为了解决这个问题,我设置了参数 delta = 1000毫秒,在此期间不能再次调用事件 我的提交

$('#formc2ff52cf').submit(function(ret) {
    form1.isStart = true;
    form1.thisStart = (new Date()).getTime();
    
    if (form1.lastStart == -1) {
        form1.lastStart = form1.thisStart;
    } else {
        if (form1.lastStart + form1.delta > form1.thisStart) {
            form1.isStart = false;
        }
    }
    
    if (form1.isStart) {
        // AJAX
    }

    return false;
});

在控制器中添加或更新时使用

如果模型类是 \yii\db\ActiveRecord\yii\base\Model,则控制器中保存的代码如下

if (Yii::$app->request->isPost) {
    if ($model->load(Yii::$app->request->post()) && $model->validate()) {
        $s = $model->save();
        return self::jsonSuccess($s);
    } else {
        $fields = [];
        foreach ($model->attributes as $k => $v) {
            $fields[$k] = Html::getInputId($model, $k);
        }
        return self::jsonErrorId(102, [
            'errors' => $model->errors,
            'fields' => $fields,
        ]);
    }
}

如果模型类是 \iAvatar777\services\FormAjax\ActiveRecord\iAvatar777\services\FormAjax\Model,则它们存在函数 getErrors102(),因此代码更简洁,可以写成这样

if (Yii::$app->request->isPost) {
    if ($model->load(Yii::$app->request->post()) && $model->validate()) {
        $s = $model->save();
        return self::jsonSuccess($s);
    } else {
        return self::jsonErrorId(102, $model->getErrors102());
    }
}

使用示例

如果保存是在Model中调用,则parent::将调用onBeforeUpdate和onAfterUpdate

标准操作

为了简化标准操作,库中存在标准操作处理器集

  • \iAvatar777\services\FormAjax\DefaultFormAjax - 用于处理AJAX表单处理
  • \iAvatar777\services\FormAjax\DefaultFormAdd - 用于在页面上绘制表单添加,页面传递变量 $model
  • \iAvatar777\services\FormAjax\DefaultFormEdit - 用于在页面上绘制表单编辑,页面传递变量 $model。记录标识符通过GET方法的方法传递参数 id
  • \iAvatar777\services\FormAjax\DefaultFormDelete - 用于通过AJAX删除记录。记录标识符通过POST方法传递参数 id

如果页面上有表单添加

在控制器中,在函数 actions() 中指定

class CabinetBlogController extends CabinetBaseController
{
    public function actions()
    {
        return [
            'add' => [
                'class'    => '\common\services\FormAjax\DefaultFormAdd',
                'model'    => '\avatar\models\forms\BlogItem',
                'view'     => '@avatar/views/blog/add',
            ],
        ];
    }
}

参数 view 不是必需的,如果未指定,则使用操作标识符 (action)。

\iAvatar777\services\FormAjax\ActiveRecord的事件模型

Widget:onBeforeLoad выполняется до   $model->load()
Widget:onAfterLoad выполняется после $model->load()

Widget:onAfterLoadDb выполняется после $model->findOne()

Widget:onBeforeInsert выполняется до   $model->save()
Widget:onAfterInsert выполняется после $model->save()

Widget:onBeforeUpdate выполняется до   $model->save()
Widget:onAfterUpdate выполняется после $model->save()

Widget:onBeforeValidate выполняется до   $model->validate()
Widget:onAfterValidate выполняется после $model->validate()

Widget:onBeforeDelete выполняется до   $model->delete()
Widget:onAfterDelete выполняется после $model->delete()

它们如何定义?传递什么内容?

function onBeforeUpdate() {}

模型启动事件

例如 delete

  1. attributeWidgets() ['events']['onBeforeDelete']
  2. 组件:onBeforeDelete
  3. \yii\db\ActiveRecord::delete()
  4. 组件:onAfterDelete
  5. attributeWidgets() ['events']['onAfterDelete']

也就是说,如果需要,可以编写自己的处理器,并且同时在该组件中使用额外的处理。

例如,如果表单中显示的值与数据库中存储的值格式不同,可以在组件处理器中指定类型转换。

或者例如,如果需要在删除记录后实际删除图片。

三种显示表单的场景

  1. 总共有三种显示表单的场景
1
$model->findOne()

2 - если прошла валидация
$model->findOne()
$model->load()
$model->validate()
$model->save() 

3 - если не прошла валидация
$model->findOne()
$model->load()
$model->validate()

如果表单中更改了字段的存储格式,则需要注意,在绘制表单时,字段应存储相同的格式。

如果进行格式转换,则最好这样做

我可以在 onAfterLoadDb 中进行转换,从 YYYY-mm-dd 转换为 dd.mm.YYYY,在 onBeforeUpdate 事件中从 dd.mm.YYYY 转换为 YYYY-mm-dd,这样在验证字段时将格式为 dd.mm.YYYY,在显示表单时将格式为 dd.mm.YYYY,在加载表单时值将以 dd.mm.YYYY 的格式加载,因此不需要进行任何更改。

如果我想要以 DateTime 格式保存(保持值在 DateTime 格式)怎么办?如何进行验证?如何在表单中显示值?例如:我可以在 onAfterLoadDb 中进行转换,从 YYYY-mm-dd 转换为 DateTime,在 onAfterLoad 事件中从 dd.mm.YYYY 转换为 DateTime,验证用户输入的值格式为 DateTime。在 onBeforeUpdate 事件中将 DateTime 转换为 YYYY-mm-dd。绘制组件时将使用 DateTime 格式。

由于保存表单后操作结束,因此保存表单后表单不再绘制。

组件

基本类: \iAvatar777\services\FormAjax\Widget

组件的目的是可以在其中调用事件以处理收到的数据。

例如,在日期组件中指定日期格式为 dd.mm.yyyy,为了进行验证,它应该是 dd.mm.yyyy 格式,在保存时它应该是 yyyy-mm-dd 格式。

组件中的函数示例

public function onAfterLoadDb($field) 
{

}

在简单 HTML 输出后初始化表单

有时需要在输出简单 HTML 后处理或附加表单处理器。在这种情况下,为了使表单正常工作,需要调用该函数

iAvatar777_ActiveForm.init(formId, formSelector, formUrl, functionSuccess, type);

获取表单字段值

有时需要以非标准方式获取字段值。例如,从文本编辑器。在这种情况下,组件中指定获取字段值的函数

class Widget extends \yii\base\Widget
{
    public function get_field_value()
    {
        $id = 'field-' . Html::getInputId($this->model, $this->attribute);
        $name = Html::getInputName($this->model, $this->attribute);
        
        return <<<JS
function (fields) {
    
    // очищенный результат
    var rows;
    var serializeArray = $(formSelector).serializeArray();
    // функция зачистки, учитывая что значений может быть много то алгоритм такой, прохожусь по всему массиву, если втретилось это поле то не вклчаю его в результат, остальное включаю.
    for (var i=0; i < serializeArray.length; i++) {
        if (serializeArray[i].name == '{name}') {
            // делаю замену
            rows.push({name: 'name', value: '1'});
        } else {
            // добавляю
            rows.push(serializeArray[i]);
        }
    }
    
    return rows; 
}
JS;
    }
}

如果没有函数,则将通过字段的 ID(INPUT)选择字段值。仅收集表中列出的字段(INPUT)。

从 serrializeArray 清除值的脚本示例

// очищенный результат
var rows;
var serializeArray = $(formSelector).serializeArray();
// функция зачистки, учитывая что значений может быть много то алгоритм такой, прохожусь по всему массиву, если втретилось это поле то не вклчаю его в результат, остальное включаю.
for (var i=0; i < serializeArray.length; i++) {
    if (serializeArray[i].name == '{name}') {
        // ничего не делаю
    } else {
        // добавляю
        rows.push(serializeArray[i]);
    }
}

替换简单字段的脚本示例

// очищенный результат
var rows;
var serializeArray = $(formSelector).serializeArray();
// функция зачистки, учитывая что значений может быть много то алгоритм такой, прохожусь по всему массиву, если втретилось это поле то не вклчаю его в результат, остальное включаю.
for (var i=0; i < serializeArray.length; i++) {
    if (serializeArray[i].name == '{name}') {
        // делаю замену
        rows.push({name: 'name', value: '1'});
    } else {
        // добавляю
        rows.push(serializeArray[i]);
    }
}