At Cruise, we had a specific requirement for a star rating, to use on our newly designed reviews page, where a cruiser could use to rate their cruise experience. Before I was shown the design for this, I thought it was just the standard rating of 5 stars.

The actual look of our rating component

I had 5 stars. When the third is clicked, all three should be highlighted and same if one or more being selected or clicked. We then introduced buttons on either sides. Left button should decrease while the other increases. Since I've never done this before, I Googled to see what was out there but my search had proven futile; there was nothing similar to what we wanted. It should be simple, right? Actually, it is.

This blog post is a conversion from my first Medium post.

<div id="rating">
  <!-- Decrease button -->
  <button v-on:click="decreaseRating()">-</button>
  
  <!-- Stars -->
  <svg
      v-for="num in (rating).times()"
      :key="num"
      :class="{active: ratingSetValue > num /* num starts at 0 😉 */}"
      version="1.1"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 19.481 19.481"
      xmlns:xlink="http://www.w3.org/1999/xlink"
      enable-background="new 0 0 19.481 19.481"
      v-on:click="starClick(num)"
    >
      <g>
        <path d="m10.201,.758l2.478,5.865 6.344,.545c0.44,0.038 0.619,0.587 0.285,0.876l-4.812,4.169 1.442,6.202c0.1,0.431-0.367,0.77-0.745,0.541l-5.452-3.288-5.452,3.288c-0.379,0.228-0.845-0.111-0.745-0.541l1.442-6.202-4.813-4.17c-0.334-0.289-0.156-0.838 0.285-0.876l6.344-.545 2.478-5.864c0.172-0.408 0.749-0.408 0.921,0z" />
      </g>
  </svg>
  
  <!-- Increase button -->
  <button v-on:click="increaseRating()">-</button>
  
</div>

I'm a Ruby person and you'll see a technique used to achieve a nice little loop. This was the layout that I wanted without having extra stars; one star should be enough where I make use of v-for. If using Ruby (not Ruby on Rails), I'd do something like:

# Button here
5.times do
  # The svg element
end
# Button here

Number.prototype

FGRibreau has a brilliant solution for number.times() that I had to use but with a slight modification:

var rating = new Vue({
  el: '#rating',
  data: {},
  created () {
    Number.prototype.times = function() {
      let i = -1
      const arr = []

      while (++i < this) {
        arr.push(i)
      }

      return arr
    }
  }
})

Now we are able to iterate a set number of times. In our case, 5 with (5).times()

Properties

Our demo component will make use of two properties

  • rating - The default number of stars to display
  • ratingValue - A default number of stars to highlight
var rating = new Vue({
  el: '#rating',
  props: {
    rating: { type: Number, default: 5 },
    ratingValue: { type: Number, default: 0 }
  },
  data: {
    ratingSetValue: null // <--- we'll need this for later
  },
  created () {
    Number.prototype.times = function() {
      let i = -1
      const arr = []

      while (++i < this) {
        arr.push(i)
      }

      return arr
    }
  }
})

Methods

Methods are some sort of action to perform when a user clicks either a button or star. Our star method will be slightly different from our button clicks.

  • starClick(num) - ratingSetValue will be equal to (num + 1) as num will be index based (starts from zero)
  • increaseRating() - ratingSetValue will be +1
  • decreaseRating() - ratingSetValue will be -1
var rating = new Vue({
  el: '#rating',
  props: {
    rating: { type: Number, default: 5 },
    ratingValue: { type: Number, default: 0 }
  },
  data: {},
  methods () {
    // num.times() will be index base
    // which means it starts from zero
    starClick(num) {
      this.ratingSetValue = num + 1
      // Dispatch to store here
    },
    increaseRating() {
      if (this.ratingSetValue < 5) {
        this.ratingSetValue += 1
        // Dispatch to store here
      }
    },
    decreaseRating() {
      // You should not be able to vote zero once voted
      if (this.ratingSetValue > 1) {
        this.ratingSetValue -= 1
        // Dispatch to store here
      }
    }
  },
  created () {
    Number.prototype.times = function() {
      let i = -1
      const arr = []

      while (++i < this) {
        arr.push(i)
      }

      return arr
    }
  }
})

To finish off, let's add that css:

<styles scoped lang='scss'>
  svg.active {
    fill: orange;
  }
</styles>

Now we have a fully functional star rating component with buttons to increase or decrease.