Star Rating Component using Livewire & AlpineJS
Tailwind CSS Alpine JS Livewire
Demo
Star Rating Component Example

New Bootstrap 5 Livewire
Simple Livewire toggle switch
This is a pro component. Get Livewiredemos pro access to view / download the component code.
Livewire Component Class
<?php
namespace App\Http\Livewire\Pro;
use Livewire\Component;
use Illuminate\Database\Eloquent\Model;
class StarRatingComponent extends Component
{
public $rating = 0, $previousRating = 0;
public Model $model;
public function mount()
{
if (auth()->check()) {
$this->rating = $this->ratings_query->first()->rating ?? 0;
$this->previousRating = $this->rating;
}
}
public function render()
{
return view('livewire.pro.star-rating-component', [
'total_ratings' => $this->total_ratings,
'average_rating' => $this->average_rating,
]);
}
public function updatedRating()
{
if ($this->rating == 0) {
$this->ratings_query->where('rating', $this->previousRating)->delete();
$this->reset([
'rating',
]);
return;
}
$this->model->ratings()->updateOrCreate(
['user_id' => auth()->id()],
[
'rating' => $this->rating,
]
);
$this->previousRating = $this->rating;
}
public function getAverageRatingProperty()
{
return number_format($this->model->ratings()->avg('rating'), 1);
}
public function getTotalRatingsProperty()
{
$this->model->refresh();
return $this->model->ratings->count();
}
public function getRatingsQueryProperty()
{
return auth()->user()->rating()->where('rateable_type', get_class($this->model))->where('rateable_id', $this->model->id);
}
}
Component View File
<div class="flex w-full justify-center items-center">
<div x-data="{
rating: @entangle('rating'),
hoverRating: 0,
ratings: [{ 'amount': 1 }, { 'amount': 2 }, { 'amount': 3 }, { 'amount': 4 }, { 'amount': 5 }],
rate(amount) {
if (this.rating == amount) {
this.rating = 0;
} else this.rating = amount;
},
}"
x-init="hoverRating = rating"
class="flex flex-col items-center justify-center space-y-2 rounded w-full bg-gray-200 mx-auto">
<div class="my-2">
Average rating: <b>{{ $average_rating }} / 5 ({{ $total_ratings }} votes)</b>
</div>
<div class="flex space-x-0 bg-gray-100">
<template x-for="(star, index) in ratings" :key="index">
@auth
<button @click="rate(star.amount)" @mouseover="hoverRating = star.amount"
@mouseleave="hoverRating = rating" aria-hidden="true"
class="rounded-sm fill-current focus:outline-none focus:shadow-outline p-1 w-12 m-0 cursor-pointer"
:class="{'text-gray-600': hoverRating >= star.amount, 'text-yellow-400': rating >= star.amount && hoverRating >= star.amount}">
<svg class="w-15 transition duration-150" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
</button>
@else
<button @click="Livewire.emitTo('internal.login-register-component','showModal')"
@mouseover="hoverRating = star.amount" @mouseleave="hoverRating = rating" aria-hidden="true"
class="rounded-sm text-gray-400 fill-current focus:outline-none focus:shadow-outline p-1 w-12 m-0 cursor-pointer"
:class="{'text-gray-600': hoverRating >= star.amount, 'text-yellow-400': rating >= star.amount && hoverRating >= star.amount}">
<svg class="w-15 transition duration-150" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
</button>
@endauth
</template>
</div>
</div>
</div>
Documentation
For storing the ratings-related data, we'll be using a Rating model. One more thing to keep in mind is the polymorphic relationship, we'll be using this relationship to store ratings of multiple models in this Model itself.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Rating extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'rateable_id',
'rateable_type',
'rating',
];
public function rateable()
{
return $this->morphTo();
}
}
The migration file of Rating Model
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateRatingsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('ratings', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users');
$table->string('rateable_id');
$table->string('rateable_type');
$table->smallInteger('rating');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('ratings');
}
}
Additionally, since we are using a polymorphic relationship, we need to define the following relationship to the Modal to which we want to attach the rating component. In our case, the model is shown below:
// WinkPost Model
public function ratings()
{
return $this->morphMany(Rating::class, 'rateable');
}
Usage
And finally, a demo showcasing the usage of the Rating Component.
@livewire('pro.star-rating-component', [
// the $post model here is a WinkPost model
'model' => $post
])