Web Development Handbook

Web Development Handbook

Development is fun in a funny way

How to create a comment system using Laravel Livewire: Part 1

How to create a comment system using Laravel Livewire: Part 1

0 votes
0 votes
0 comments
846 views
Share

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 

The full layout of the comment system

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.

  1. Comment create form
  2. Comments component
  3. Single comment block


If you like our tutorial, do make sure to support us by buy us a coffee ☕️

Comments

Default avatar

Are you interested to learn more?

Be notified on future content. Never spam.