Today we are going to create the 'like' button you see at the bottom of this post using Laravel Livewire and Tailwind CSS. We will have it restrict clicks first based on user if available, otherwise based on IP address and user agent.
We will create all the needed files:
- Migration
- Model
- Supporting Methods
- Livewire Component
- Livewire View
We need to set up some supporting files first that are necessary for this tutorial to work.
Now you can definitely set this up to be morphable and work with multiple models, but for the sake of this tutorial and this site which only needed one 'like' button, I went with a specific model to the resource I was working with.
First we will set up a simple post model and migration:
1<?php 2 3use Illuminate\Database\Migrations\Migration; 4use Illuminate\Database\Schema\Blueprint; 5use Illuminate\Support\Facades\Schema; 6 7class CreatePostsTable extends Migration 8{ 9 public function up()10 {11 Schema::create('posts', function (Blueprint $table) {12 $table->id();13 $table->foreignId('user_id')->nullable();14 $table->string('title');15 $table->text('body')->nullable();16 $table->timestamps();17 });18 }19}
1<?php 2 3namespace App\Domains\Blog\Models; 4 5use Illuminate\Database\Eloquent\Model; 6 7class Post extends Model 8{ 9 protected $fillable = [10 'title',11 'body',12 ];13 14 protected static function booted()15 {16 // We will automatically add the user to the post when it's saved.17 static::creating(function ($post) {18 if (auth()->user()) {19 $post->user_id = auth()->id();20 }21 });22 }23}
Obviously the actual post model has a little more functionality than this, but this is good for the sake of this example.
Next, we will set up the model and migration for the PostLike resource.
1<?php 2 3use Illuminate\Database\Migrations\Migration; 4use Illuminate\Database\Schema\Blueprint; 5use Illuminate\Support\Facades\Schema; 6 7class CreatePostLikesTable extends Migration 8{ 9 public function up()10 {11 Schema::create('post_likes', function (Blueprint $table) {12 $table->id();13 $table->foreignId('post_id');14 $table->foreignId('user_id')->nullable();15 $table->ipAddress('ip')->nullable();16 $table->string('user_agent')->nullable();17 $table->timestamps();18 });19 }20}
1<?php 2 3namespace App\Domains\Blog\Models; 4 5use Illuminate\Database\Eloquent\Model; 6 7class PostLike extends Model 8{ 9 protected $fillable = [10 'user_id',11 'ip',12 'user_agent',13 ];14}
Next we need to specify the relationships we need to work with.
We need to add this relationship to both the User and Post models:
1use App\Domains\Blog\Models\PostLike;2use Illuminate\Database\Eloquent\Relations\HasMany;3 4...5public function likes(): HasMany6{7 return $this->hasMany(PostLike::class);8}9...
We are finally up to creating our Livewire component!
We will use the built-in Livewire command to create our new component:
1php artisan make:livewire Like
This will create us two new files:
1app/Http/Livewire/Like.php2resources/views/livewire/like.blade.php
Our Livewire component will need to accept the current Post as a parameter, and will have a single method called like()
that will handle both liking and un-liking:
1<?php 2 3namespace App\Http\Livewire\Frontend\Blog\Post; 4 5use App\Models\Post; 6use Livewire\Component; 7 8class Like extends Component 9{10 public Post $post;11 12 public function mount(Post $post)13 {14 $this->post = $post;15 }16 17 public function like(): void18 {19 // TODO20 }21 22 public function render()23 {24 return view('components.like');25 }26}
Our view will look like this:
1<span class="inline-flex items-center text-sm"> 2 <button wire:click="like" class="inline-flex space-x-2 {{ $post->isLiked() ? 'text-green-400 hover:text-green-500' : 'text-gray-400 hover:text-gray-500' }} focus:outline-none focus:ring-0"> 3 <svg class="h-5 w-5" x-description="solid/thumb-up" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> 4 <path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.56 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z"></path> 5 </svg> 6 7 <span class="font-medium text-gray-900">{{ $count }}</span> 8 <span class="sr-only">likes</span> 9 </button>10</span>
This is what it will look like when inactive:
This is what it will look like when active:
There are a few convenience methods we will need that we will create soon.
First, we need to pass through the current like
count to the view, this will update automatically when the property is changed thanks to Livewire.
We will use the withCount property on the model to always load the current amount of likes when we retrieve a post:
This goes on the Post model:
1protected $withCount = [2 'likes',3];
Then we will pass it through to the view:
1<?php 2 3namespace App\Http\Livewire\Frontend\Blog\Post; 4 5use App\Models\Post; 6use Livewire\Component; 7 8class Like extends Component 9{10 public Post $post;11 public int $count;12 13 public function mount(Post $post)14 {15 $this->post = $post;16 $this->count = $post->likes_count;17 }18}
As you can see from the view, we have a wire:click="like"
on our button, so we need to implement that method:
Our method needs to do a few things:
- If the post is already liked, remove that like.
- If not, and a user is logged in, add a like to that user.
- If no user is logged in, add a like based on the IP and user agent.
Here is what the final method will look like:
1public function like(): void 2{ 3 if ($this->post->isLiked()) { 4 $this->post->removeLike(); 5 6 $this->count--; 7 } elseif (auth()->user()) { 8 $this->post->likes()->create([ 9 'user_id' => auth()->id(),10 ]);11 12 $this->count++;13 } elseif (($ip = request()->ip()) && ($userAgent = request()->userAgent())) {14 $this->post->likes()->create([15 'ip' => $ip,16 'user_agent' => $userAgent,17 ]);18 19 $this->count++;20 }21}
As you can see if the button is clicked and the post already has a like, we remove that like. If the user is logged in, we add a like to the post using the likes relationship and the current user. If there is no user logged in, we add a like to that post using the current IP address and user agent (If we can ge them from the request).
I extracted out the checking for existing like and remove like functionality to a method on the model for convenience:
1public function isLiked(): bool 2{ 3 if (auth()->user()) { 4 return auth()->user()->likes()->forPost($this)->count(); 5 } 6 7 if (($ip = request()->ip()) && ($userAgent = request()->userAgent())) { 8 return $this->likes()->forIp($ip)->forUserAgent($userAgent)->count(); 9 }10 11 return false;12}13 14public function removeLike(): bool15{16 if (auth()->user()) {17 return auth()->user()->likes()->forPost($this)->delete();18 }19 20 if (($ip = request()->ip()) && ($userAgent = request()->userAgent())) {21 return $this->likes()->forIp($ip)->forUserAgent($userAgent)->delete();22 }23 24 return false;25}
As you can see again, I extracted even more functionality out to reusable scopes for the PostLike model:
1public function scopeForPost($query, Post $post) 2{ 3 return $query->where('post_id', $post->id); 4} 5 6public function scopeForIp($query, string $ip) 7{ 8 return $query->where('ip', $ip); 9}10 11public function scopeForUserAgent($query, string $userAgent)12{13 return $query->where('user_agent', $userAgent);14}
Here is what our final component looks like:
Here is our final component code:
1<?php 2 3namespace App\Http\Livewire\Frontend\Blog\Post; 4 5use App\Domains\Blog\Models\Post; 6use Livewire\Component; 7 8class Like extends Component 9{10 public Post $post;11 public int $count;12 13 public function mount(Post $post)14 {15 $this->post = $post;16 $this->count = $post->likes_count;17 }18 19 public function like(): void20 {21 if ($this->post->isLiked()) {22 $this->post->removeLike();23 24 $this->count--;25 } elseif (auth()->user()) {26 $this->post->likes()->create([27 'user_id' => auth()->id(),28 ]);29 30 $this->count++;31 } elseif (($ip = request()->ip()) && ($userAgent = request()->userAgent())) {32 $this->post->likes()->create([33 'ip' => $ip,34 'user_agent' => $userAgent,35 ]);36 37 $this->count++;38 }39 }40 41 public function render()42 {43 return view('components.livewire.frontend.blog.post.like');44 }45}
1<span class="inline-flex items-center text-sm"> 2 <button wire:click="like" class="inline-flex space-x-2 {{ $post->isLiked() ? 'text-green-400 hover:text-green-500' : 'text-gray-400 hover:text-gray-500' }} focus:outline-none focus:ring-0"> 3 <svg class="h-5 w-5" x-description="solid/thumb-up" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> 4 <pat d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.56 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z"></pat> 5 </svg> 6 7 <span class="font-medium text-gray-900">{{ $count }}</span> 8 <span class="sr-only">likes</span> 9 </button>10</span>
I hope you enjoyed this super simple tutorial on creating a Livewire component. Check back soon for more advanced tutorials on Livewire and all things Laravel.