21
Implementing a Star Rating component in Vanilla JS
Star Rating/Review UI is a common sighting across different sites on the Internet.
Today, we will implement a simple star rating component using Vanilla JS.
We are going to use Revealing-module-pattern here and our main module API would look like so :-
const ratingModule = makeStarRating();
ratingModule
will expose two methods in the name of getStarComponent
and getRating
.
But before we go into the technical nitty-gritties of JS here, let's try to visualize how the HTML will look for this :-
<ul class="stcomp">
<li data-rating="1" class="star" ></li>
<li data-rating="2" class="star" ></li>
<li data-rating="3" class="star" ></li>
<li data-rating="4" class="star" ></li>
<li data-rating="5" class="star" ></li>
</ul>
We will not actually use HTML to make this but the DOM APIs. Still it's good to pre-visualize how we are going to identify the rating value from each element and that is via the data-rating
custom attribute which will be available to us as dataset.rating
when using DOM APIs. Also CSS isn't the focus of this article. Though, it will available in the final codepen implementation.
So let's start by making a basic skeleton in JS for now :-
const makeStarRating = function (noOfStars = 5) {
let rating = 0;
let starComponent;
function changeRating(newRating) {
rating = newRating;
}
function getStarComponent() {
if (!starComponent) {
// create Star Component
}
return starComponent;
}
function renderChanges(rating) {
// render UI changes as per rating passed
}
function getRating() {
return rating;
}
function onMouseClick(){
// click event handler
}
function onMouseOver(){
// mouseover event handler
}
function onMouseLeave(){
// mouseleave event handler
}
return { getRating, getStarComponent };
};
That's a skeleton alright !!!
So from the above you can see that we also have provided noOfStars
(with default value of 5) as argument to makeStarRating
which will be used by renderChanges(rating)
later on.
So we have to first create a star component and return it if it's not already present. Here is how we can do it by implementing getStarComponent()
:-
function getStarComponent() {
if (!starComponent) {
starComponent = document.createElement("ul");
starComponent.className = "stcomp";
for (let i = 0; i < noOfStars; i++) {
const li = document.createElement("li");
li.setAttribute("data-rating", i + 1);
li.className = "star";
starComponent.append(li);
}
starComponent.addEventListener("mouseover", onMouseOver);
starComponent.addEventListener("mouseleave", onMouseLeave);
starComponent.addEventListener("click", onMouseClick);
}
return starComponent;
}
Here we are basically creating an ul
element and appending to it li
, noOfStars
times. And setting the data-rating
attribute and className
property of each li
element. Finally adding the relevant code for registering event handlers. One important thing to notice is that we are making use of event delegation so that only our parent ul
has one event handler (for each relevant event) which can take care of events bubbling from child li
elements. The event bubbling is only beneficial for click and mouseover events. For mouseleave event we don't need it since we only want the rating
to get reflected once we leave the parent ul
container. And fun fact, mouseleave
doesn't bubble !!
Now let's see how renderChanges(rating)
will look like :-
function renderChanges(rating) {
for (let index = 0; index < rating; index++) {
starComponent.children[index].classList.add("star-filled");
}
for (let index = rating; index < noOfStars; index++) {
starComponent.children[index].classList.remove("star-filled");
}
}
The above is actually going to reflect our UI changes for the stars. We will have a class by the name of star-filled
to highlight a star.
Up to the rating
number, all the stars would be highlighted and after that all the stars will remain non-highlighted.
Now comes the part where our event handlers come into picture, the first one being, onMouseClick
:-
function onMouseClick(e) {
let star = e.target;
let isStar = star.classList.contains("star");
if (isStar) {
let { rating } = star.dataset;
rating = rating === getRating() ? 0 : rating;
changeRating(rating);
renderChanges(rating);
}
}
Above we first check whether the target which is clicked is a star or not. If it is, we get the rating
from the dataset
property. Now we compare it with existing rating
(via getRating()
) and if both are equal, reset the rating
to 0. Then we save this rating
and render the changes.
We also want a hoverable star highlight feature for our component. We can achieve that via the combination of mouseover
and mouseleave
like so :-
function onMouseOver(e) {
let isStar = e.target.classList.contains("star");
if (isStar) {
const { rating } = e.target.dataset;
renderChanges(rating);
}
}
function onMouseLeave(e) {
renderChanges(rating);
}
Here inside onMouseOver
, we just skip the check for rating
and saving rating
bit which we are earlier doing using changeRating(rating)
inside onMouseClick
. We only want to reflect these changes in the UI but not persist unless click action is performed.
And on mouseleave
, just render the changes with the current saved rating
(Bless you closures!!!).
And that's it for a simple implementation !!
We can use makeStarRating
each time to give us new modules and each of such modules can call their getStarComponent
to return the parent ul
which can be appended to other containers.
Below is a working implementation of the same with keyboard focusing capabilities as well. I didn't cover it since that could be an overkill for a simple implementation but can surely be looked into. Roving tabindex is the technique which I have used which you can learn from here.
I am open to any feedback you have regarding the writeup or implementation. That's how I learn :)
21