This post walks through the process of adding a rating system to a Turbo powered single page application. The application in question was built up in a previous post. If you want to follow along with the post, you can grab the repo at the starting point or just read the pull request.
What We’re Building
In this walkthrough, we’re going to be adding a basic star bar rating system to our movies single page application. You might be surprised to see that we can get a basic working version of this with no additional javascript whatsoever!
Add Ratings
The first step is to add ratings to our movies. Run the following in your
terminal to create a migration file that will add a new column to our
movies
table.
Open the file and edit it to add a not-null constraint and a default value
of 0
.
We can also add a validation to our Movie
model to ensure the rating
stays within a sane bound that we can display on our star bar.
Now we can run db:migrate
to add the column. If you open
up a console with bin/rails console
now you’ll be able to update the
rating on a movie e.g. Movie.first.update(rating: 5)
. Our validation
will prevent you from setting the rating to something we can’t display
like -1
or 6
.
Now we can update the view for our movies to display the rating.
It’s pretty basic though, we can improve on that.
Add the Star Bar
The next step in creating our star bar is the stars! We’re going to use icons
from the wonderful heroicons which is a suite of
beautiful SVG icons from the makers of TailwindCSS.
There’s an icon called star
that we’ll use, copy the
SVG contents of the icon and paste it into a file called
app/assets/images/star.svg
.
The best way to style an SVG icon (which is one of the big
advantages of the format, in my opinion) is to embed the SVG in the HTML
document (render it inline with the rest of the HTML) and from there apply
CSS styles as you might expect. There’s a gem called
inline_svg
that we’ll make use of to make that easier.
Add to your Gemfile
:
Run in your terminal:
Now we can use the inline_svg_tag
in our views.
To render our star bar we need to swap out the text rating we just added and replace it with this.
We going to render stars using the inline_svg_tag
a total of
Movie::MAX_RATING
number of times. This is a constant we’ve introduced for the
maximum rating i.e. the maximum number of stars. To avoid duplication, we
should reuse it in the validation.
We’ve also extracted a new helper method star_rating_class
so we can keep the
template readable by extracting the logic for which CSS classes the star should
have, based on what rating the movie has. If the movie has a rating of 4
then
stars with index 0
through 3
should be solid yellow and the last star, with
index 4
, should be a grey outline.
Our star bar looks a lot nicer, but it’s still not really doing anything. Let’s change that next.
Submit a Rating
We want users to be able to click a star and apply that as a rating. We will
need to add the update
action to the routes for movies
and implement that
action in the MoviesController
.
The update
action is fairly simple CRUD, but we only support updating
the rating
so there’s no need for mass assignment. We’re also rendering
the show
template at the end where we would normally redirect back to the
show
action. Redirecting would work here, but we’re going to make use of
not redirecting later when we add feedback.
In the template, we just need to turn the stars into buttons that will submit
a form to the update endpoint PATCH movies/:id
. We’ll use the
button_to
template helper to do this. We’ll set the method
to
:patch
, the default is :post
. We also need to add the index of the star
(plus 1
to account for the 0
-based index) as a parameter in the form. In
the HTML, this will be a hidden field with the name movie[rating]
.
If you try clicking stars now, you’ll notice that it works perfectly, without
any page refreshes. How? Turbo is taking over the form submission so that it is
asynchronous, and when the response comes back and includes the :details
turbo-frame, it swaps out the contents for the contents of the frame in the
response.
In previous versions of Turbo, you could not return HTML from a form submission unless the status code was an error, successful submissions had to redirect. Fortunately for us, this is no longer a restriction.
User Feedback
The star bar is now working as intended, but it happens so seamlessly, it’s not obvious that the user’s rating has been persisted. We can improve this with a little user feedback. We can add a small ‘toast’ next to the star bar after the user rates a movie.
Let’s start by adding and styling our toast. It’s just some plain HTML next to
the star bar, coupled with a CSS animation to make it disappear after a
short time. The icons used are also from heroicons. I’ve used the
check-circle
and exclamation
icons.
By setting visibility: hidden
we remove the toast from the accessibility
tree and focus events, though it still affects layout. We use this over
display: none
since it can be used in an animation, we would need to
use javascript to apply a change to the display
attribute and it doesn’t
matter to us here that it still affects layout - you might argue that it’s
better since nothing will move when the toast completes.
The @toast
variable comes from the update
action. This is why we chose to
render the :show
template, rather than redirecting. Though we could achieve
the same effect with a redirect by making use of the flash
object.
We simply set the @toast
variable to whether or not the update happened
successfully. Since the show
action does not define the @toast
variable,
the message only shows in response to a call to the update
action, i.e.
clicking on a star.
Filling the Star Bar
If you’ve ever used a star bar in the past (I’m guessing you have) you’ll have noticed that they tend to fill up from 0 to the point you hover over. This is an effect we can reproduce with a small amount of stimulus.
To start with, create a new stimulus controller by running this in your terminal. This will make sure your stimulus manifest is up to date (not applicable if you’re using webpacker), as well as add the boilerplate for the controller and create a correctly named file.
The basic expectation of the star bar is that when you hover over a star, that star and all the stars within an index lower than that star will show their hover state. When not hovering over any star, the star bar should simply display the rating.
The StarBarController
looks like this.
The two actions enter
and leave
simply toggle the hover class on the
appropriate stars, depending on which star the user is hovering over or none
if the user is not hovering over any stars.
The controller has one target called star
. Since we expect a list of these
(the 5 star icons) we’re using this.starTargets
in the plural to access them.
The controller also has a class
called hover
- we use this so we can define
the hover class for the stars in the markup rather than hardcode them in the
controller.
Now we just need to add to markup to connect our star bar to the controller. Our star bar now looks like this.
The definition for the hover
classes, data-star-bar-hover-class
has to be
on the same HTML element that attaches the controller. We also mark each of
the stars as part of the star
target with data-star-bar-target="star"
, we
don’t have to treat it differently just because it is going to be used as an
array within the controller.
When the user hovers over a star we need to toggle the hover class on for each
of the stars up to the star the user hovered over. We can pass parameters to
our stimulus controller actions. In our case, we need to pass the index of the
star the user hovered over. This is what the
data-star-bar-star-index-param="n"
is doing. We can then access the parameter
in the action with event.params.starIndex
. The generic pattern for this is
data-controller-name-param-name="value"
, the parameter can then be accessed
from the action with event.params.paramName
- the parameters name is
converted to snakeCase, just like the controller names are.
This star bar is very limited, I only really did it for some fun. For a more complete implementation of a star rating bar, check out a component library like Shoelace.
Conclusion
We’ve managed to create a fully-featured, interactive star rating bar and most of the changes were in our HTML. We didn’t have to write any javascript if we didn’t want to.