How to create a comment system using Laravel Livewire: Part 1
By Nihar Ranjan Das, Published on June 6th 2023 | 20 mins, 1933 words
When I first watch a Livewire tutorial on Youtube, I was just surprised. I saw a form submit without page refresh and without any JavaScript code. Then I start learning about Livewire Laravel. And I loved it so much, while I learning it.
In this article, I will show you, how we can create a comment system using Laravel Livewire. Let's dive deep and explore.
Install Livewire into Laravel
Run the bellow code and install Livewire into Laravel
composer require livewire/livewire
Then Include the style in the head section and the scripts before the end of the body section.
<head> ... @livewireStyles </head> <body> ... @livewireScripts </body> </html>
If you want to use the livewire for only the comment system, then add those styles & scripts to the target page
Database Schema
For the comment system, we will use MySQL and we need three tables.
- users table
- posts table
- comments table
For those tables, we need three models also in the model's folder
- User.php
- Post.php
- Comment.php
In this system, a post has multiple comments also a user has multiple comments. A comment belongs to a post and a user. There will be multiple replies to a comment also.
Let's make the relationships in the models.
In User model
// app/Models/User.php public function comments(): HasMany { return $this->hasMany(Comment::class); }
In Post model
// app/Models/Post.php public function comments(): HasMany { return $this->hasMany(Comment::class); }
In Comment model
// app/Models/Comment.php public function user(): BelongsTo { return $this->belongsTo(User::class); } public function replies(): HasMany { return $this->hasMany(Comment::class, 'parent_id'); } public function parentComment(): BelongsTo { return $this->belongsTo(Comment::class, 'parent_id'); } public function post(): BelongsTo { return $this->belongsTo(Post::class); }
Create Livewire Components
Generate a new Livewire component called Comments by running the following command.
php artisan make:livewire Comments
The following command will generate two files
// app/Http/Livewire/Comments.php namespace App\Http\Livewire; use Livewire\Component; class Counter extends Component { public function render() { return view('livewire.comments'); } }
// resources/views/livewire/comments.blade.php <div> ... </div>
We also need two more Livewire components for the comment system
// For comment create form php artisan make:livewire CommentCreate // For comment single item php artisan make:livewire CommentItem
Let's add some HTML to the generated views and some code to the generated classes for the comment system
All Livewire components MUST have a single root element
Comments Component
Here is the full code of the Comment component. The see the code description click here.
// app/Http/Livewire/Comments.php namespace App\Http\Livewire; use App\Models\Comment; use App\Models\Post; use Illuminate\Database\Eloquent\Collection; use Livewire\Component; class Comments extends Component { public Post $post; public Collection $comments; public $listeners = [ 'commentCreated' => 'commentCreated', 'commentDeleted' => 'commentDeleted', ]; public function mount(Post $post) { $this->post = $post; $this->comments = $this->getComments(); } public function render() { $comments = $this->comments; return view('livewire.comments', compact('comments')); } public function commentCreated(int $id) { $comment = Comment::query()->find($id); if (!$comment->parent_id) { $this->comments = $this->comments->prepend($comment); } } public function commentDeleted(int $id) { $this->comments = $this->comments->reject(function ($comment, int $key) use ($id) { return $comment->id == $id; }); } private function getComments(): Collection|array { return Comment::query() ->with([ 'user', 'replies' ]) ->where('post_id', $this->post->id) ->whereNull('parent_id') ->orderByDesc('created_at') ->get(); } }
// resources/views/livewire/comments.blade.php <div class="w-full"> <p class="mb-4"><span>{{ $comments->count() > 0 ? $comments->count() : '' }}</span> Comments</p> <livewire:comment-create :post-id="$post->id" /> <div> @if($comments->count() > 0) @foreach($comments as $comment) <livewire:comment-item wire:key="{{ $comment->id }}" :comment="$comment" /> @endforeach @endif </div> </div>
Comment Form Component
This is the code of the Comment Create form component. The see the code description click here.
// app/Http/Livewire/CommentCreate.php namespace App\Http\Livewire; use App\Models\Comment; use App\Models\User; use Illuminate\Support\Facades\Auth; use Livewire\Component; class CommentCreate extends Component { public ?User $user = null; public ?Comment $commentModel = null; public string $comment = ''; public int $postId; public ?int $parentId = null; public ?string $message = null; public bool $showProfile = true; protected array $rules = [ 'comment' => 'required|string' ]; protected array $messages = [ 'comment.required' => 'Comment is required', 'comment.string' => 'Invalid comment', ]; public function mount( int $postId, $commentModel = null, ?int $parentId = null, bool $showProfile = true ) { if (Auth::check()) { $this->user = Auth::user(); } $this->postId = $postId; $this->parentId = $parentId; $this->commentModel = $commentModel; $this->comment = $this->commentModel?->comment ?? ''; $this->showProfile = $showProfile; } public function resetForm() { $this->comment = ''; $this->parentId = null; $this->commentModel = null; $this->emitUp('hideCommentForm'); } public function createComment() { $this->validate(); if (!\auth()->check()) { return $this->redirect(route('login')); } if ($this->commentModel && $this->commentModel->comment) { if ($this->user->id != $this->commentModel->user_id) { return response('You are allowed to perform this action.')->status(403); } $this->commentModel->comment = $this->comment; $this->commentModel->save(); $this->emitUp('commentUpdated'); } else { Comment::query() ->create([ 'comment' => $this->comment, 'post_id' => $this->postId, 'user_id' => $this->user->id, 'parent_id' => $this->parentId ]); $this->emitUp('commentCreated', $comment->id); } $this->comment = ''; } public function render() { return view('livewire.comment-create'); } }
// resources/views/livewire/comment-create.blade.php <div> <form wire:submit.prevent="createComment" x-data="{ focused: {{ $parentId ? 'true' : 'false' }}, isEdit: {{ $commentModel ? 'true' : 'false' }}, init() { if (this.isEdit || this.focused) { this.$refs.input.focus(); } $wire.on('closeCommentBoxFromCreate', () => { this.focused = false; }) $wire.on('hideCommentForm', () => { this.focused = false; this.isEdit = false; }) } }"> <div class="flex items-start gap-2 justify-start"> @if($showProfile) <div class="overflow-hidden rounded-full w-8 h-8"> <img class="object-cover aspect-[3/3]" src="{{ asset($user ? "storage/$user->avatar" : 'images/default-user.png') }}" alt="{{ $user ? $user->name : 'Default avatar' }}"> </div> @endif <div class="flex-1"> <textarea x-ref="input" name="comment" @click="focused = true" id="comment" wire:model.lazy="comment" placeholder="Write a comment..." class="block w-full border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 font-medium transition-all duration-300 ease-in-out" :rows="focused || isEdit ? 4 : 1"></textarea> @error('comment') <x-input-error class="mt-2 text-sm" :messages="$message" /> @enderror </div> </div> <div class="text-right mt-4 {{ strlen($comment) > 0 ? 'block' : 'hidden' }}"> <button type="submit" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">Post</button> <button wire:click="resetForm" type="button" class="inline-flex items-center px-4 py-2 border border-transparent font-semibold text-xs text-gray-800 uppercase tracking-widest hover:text-gray-700 focus:text-gray-700 active:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">Cancel</button> </div> </form> </div>
Single Comment Component
This is the code of single comment box component. The see the code description click here.
// app/Http/Livewire/CommentItem.php namespace App\Http\Livewire; use App\Models\Comment; use Livewire\Component; class CommentItem extends Component { public Comment $comment; public bool $replying = false; public bool $editing = false; public $listeners = [ 'commentUpdated' => 'commentUpdated', 'commentCreated' => 'commentCreated', 'hideCommentForm' => 'hideCommentForm' ]; public function mount(Comment $comment) { $this->comment = $comment; } public function render() { return view('livewire.comment-item'); } public function startReplying() { $this->replying = true; $this->editing = false; } public function cancelReplying() { $this->replying = false; } private function commentUpdated() { $this->editing = false; } public function hideCommentForm() { $this->cancelReplying(); $this->cancelEditing(); } public function deleteComment() { $user = auth()->user(); if (!$user) { return $this->redirect(route('login')); } if ($user->id != $this->comment->user_id) { return response('You are allowed to perform this action.')->status(403); } $id = $this->comment->id; $this->comment->delete(); $this->emitUp('commentDeleted', $id); } private function commentCreated(int $replyId) { $this->cancelReplying(); } }
// resources/views/livewire/comment-item.blade.php <div> <div class="flex items-start gap-4 justify-start mt-4"> <div class="overflow-hidden rounded-full w-8 h-8"> <img class="object-cover aspect-[3/3]" src="{{ asset($comment->user->avatar ?? 'assets/default-user.png') }}" alt="{{ $comment->user->name }}"> </div> <div class="flex-1"> <div class=""> <div> <div class="flex items-center justify-start gap-1"> <h3 class="font-semibold">{{ $comment->user->name }}</h3> <span>-</span> <p class="text-xs">{{ $comment->getFormattedDate() }}</p> </div> @if($editing) <livewire:comment-create wire:key="'edit-'.$comment->id" :post-id="$comment->post_id" :comment-model="$comment" :show-profile="false" /> @else <p class="text-sm"> {{ $comment->comment }} </p> @endif <div class="flex items-center justify-start gap-4"> <p wire:click.prevent="startReplying" class="flex items-center uppercase text-[10px] cursor-pointer font-semibold transition-all duration-300 ease-in-out hover:underline"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-[13px] h-[13px]"> <path fill-rule="evenodd" d="M2 10c0-3.967 3.69-7 8-7 4.31 0 8 3.033 8 7s-3.69 7-8 7a9.165 9.165 0 01-1.504-.123 5.976 5.976 0 01-3.935 1.107.75.75 0 01-.584-1.143 3.478 3.478 0 00.522-1.756C2.979 13.825 2 12.025 2 10z" clip-rule="evenodd" /> </svg> <span class="ml-1">Reply</span> </p> @if(auth()->check() && auth()->id() == $comment->user_id) <p wire:click.prevent="startCommentEdit" class="flex items-center uppercase text-[10px] cursor-pointer font-semibold transition-all duration-300 ease-in-out hover:underline"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-[13px] h-[13px]"> <path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" /> <path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" /> </svg> <span class="ml-1">Edit</span> </p> <p wire:click.prevent="deleteComment" class="flex items-center uppercase text-[10px] cursor-pointer font-semibold transition-all duration-300 ease-in-out hover:underline"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-[13px] h-[13px]"> <path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z" clip-rule="evenodd" /> </svg> <span class="ml-1">Delete</span> </p> @endif </div> </div> @if($comment->replies) @foreach($comment->replies as $reply) <livewire:comment-item wire:key="{{ $reply->id }}" :comment="$reply" /> @endforeach @endif </div> @if($replying) <livewire:comment-create wire:key="'reply-'.$comment->id" :post-id="$comment->post_id" :parent-id="$comment->id" :show-profile="false" /> @endif </div> </div> </div>
This will be a big article. Let's split it and describe those components part by part in separate articles.