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
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
Open the file and edit it to add a not-null constraint and a default value
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
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
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
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
stars with index
3 should be solid yellow and the last star, with
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
update action is fairly simple CRUD, but we only support updating
rating so there’s no need for mass assignment. We’re also rendering
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
:patch, the default is
:post. We also need to add the index of the star
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
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
turbo-frame, it swaps out the contents for the contents of the frame in the
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.
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
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
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.
@toast variable comes from the
update action. This is why we chose to
:show template, rather than redirecting. Though we could achieve
the same effect with a redirect by making use of the
We simply set the
@toast variable to whether or not the update happened
successfully. Since the
show action does not define the
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.
StarBarController looks like this.
The two actions
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
hover - we use this so we can define
the hover class for the stars in the markup rather than hardcode them in the
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
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
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.