Implementing a Star Rating component in Vanilla JS
Tags: javascript, webdev, uiweekly
Published: 2021-07-02 13:24:17

**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 :-

```js
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 :-

```html
<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 :-

```js
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 !!!** 

![skeleton alright](https://i1.wp.com/the-avocado.org/wp-content/uploads/2018/08/skeleton-gif.gif?fit=474%2C346&ssl=1)

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()` :-

```js
  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 !!

![tony stark swag](https://mavitech.com/wp-content/uploads/2016/07/get-a-delegation-jab-final-11.gif)

Now let's see how `renderChanges(rating)` will look like :-

```js
  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.

![wow](https://thumbs.gfycat.com/SadSorrowfulJuliabutterfly-size_restricted.gif)

Now comes the part where our **event handlers** come into picture, the first one being, `onMouseClick` :- 

```js
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 :-


```js
  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 !! 

![simple](https://media.tenor.com/images/040dbab16b864a0e250d71ae20c35131/tenor.gif)

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](https://web.dev/control-focus-with-tabindex/).

{% codepen https://codepen.io/lapstjup/pen/gOWYRpe %}

**I am open to any feedback you have regarding the writeup or implementation. That's how I learn :)**