در دو قسمت قبلی از مجموعه مقالات Livewire چیست، علاوه بر آشنایی با فریم ورک فول استک Livewire در لاراول، فرآیند و شیوهی کار با آن را آموختیم. کامپوننت مورد نظر خود را ایجاد کردیم و با خصوصیات و متدهای آن آشنا شدیم. نحوهی کار با اکشنها را فراگرفتیم و توانستیم با استفاده از Modifierها، قابلیتهایی را به اکشنها اضافه کنیم. ارتباط با دیتابیس را برقرار کردیم و توانستیم نحوهی کار با Lifecycle Hookها را بیاموزیم. همچنین نحوهی صفحه بندی و اعتبارسنجیِ Real-Time در Livewire را به صورت عملی آموختیم.
در قسمت سوم و پایانی از مجموعهی Livewire چیست، پروژهی خود را کامل میکنیم و نحوهی آپلود فایل در Livewire را میآموزیم. سپس با مفهوم Polling آشنا میشویم و پس از آن، با ایجاد بخش مطالب و استفاده از Events and Listeners، ارتباط بین کامپوننتهای یک صفحه را برقرار میکنیم. پس تا انتهای این مقاله با ما همراه باشید.
فهرست محتوای این مقاله
آپلود فایل
در این بخش قصد داریم نحوهی آپلود فایل در Livewire را بدون نیاز به Refresh شدن صفحه به شما آموزش دهیم. برای این منظور، قصد داریم یک ویژگی جدید به پروژه اضافه کنیم که به وسیلهی آن، کاربر بتواند در کنار هر نظر، یک تصویر را نیز ارسال کند.
تغییر دیتابیس
ابتدا نیاز است که یک ستون جدید با نام imageUrl را به جدول comments اضافه کنیم. همچنین ستون post_id را برای بخش مطالب نیاز داریم. پس فایل Migration مربوط به جدول comments را به صورت زیر ویرایش کنید:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCommentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('post_id');
$table->text('body');
$table->string('imageUrl');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('comments');
}
}
و سپس دستور زیر را اجرا کنید:
php artisan migrate
تغییر View
سپس نیاز است که در فایل comments.blade.php موجود در مسیر resources/views/livewire، که همان خروجی کامپوننت است، تغییراتی ایجاد کنیم.
<div class="mx-3">
<h1 style="font-size: 2rem">Comments</h1>
@error('newComment') <div class="alert-danger p-3 rounded">{{ $message }}</div> @enderror
@error('image') <div class="alert-danger p-3 rounded mt-1">{{ $message }}</div> @enderror
<section>
<div class="mt-3">
<div wire:loading wire:target="image">Uploading...</div>
@if ($image)
Image Preview:
<img src="{{ $image->temporaryUrl() }}" width="200" />
@endif
</div>
</section>
<form class="my-4 d-flex flex-column" wire:submit.prevent="addComment">
<input type="file" class="py-2 my-2" wire:model="image">
<div class="d-flex">
<input type="text" class="w-100 rounded border shadow py-2 px-3 mr-2 my-2" placeholder="Write your comment here..." wire:model.debounce.500ms="newComment">
<div class="py-2">
<button type="submit" class="p-2 btn btn-success w-20 rounded shadow text-white">Add</button>
</div>
</div>
</form>
@foreach($comments as $comment)
<div class="rounded border shadow p-3 my-2">
<div class="d-flex justify-content-between my-2">
<p>{{ $comment['body'] }}</p>
<div class="d-flex">
<img src="{{$comment->imageUrl}}" />
<i class="fas fa-times text-danger ml-3" style="cursor: pointer;"
wire:click="remove({{$comment->id}})"></i>
</div>
</div>
</div>
@endforeach
@if($comments->isNotEmpty())
<div class="d-flex justify-content-center mt-4">
{{ $comments->links() }}
</div>
@endif
</div>
با مشاهدهی کد فوق میبینید که در فرم ارسال نظری که قبلا داشتیم، یک ورودی فایل را اضافه کردهایم. این ورودی نیز مثل سایر تگهای input، با استفاده از wire:model اطلاعات فایل را به سمت سرور ارسال میکند.
بالاتر از تگ form، از یک تگ section استفاده کردهایم. درون این تگ، ابتدا از یک تگ div برای نمایش Loading Indicator، تا زمانی که تصویر به صورت کامل بارگذاری شود، استفاده کردهایم. در این تگ، دستور wire:loading را نیز مشاهده میکنید. تگهایی که از این دستور بهره میبرند، فقط در زمان انتظار تا کامل شدن یک اکشن خاص، Visible یا قابل مشاهده هستند. همچنین، با استفاده از دستور wire:target ،Loading Indicator را به اکشن مورد نظرمان که در اینجا image است، متصل کردهایم. در ادامه از یک تگ img برای پیشنمایش تصویر بارگذاری شده استفاده کردهایم که Url موقت تصویر را میگیرد و نمایش میدهد.
یک تگ div را نیز بالاتر از تگ section، برای نمایش پیغامهای اخطار مربوط به اعتبارسنجی تصویر قرار دادهایم.
در انتها نیز، برای نمایش تصاویر در بخش نظرات، از یک تگ img استفاده کردهایم.
تغییر کامپوننت
اکنون که تغییرات موردنظرمان را در دیتابیس و View ایجاد کردهایم، نوبت آن رسیده که عکس ارسال شده را ذخیرهسازی کنیم:
<?php
namespace App\Http\Livewire;
use App\Comment;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\WithPagination;
class Comments extends Component
{
use WithPagination;
use WithFileUploads;
protected $paginationTheme = 'bootstrap';
public $newComment;
public $image;
public function updated($field)
{
$this->validateOnly($field, ['newComment' => 'required|max:255','image' => 'image|max:1024']);
}
public function addComment()
{
$imagePath = $this->image->store('images', 'public');
$this->validate(['newComment' => 'required|max:255', 'image' => 'required|max:1024']);
$newComment = Comment::create([
'body' => $this->newComment,
'imageUrl' => Storage::url($imagePath),
'user_id' => 1
]);
$this->newComment = '';
$this->image = '';
}
public function remove($commentId)
{
$comment = Comment::find($commentId)->delete();
}
public function render()
{
return view('livewire.comments', [
'comments' => Comment::latest()->paginate(2),
]);
}
}
اگر کدهای فوق را با دقت ملاحظه کنید، میبینید که از Trait دیگری با نام WithFileUploads استفاده کردهایم که در هستهی Livewire تعریف شده است. با استفاده از دستور wire:model در تگ ورودی تصویر و همچنین ایجاد خصوصیت image در کامپوننت، این Trait وظیفهی ذخیرهی اطلاعات فایل ارسالی در این خصوصیت را بر عهده میگیرد.
قوانین مربوط به اعتبارسنجی تصویر ارسالی را نیز به متدهای ()updated و ()addComment افزودهایم. در متد ()addComment برای ذخیرهی تصویر، از متد ()store استفاده کردهایم. این متد، تصویر را در پوشهای با نام موجود در پارامتر اول در دیسک ذخیرهسازی پیشفرض یا Default Filesystem Disk ذخیره میکند. تنظیمات مربوط به دیسک ذخیرهسازی پیشفرض در فایل filesystem.php در پوشهی config موجود است. در پارامتر دوم میتوان دیسک دلخواه خود را به متد ()store پاس داد که ما در اینجا از دیسک public استفاده کردهایم.
همچنین با استفاده از Storage Facade، آدرس Url نسبی را در رکورد دیتابیس ذخیره کرده و در انتهای متد ()addComment، خصوصیت image را با رشتهی خالی جایگزین کردهایم.
اکنون میتوانیم تصاویر دلخواه را بدون نیاز به Refresh شدن صفحه، ارسال کنیم و در بخش نظرها نمایش دهیم:
Polling و ایجاد حالت Real-Time
در این حالت نیاز داریم که هر نظر جدید، بدون نیاز به Refresh کردن در صفحه نمایش داده شود. برای این منظور از ویژگی Polling در Livewire استفاده میکنیم. پس در صفحه comments.blade.php در پوشهی views، حلقهی نظرها و شرط مربوط به ایجاد صفحه بندی را درون یک تگ div جدید قرار میدهیم و از دستور wire:poll در تگ ایجاد شده استفاده میکنیم:
<div class="mx-3">
<h1 style="font-size: 2rem">Comments</h1>
@error('newComment') <div class="alert-danger p-3 rounded">{{ $message }}</div> @enderror
@error('image') <div class="alert-danger p-3 rounded mt-1">{{ $message }}</div> @enderror
<section>
<div class="mt-3">
<div wire:loading wire:target="image">Uploading...</div>
@if ($image)
Image Preview:
<img src="{{ $image->temporaryUrl() }}" width="200" />
@endif
</div>
</section>
<form class="my-4 d-flex flex-column" wire:submit.prevent="addComment">
<input type="file" class="py-2 my-2" wire:model="image">
<div class="d-flex">
<input type="text" class="w-100 rounded border shadow py-2 px-3 mr-2 my-2" placeholder="Write your comment here..." wire:model.debounce.500ms="newComment">
<div class="py-2">
<button type="submit" class="p-2 btn btn-success w-20 rounded shadow text-white">Add</button>
</div>
</div>
</form>
<div wire:poll>
@foreach($comments as $comment)
<div class="rounded border shadow p-3 my-2">
<div class="d-flex justify-content-between my-2">
<p>{{ $comment['body'] }}</p>
<div class="d-flex">
<img src="{{$comment->imageUrl}}" />
<i class="fas fa-times text-danger ml-3" style="cursor: pointer;"
wire:click="remove({{$comment->id}})"></i>
</div>
</div>
</div>
@endforeach
@if($comments->isNotEmpty())
<div class="d-flex justify-content-center mt-4">
{{ $comments->links() }}
</div>
@endif
</div>
</div>
با استفاده از این دستور، بخشی که درون تگ div جدید قرار میگیرد، هر 2 ثانیه یک بار بروز میشود و میتوانیم نظرات جدید را نیز مشاهده کنیم. این زمان 2 ثانیه را میتوانیم با استفاده از Modifier به مقدار دلخواه تغییر دهیم. برای مثال از دستور wire:poll.750ms استفاده کنیم که باعث میشود محتوای تگ div، هر 750 میلیثانیه بروز شود. همچنین میتوان یک اکشن خاص را به دستور فوق پاس داد. یعنی برای مثال، با استفاده از دستور ”wire:poll=”foo، متد دلخواه foo در کامپوننت، هر 2 ثانیه یکبار صدا زده میشود.
Events and Listeners
کامپوننتهای مختلف Livewire اگر در یک صفحه قرار داشته باشند، میتوانند از طریق Eventها و Listenerها با هم در ارتباط باشند. این مفهوم را در ادامه برای شما تشریح خواهیم کرد، ولی ابتدا نیاز است بخش Post را به پروژه اضافه کنیم.
ایجاد بخش مطالب
قصد داریم به منظور تکمیل پروژه، بخش مطالب یا Posts را نیز راهاندازی کنیم و بتوانیم برای هر مطلب، نظر مخصوص به آن مطلب را ارسال یا مشاهده کنیم.
ایجاد جدول
فایل Migration، جدول نظرات را با استفاده از دستور زیر ایجاد کنید:
php artisan make:migration create_posts_table --create=posts
سپس در فایل ایجاد شده در مسیر database/migrations:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('posts');
}
}
و دستور زیر را اجرا کنید:
php artisan migrate
پس از اجرای این دستور، به صورت دستی یا با استفاده از Seeder در لاراول، چند مطلب را به جدول مطالبی که ایجاد کردهایم، اضافه کنید.
ایجاد Model
دستور زیر را اجرا کنید:
php artisan make:model Post
سپس فایل Post.php در پوشهی app را به شکل زیر ویرایش کنید:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $guarded = [];
public function comments()
{
return $this->hasMany(Comment::class, 'post_id');
}
}
میبینید که رابطهی یک به چند بین جدول مطالب و نظرات را تعریف کردهایم.
ایجاد کامپوننت
کامپوننت مطالب را با اجرای دستور زیر، ایجاد کنید.
php artisan make:livewire Posts
اکنون باید خروجی کامپوننت را به شکل زیر در فایل welcome.blade.php در مسیر resources/views، با استفاده از دستوری که قبلا در Livewire بررسی کردیم، قرار دهیم:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Livewire</title>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css"
integrity="sha256-h20CPZ0QyXlBuAw7A+KluUYx/3pK+c7lYEpqLTlxjYQ=" crossorigin="anonymous" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<livewire:styles />
<style>
* {
font-family: 'Nunito', monospace;
}
</style>
</head>
<body class="d-flex justify-content-center">
<div class="w-100 d-flex m-5">
<div class="w-100 mt-3 rounded border p-5">
<livewire:posts />
</div>
<div class="w-100 mt-3 ml-1 rounded border p-5">
<livewire:comments />
</div>
</div>
<livewire:scripts />
</body>
</html>
سپس در فایل posts.blade.php در مسیر resources/views/livewire:
<div class="mx-3">
<h1>Posts</h1>
@foreach($posts as $post)
<div class="rounded border p-3 my-2 {{$active == $post->id ? 'bg-success':''}}" style="cursor: pointer"
wire:click="$emit('postSelected',{{$post->id}})">
<h3>{{$post->title}}</h3>
<p class="text-secondary">{{$post->body}}</p>
</div>
@endforeach
</div>
اگر به تگ div هر مطلب دقت کنید، میبینید که در دستور wire:click به جای اکشن، از یک Event یا رویداد استفاده کردهایم. با استفاده از ()emit رویداد postSelected را صدا زدهایم و پارامتر post->id$ را نیز به همراه آن ارسال کردهایم. این رویداد را برای انتخاب مطلب دلخواه تعریف کردهایم. در قسمت دیگر این تگ نیز از خصوصیت active استفاده کردهایم تا با استفاده از رنگ پسزمینه، مطلب انتخاب شده را از سایر مطالب جدا کنیم.
سپس در کامپوننت Posts ایجاد شده در مسیر app/Http/Livewire:
<?php
namespace App\Http\Livewire;
use App\Post;
use Livewire\Component;
class Posts extends Component
{
public $active ;
protected $listeners = ['postSelected'];
public function postSelected($postId)
{
$this->active = $postId;
}
public function render()
{
return view('livewire.posts', [
'posts' => Post::latest()->get(),
]);
}
}
در اینجا خصوصیت listeners را میبینید که به صورت protected تعریف شده است. این خصوصیت، هر رویداد را به متدی در کامپوننت مورد نظر مرتبط میسازد. کلید آرایه و نام رویداد و مقدار آن، نام متد مورد نظر است. اگر رویداد و متد همنام باشند، مانند کد فوق، فقط همان نام را به عنوان مقدار در آرایه قرار دهید.
سپس در کامپوننت Comments:
<?php
namespace App\Http\Livewire;
use App\Comment;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\WithPagination;
class Comments extends Component
{
use WithPagination;
use WithFileUploads;
protected $paginationTheme = 'bootstrap';
public $newComment;
public $image;
public $postId ;
protected $listeners = [
'postSelected' => 'activePost'
];
public function activePost($postId)
{
$this->postId = $postId;
}
public function updated($field)
{
$this->validateOnly($field, ['newComment' => 'required|max:255','image' => 'image|max:1024']);
}
public function addComment()
{
$imagePath = $this->image->store('images', 'public');
$this->validate(['newComment' => 'required|max:255', 'image' => 'required|max:1024']);
$newComment = Comment::create([
'body' => $this->newComment,
'imageUrl' => Storage::url($imagePath),
'user_id' => 1,
'post_id' => $this->postId
]);
$this->newComment = '';
$this->image = '';
}
public function remove($commentId)
{
$comment = Comment::find($commentId)->delete();
}
public function render()
{
return view('livewire.comments', [
'comments' => Comment::where('post_id', $this->postId)->latest()->paginate(2),
]);
}
}
همانطور که میبینید خصوصیت listeners را تعریف کردهایم و همان رویداد را به عنوان کلید در آن قرار دادهایم. در واقع مزیت ویژگی Events and Listeners در Livewire این است که میتوان با استفاده از یک رویداد، چند متد را در چند کامپوننت موجود در یک صفحه، صدا زد و بین این کامپوننتها ارتباط برقرار کرد. در اینجا رویداد و متد را همنام قرار ندادهایم تا بتوانید نحوهی ارتباط آنها در این حالت را نیز مشاهده کنید.
هنگام ذخیرهسازی نظر در متد ()addComment، مقدار post_id را نیز اضافه کردهایم و در متد ()render نیز، با استفاده از شرط ()where فقط نظرات مربوط به مطلب انتخاب شده را به سمت کاربر ارسال میکنیم.
در نهایت، خروجی صفحه به شکل زیر خواهد بود:
قسمتهای دیگر این آموزش:
جمعبندی:
در این مقاله از سری مقالات آموزش لاراول و در ادامهی مجموعهی مقالات Livewire چیست، توانستیم با ویژگیهای بیشتری از Livewire آشنا شویم و نحوهی استفاده از آنها را به صورت عملی فراگیریم. نحوهی آپلود فایل را در Livewire به صورت عملی آموختیم. با Polling آشنا شدیم و توانستیم نظراتمان را به شکل Real-Time برای کاربران نمایش دهیم. کامپوننت مطالب را ایجاد کردیم و به واسطهی آن، مفهوم Events and Listeners در Livewire را فرا گرفتیم.
به عنوان تمرین و به منظور جا افتادن مطالب، بخش ورود و ثبت نام را با آموختههایی که در این مجموعه مقالات داشتید، پیادهسازی کنید. امیدواریم که در پایان این مجموعه مقالات، به درک مناسبی از فریم ورک فول استک Livewire در لاراول رسیده باشید و بتوانید در پروژههای خود از ویژگیهای جذاب آن، بهره ببرید.
خوشحال میشویم نظرات، تجربیات و سوالات خود را با ما و سایر کاربران سون لرن به اشتراک بگذارید.
اگر به یادگیری بیشتر لاراول علاقه دارید میتوانید در دوره آموزشی لاراول کاربردی (بسته پروژه محور) شرکت کنید، این دوره شامل ۱۲ پروژه کاربردی و پر استفاده در دنیای واقعی است، که تمامی پروژهها به صورت کامل برنامه نویسی خواهند شد، تا دانشجو بتواند با روند ایجاد و تکمیل پروژه به صورت کامل آشنا شود.
ممنون
کامل و مفید بود