Yii2 + jQuery ajax - отправка данных, загрузка файла на сервер
В этом уроке мы рассмотрим реализацию асинхронной отправки данных из формы на примере простых комментариев (условный пример). При заполнении поля для текста комментария и нажатия на кнопку отправки, комментарий записывается в БД и без перезагрузки страницы выводится под формой.
Во втором примере мы с помощью 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 ]; }