В этом уроке мы рассмотрим реализацию асинхронной отправки данных из формы на примере простых комментариев (условный пример). При заполнении поля для текста комментария и нажатия на кнопку отправки, комментарий записывается в БД и без перезагрузки страницы выводится под формой.

Во втором примере мы с помощью jQuery ajax прикрепим изображение к модели нашей новости из прошлого урока.

Простая отправка формы асинхронно

Простая отправка формы асинхронно на примере добавления комментариев.

Миграция:

public function up()
{
  $this->createTable('{{%comment}}', [
    'id' => $this->primaryKey(),
    'comment' => $this->text()
  ], $tableOptions);
}

public function down()
{
  $this->dropTable('{{%comment}}');
}

Класс AjaxCommentForm:

class AjaxCommentForm extends Model
{
  public $comment;

  public function rules()
  {
    return [
      ['comment', 'string'],
    ];
  }
}

Класс Comment:

class Comment extends ActiveRecord
{
  public static function tableName()
  {
    return '{{%comment}}';
  }

  public static function getComments()
  {
    return self::find()->all();
  }
}

Представление:

<?php $form = ActiveForm::begin([
  'enableClientValidation' => true,
  'enableAjaxValidation'   => false,
  'action' => Url::to(['site/index']),
  'method' => 'post',
  'options' => ['id' => 'form']
]) ?>
<?= $form->field($model, 'comment')->textarea(['rows' => 5]); ?>
<?= Html::submitButton("Submit", ['class' => "btn btn-default"]); ?>
<?php ActiveForm::end() ?>
<div id="process">
  <img src="/img/loading.gif" alt="Loading">
</div>
<h2>Comments</h2>
<div id="comments">
  <?
  /**
   * @var $comments
   * @var $item \frontend\models\Comment
   */
  foreach ($comments as $item):
  ?>
  <div class="comment"><?= $item->comment ?></div>
  <? endforeach; ?>
</div>
<?php
$js = <<<JS
$('#form').on('beforeSubmit', function(){        
  var form = $(this),
    data = $(this).serialize();
  $.ajax({
    url: form.attr("action"),
    type: form.attr("method"),
    data: data,
    beforeSend: function(){
      $('#process').fadeIn();
    },
    success: function(data){
      form[0].reset();
      $("#comments").append('<div class="comment">'+ data.comment +'</div>');
      $('#process').fadeOut();
    },
    error: function(){
      $('#process').fadeOut();
      alert('Error!');
    }
  });
  return false;
}).on('submit', function(e){
  e.preventDefault();
});
JS;
$this->registerJs($js);
?>

Стили для #process:

#process{
 position: absolute;
 top: 0;
 right: 0;
 left: 0;
 bottom: 0;
 background: rgba(255, 255, 255, 1);
 z-index: 1;
 display: none;
}

#process img{
 position: absolute;
 top: 80px;
 right: 30px;
 z-index: 2;
}

Метод контроллера:

public function actionIndex()
{
  $form = new AjaxCommentForm();
  if(Yii::$app->request->isAjax){
    Yii::$app->response->format = Response::FORMAT_JSON;
    if($form->load(Yii::$app->request->post()) && $form->validate()){
      $commentModel = new Comment();
      $commentModel->comment = $form->comment;
      if ($commentModel->save()) {
        return $data = [
          'success' => true,
          'comment' => $form->comment,
        ];
      }
    }
  }

  return $this->render('index', [
    'model' => $form,
    'comments' => Comment::getComments()
  ]);
}

Асинхронная загрузка изображения для новости

В этом примере мы реализуем загрузку изображения на сервер с помощью Ajax. Возьмём за основу код из примера загрузки изображения для новости в новостной ленте.

Класс News:

class News extends ActiveRecord
{
  public static function tableName()
  {
    return '{{%news}}';
  }

  public function getImagePath()
  {
    if ($this->image)
      return $this->getImage($this->image);
    return 'https://via.placeholder.com/300x200';
  }

  private function getImage(string $filename): string
  {
    return Yii::$app->params['uploadHostInfo'] . 'news/' . $filename;
  }

  public function beforeDelete()
  {
    $this->deleteImage();
    return parent::beforeDelete();
  }

  public function deleteImage()
  {
    $form = new NewsImageForm();
    $form->deleteCurrentImage($this->image);
  }
}

Класс NewsForm:

class NewsForm extends Model{

  public $name;
  public $content;
  public $slug;
  public $created_at;
  private $_model;

  public function __construct(News $model = null, $config = []) {

    if ($model) {
      $this->name = $model->name;
      $this->content = $model->content;
      $this->slug = $model->slug;
      $this->created_at = $model->created_at;
      $this->_model = $model;
    }
    parent::__construct($config);
  }


  public function rules()
  {
    return [
      [['name'], 'required'],
      [['name', 'slug'], 'string', 'max' => 255],
      ['content', 'string'],
      ['slug', SlugValidator::class],
      [['name', 'slug'], 'unique',
        'targetClass' => News::class,
        'filter' => $this->_model ? ['<>', 'id', $this->_model->id] : null
      ]
    ];
  }
}

Класс NewsImageForm:

class NewsImageForm extends Model{

  /**
   * @var UploadedFile
   */
  public $image;

  public function rules()
  {
    return [
      ['image', 'image',
        'extensions' => ['jpg', 'jpeg', 'png'],
        'checkExtensionByMimeType' => true,
        'maxSize' => 512000, // 500 килобайт = 500 * 1024 байта = 512 000 байт
        'tooBig' => 'Limit is 500KB'
      ]
    ];
  }

  public function uploadImage(UploadedFile $image, $currentImage = null)
  {
    if (!is_null($currentImage))
      $this->deleteCurrentImage($currentImage);

    $this->image = $image;

    if($this->validate())
      return $this->saveImage();

    return false;
  }

  private function getUploadPath()
  {
    return Yii::$app->params['uploadPath'] . 'news/';
  }

  /**
   * @return string
   */
  public function generateFileName(): string
  {
    do {
      $name = substr(md5(microtime() . rand(0, 1000)), 0, 20);
      $file = strtolower($name .'.'. $this->image->extension);
    } while (file_exists($file));

    return $file;
  }

  public function deleteCurrentImage($currentImage)
  {
    if ($currentImage && $this->fileExists($currentImage)) {
      unlink($this->getUploadPath() . $currentImage);
    }
  }
 
  public function fileExists($currentFile): bool
  {
    $file = $currentFile ? $this->getUploadPath() . $currentFile : null;
    return file_exists($file);
  }
  
  public function saveImage(): string
  {
    $filename = $this->generateFilename();
    $this->image->saveAs($this->getUploadPath() . $filename);
    return $filename;
  }
}

Представление (страница новости):

<?php

use common\models\News;
use yii\helpers\Html;
use yii\widgets\DetailView;

/* @var $this yii\web\View */
/* @var $model common\models\News */
/* @var $imageForm \common\forms\NewsImageForm */

$this->title = $model->name;
$this->params['breadcrumbs'][] = ['label' => 'News', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
\yii\web\YiiAsset::register($this);
?>
<div class="new-view">
  <h1><?= Html::encode($this->title) ?></h1>
  <p>
    <?= Html::a('Update', ['update', 'id' => $model->id], ['class' => 'btn btn-primary']) ?>
    <?= Html::a('Delete', ['delete', 'id' => $model->id], [
      'class' => 'btn btn-danger',
      'data' => [
        'confirm' => 'Are you sure you want to delete this item?',
        'method' => 'post'
      ]
    ]) ?>
  </p>
  <?= DetailView::widget([
    'model' => $model,
    'attributes' => [
      'id',
      'name',
      'slug',
      'content:raw',
      'created_at:date',
      [
        'value' => function (News $model) {
          return Html::img($model->getImagePath(), ['width' => 200, 'alt' => $model->name, 'id' => 'upload-image']);
        },
        'label' => 'Image',
        'format' => 'raw'
      ]
    ]
  ]) ?>
</div>
<?php
use yii\helpers\Url;
use yii\widgets\ActiveForm;
?>
<?php $form = ActiveForm::begin([
  'enableClientValidation' => true,
  'enableAjaxValidation'   => false,
  'action' => Url::to(['ajax-upload-image', 'id' => $model->id]),
  'method' => 'post',
  'options' => ['id' => 'form']
]) ?>
<?= $form->field($imageForm, 'image')->fileInput(['id' => 'form-image']); ?>
<p>
  <?= Html::submitButton('Save Image', ['class' => 'btn btn-default', 'id' => 'btn-save-image', 'disabled' => true]) ?>

  <? if ($model->image): ?>
    <?= Html::a('Delete Image', ['ajax-delete-image', 'id' => $model->id], [
      'class' => 'btn btn-warning',
      'id' => 'delete-image',
      'data' => ['method' => 'post']
    ]) ?>
  <? endif; ?>
</p>
<?php ActiveForm::end() ?>
<?php
$js = <<<JS
// Загрузка изображения
var btnSaveImage = $('#btn-save-image'),
    btnDeleteImage = $('#delete-image'),
    inputImage = $('#form-image'),
    image = $("#upload-image"),
    process = $('#process');

inputImage.on('change', function() {
  btnSaveImage.prop('disabled', false);
});

btnSaveImage.on('click', function(e){        
  e.preventDefault();
  var form = $('#form');
  var formData = new FormData(form[0]);
  $.ajax({
    url: form.attr("action"),
    type: form.attr("method"),
    data: formData,
    dataType : 'text',
    processData: false,
    contentType: false,
    cache: false,
    beforeSend: function(){
      process.fadeIn('fast');                
    },
    success: function(data){    
      form[0].reset(); 
      data = JSON.parse(data);
      btnSaveImage.prop('disabled', true);
      btnDeleteImage.fadeIn();
      image.attr('src', data.image);
      process.delay(1000).fadeOut();
    },
     error: function(){
      process.delay(1000).fadeOut();
      alert('Error!');
    }
  });
  return false;
});

// Удаление изображения
btnDeleteImage.on("click", function(e) {
  e.preventDefault();
  var res = confirm('Вы действительно хотите удалить текущее изображение?');
  if(!res) return false;
  $.ajax({
   url: $(this).attr("href"),
   type: $(this).data("method"), 
   dataType : 'text',
   processData: false,
   contentType: false,  
   beforeSend: function(){
    process.fadeIn('fast');                 
   },
   success: function(data){       
    data = JSON.parse(data);
    btnSaveImage.prop('disabled', true);
    image.attr('src', data.image);
    btnDeleteImage.fadeOut();
    process.delay(1000).fadeOut();
   },
   error: function(){
    process.delay(1000).fadeOut();   
    alert('Error!');
   }
  });
  return false;
});
JS;
$this->registerJs($js);
?>
<div id="process">
  <img src="/img/loading.gif" alt="Loading">
</div>

Контроллер:

// Получаем модель
protected function findModel($id)
{
  if (($model = News::findOne($id)) !== null) {
    return $model;
  }
  throw new NotFoundHttpException('The requested page does not exist.');
}

// Страница новости
public function actionView($id)
{
  return $this->render('view', [
    'model' => $this->findModel($id),
    'imageForm' => new NewsImageForm()
  ]);
}


// Создание новости
public function actionCreate()
{
  $model = new News();
  $modelForm = new NewsForm();
  if ($modelForm->load(Yii::$app->request->post()) && $modelForm->validate()) {
    $model->name = $modelForm->name;
    $model->content = $modelForm->content;
    $model->slug = $modelForm->slug ?: Inflector::slug($model->name);
    $model->created_at = $modelForm->created_at ?: time();
    if($model->save()) {
      return $this->redirect(['view', 'id' => $model->id]);
    }
  }

  return $this->render('create', [
    'model' => $model,
    'modelForm' => $modelForm
  ]);
}

// Редактирование новости
public function actionUpdate($id)
{
  $model = $this->findModel($id);
  $modelForm = new NewsForm($model);
  if ($modelForm->load(Yii::$app->request->post()) && $modelForm->validate()) {
    $model->name = $modelForm->name;
    $model->content = $modelForm->content;
    $model->slug = $modelForm->slug ?: Inflector::slug($model->name);
    if($model->save()) {
      Yii::$app->session->setFlash('success', 'Все прошло удачно');
      return $this->redirect(['view', 'id' => $model->id]);
    }
  }

  return $this->render('update', [
    'model' => $model,
    'modelForm' => $modelForm,
  ]);
}

// Загрузка изображения
public function actionAjaxUploadImage($id)
{
  Yii::$app->response->format = Response::FORMAT_JSON;
  if(Yii::$app->request->isAjax){
    $model = $this->findModel($id);
    $form = new NewsImageForm();
    $form->image = UploadedFile::getInstance($form, 'image');
    if($form->image && $form->validate()){
      $model->image = $form->uploadImage($form->image, $model->image);
      if ($model->save(false)) {
        return $data = [
          'success' => true,
          'image' => $model->getImagePath()
        ];
      }
    }
  }
  return $data = [
    'success' => false
  ];
}


// Удаление изображения
public function actionAjaxDeleteImage($id)
{
  Yii::$app->response->format = Response::FORMAT_JSON;
  if (Yii::$app->request->isAjax) {
    $model = $this->findModel($id);
    $model->deleteImage();
    $model->image = '';
    if ($model->save(false)) {
      return $data = [
        'success' => true,
        'image' => $model->getImagePath()
      ];
    }
  }
  return $data = [
    'success' => false
  ];
}