This is part two of a multi-part series about React and Vue.
Part 1: React
Part 2: Vue
As I have been learning React lately, I’ve also been learning Vue. I currently use Vue at work, but I wanted to make a sample project for myself. I’ve been interested in doing a comparison between React and Vue, so I’ve chosen to build a Vue version of the Star-Rating app I built in React. The app is small and simple: you have a rating represented as a line of stars, and you can change how the star rating is calculated via an inputs component.
For the final result, click this link.
This tutorial presumes a basic knowledge of HTML, CSS, and JS, along with some basic knowledge of how to develop using a command line interface (or CLI) and how to use Node Package Manager (or NPM). If you’re unfamiliar with any of these things, feel free to make an online search for whatever you don’t know.
Vue makes some use of ES6 syntax, and I try to write my code using ES6 as well. I’ll try to link to definitions of ES6 syntax when I initially use them, but I won’t take especial time to explain them in-depth. If you want a quick overview of ES6, you can read this.
Table of Contents
Setting Up
Looking Around
Creating Our First Component
Rendering Stars with FontAwesome
Calculating How Many Stars to Render
Rendering the Stars
Vue Slots
Finishing the First Component
Introducing a Second Component
Rendering the Input
Adding the Other Inputs
Better Validation
Final Touches
Conclusion
Setting Up #
For this tutorial, I chose to use vue-cli, a command-line tool that will set up a boilerplate Vue single-page application, similar to create-react-app
. If you don’t have it already, you can install it via npm install -g [email protected]
.
I’ve specified a specific version number because, as of this post, vue-cli V3 is in beta and will be released soon. It works differently from V2, so to follow this tutorial you’ll need a V2 version of the tool.
To set up a project, navigate in the terminal to the directory you want to make the project in (for me, this is ~/jdev/projects
), and then enter this command:
vue init webpack star-rating-vue
Right away, you’ll be presented with a text interface that asks you a number of questions regarding how you want to set up your project. But, before we get to that, let’s look at the command we just entered. The first part is vue
, calling the tool; the second part is init
, which tells the tool to start up the project; and the fourth part, star-rating-vue
, is the directory our app is going to be saved in. So what is the third part, webpack
, for? The way vue-cli
V2 works is by using pre-built templates, and which template you choose determines what kinds of goodies you start out with. In this case, we’re using the “webpack” template, which will set up a full Webpack-based project, as well as a few other things:
- vue-loader
- Allows us to write single-file components (we’ll get to that momentarily).
- hot-reload
- Allows us to see changes to our app on save.
- linting
- Helps enforce proper coding conventions by marking up your code editor.
- testing
- Sets up a unit testing framework. (We won’t be using tests in this tutorial.)
- css extraction
- Pulls our CSS out of single-file components (we’ll get to that momentarily, too).
If you look on the official page for vue-cli, you’ll find some other “official” templates you could use, instead. There are also third-party templates; just search online for “vue third-party templates” and you’ll be bound to find some.
Now that we’ve taken a brief overview of what templates are, let’s get back to the actual setup. The command line tool will present you with a set of options, one at a time. For most of the options, you’ll need to type in either y
or n
and hit enter to make your choice; some are multiple choice, and a few involve typing up strings.
I’ll go over each option briefly:
- Project name: “name-of-your-project” (requires URL-friendly characters, so no spaces; also, no capital letters allowed)
- Project description: “Whatever description you feel like including.”
- Author: “Your Name <[email protected]>” (if you have a global Git user, this should be autofilled).</[email protected]>
- Vue build: Here, you’ll have two options:
Runtime + Compiler
andRuntime-only
.Runtime + Compiler
allows more flexibility in how you build your projects, such as passing in templates as a string (from an AJAX call, for example), but it makes the final build larger (by as much as 30%).Runtime-only
is a lighter build, and performs slightly faster, but imposes some restrictions on how you can set up your project.
We shouldn’t need any functionality from the compiler for this project, so let’s select
Runtime-only
. - Install vue-router?
vue-router
is useful for setting up something resembling url navigation within an SPA. We won’t be doing that here, so you can choosen
. - Use ESLint to lint your code? If you care about following style guides, choose
y
, and then pick the linter you want to use in the subsequent section. Otherwise, choosen
. - Set up unit tests: As mentioned earlier, we won’t be setting up unit tests here, so choose
n
. - Setup e2e tests with Nightwatch? Nightwatch is a testing tool that allows you to render the app in full, then set up “scenarios” of user actions to test how your app acts under those circumstances. We will not be doing that here, so you can choose
n
. - Should we run `npm install` for you after the project has been created? Since we would need to do this anyway, go ahead and choose
Yes, use NPM
. If you prefer Yarn, you can choose that instead.
After that last choice, vue-cli
will get to work setting up your app with the choices you’ve made. It may take a while, depending on your internet speed, but in a minute or two your boilerplate app should be generated and ready for you to work with. When it’s finished, start the development server by cd-ing into the project directory and typing npm run dev
. Open a browser and navigate to localhost:8080
; if you see the Vue logo and some links, the dev server is successfully running!
If you don’t want to use vue-cli
, you can import Vue directly, via a CDN, locally-hosted script, or NPM install. Check out the official docs for more information. For this tutorial, however, I’ll be assuming you’re using vue-cli
.
Looking Around #
Now that we’ve (finally) got the barebones project set up, open the project in your preferred code editor.
If you use VS Code, you can spawn a new editor window from the project directory by cd-ing into it, then typing
code .
(you may need to explicitly enable this if you use a Mac). Other code editors likely support this functionality as well, though the commands you use may be different.
You should see a number of directories and files already set up. Let’s look in the src
directory and open up the file called main.js
. It should look something like this:
This file sets up the root Vue instance. All the other components in our application will be kept under this root instance. We’re importing Vue itself, as well as a file called App
(which we will be examining shortly). There’s a quick line disabling the production tip. Finally, we create a new Vue
instance, and pass in a config object with two settings:
el: '#app'
This tells Vue what element from the HTML document to bind the instance to. The original element will be replaced with our rendered application. (You can see this element by looking in the `index.html` file in the project root directory.)render: h => h(App)
This tells the instance’s render function what should be rendered, via passing in a callback function that handles the actual rendering. In this case, we’re rendering theApp
file imported at the top of the page.
Let’s dig into that second option a little bit. The code is using an ES6 arrow function, with a single argument: h
. What’s h
? It’s an alias for the createElement
function, the letter itself short for “hyperscript” (you can check this Github issue comment for a deeper explanation of both the render prop and the meaning of h
). h
is then called, and App
is passed in and rendered.
So what is App
doing? Let’s find out:
Note the .vue
extension. This is what Vue calls a Single File Component. A single file component allows you to write your HTML, JavaScript, and CSS for that component in the same file. For applications comprised of multiple, modular parts, this can be very useful for keeping markup, logic, and styles associated with a single component located in one place. We will be creating our own single file components (henceforth just “components”) soon enough, but first let’s dig into the default App
component, section by section.
First, the HTML:
The entire section is wrapped in a template tag. This gives Vue access to the markup contained within; the tag itself will not be rendered. Next, you have a single div
with an id of app
. This is not the same app
element that the root Vue instance replaced; it merely shares the same id. Finally, we have an image tag — the Vue logo — and a strange-looking, self-closing tag named HelloWorld
. This is another component, imported into App
and being rendered below the logo.
All elements in your component must be wrapped by a single container element; here, the
#app
div.
Next, in the javascript section:
This contains the JavaScript code for the component. First, you’ll see the HelloWorld
component being imported from the components
directory. Next, the code exports an object as default, containing two properties:
- name The name assigned to this component. While not strictly required (except in a few cases), it’s good practice to always give your components names.
- components This tells the component instance what custom components should be expected. This is how we can write
<HelloWorld/>
in our template and have Vue replace it with the template from theHelloWorld
component.
Finally, there’s the styles section:
This is just regular ol’ CSS, styling the #app
element. During development, these styles will be added to the <head>
, but because our Webpack template includes CSS extraction, the styles in this section will get pulled out and added to a single CSS file during the build stage.
That does it for App
. Now let’s look at the other component included in the boilerplate — the HelloWorld
component.
As you may have noticed, the attributes for the
a
tags in your local file are split onto separate lines. This is considered by Vue’s style guide to be best practice. I’ve chosen to condense the tags back onto one line for my gist for the sake of brevity, but in general I agree with this philosophy.
It’s another single file component. The HTML template contains a couple unordered lists of links and some h2
tags titling them, as well as an h1
with {{ msg }}
. If you’re looking at the app in the browser, however, the h1
says “Welcome to Your Vue.js App”. What’s going on?
Let’s look in the script section to find out:
There isn’t much here. We have the component’s name
, “HelloWorld”. There are no components being passed in; however, we do have a method called data
, which returns an object. This object contains one property: msg
, whose value is set to “Welcome to Your Vue.js App”. Hey, that’s what the h1
says on the rendered page! That’s where it comes from. Anything you set in a Vue component’s data
method becomes available in your template as a variable. You can output the values of those variables using a pair of curly braces.
Why is data
a method returning an object, instead of just being a property set as an object? If you have multiple instances of the same component on a page, each component’s properties are shared with each other by reference. Thus, if data were an object, updating the data on one component would update that piece of data for every instance of that component. To avoid this, we just make data
a method that returns a fresh object each time it is called.
The docs provide additional explanation, as well as an example.
Lastly, let’s take a look at the style section for this component:
Again, it’s just CSS, but this time there’s a scoped
attribute in the style tag. This means the CSS in this component will only be applied to styles within that component. How does it do this? If you go to the web page and do a dev inspection on one of the links, you’ll notice there’s a data
attribute attached with a long, random string (prefixed with data-v-
) as its value. Vue sets the CSS rules in HelloWorld
to target both the rule specified in the code and the value of this data attribute. Thus, the styles in this component will not affect anything outside of its scope.
You are still able to affect the element styles in this component from the outside, but be aware that, since the data attribute specification adds specificity, you may need to be more explicit with your rule declarations than usual to override the component’s styles.
Whew! For a boilerplate example, that was a lot of stuff to look over. Hopefully, this overview gave you a basic idea of how Vue works; the rest of what we need to know, we’ll pick up as we go along. Let’s start making our Star-Rating app by creating our first component!
Creating Our First Component #
In the “components” directory, create a new file and name it StarRating.vue
. Here’s the structure we’ll need to start with:
In the template section, we’ve just added a single div
with a class of “star-rating”, which will serve as the container element Vue requires. That’s all we’ll add, for the moment; we’ll get to adding stars shortly.
In the script section, we’re exporting a default object with two properties: name
, and props
. Props are pieces of data that a component expects to be passed down from a parent component. We’ll be setting up App
to do that, but for now we’ll just set up defaults to work with. We’ll also add a type
property to each prop, which allows Vue to detect when a prop gets passed down that doesn’t match the type specified, and warn you in the developer console.
The style section is currently empty. Eventually, we’ll put in the styles specific to the StarRating
component, but first let’s have something to actually render.
Rendering Stars With FontAwesome #
If you read the React version of this sample project, you’ll remember that we chose an icon set called FontAwesome, imported some star icons into our app via a series of NPM libraries, and wrote some logic to control how to render those icons. We’re going to do the same thing here, but instead of using react-fontawesome
, we’ll be using vue-fontawesome.
npm i --save npm i --save @fortawesome/fontawesome-svg-core @fortawesome/fontawesome-free-solid @fortawesome/fontawesome-free-regular @fortawesome/vue-fontawesome
To summarize, we’re installing Fontawesome, two sets of Fontawesome icons, and a package which provides FontAwesome Vue components. Once the packages have finished installing, we need to import them into our project.
First, in App.vue
, replace the HelloWorld
import with this:
The first statement is importing the main fontawesome
library. The next two import statements are using multiple import syntax to extract specific parts of the fontawesome-free-solid
and fontawesome-free-regular
icon libraries. For the latter, we are also assigning aliases to the imports, since they share names with the solid
counterparts. Finally, we use fontawesome.library.add()
to make these four icons globally available for all our other components to use.
We could also have just imported all of a library’s icons — with, say,
import solid from '@fortawesome/fontawesome-free-solid'
— and added all the icons globally. If you know you only need a small set of icons, however, it’s best to just import the ones you need.
Next, let’s go back to StarRating
and add one more import:
Similarly to how we imported parts of the icon libraries, we are importing parts of the @fortawesome/vue-fontawesome
library — specifically, FontAwesomeIcon
and FontAwesomeLayers
. Soon, we’ll be using these to render our star ratings. First, though, we need to tell our StarRating
component to make these FontAwesome components available to the template, and that’s what we’re doing by adding the components
property to our exported object, its members FontAwesomeIcon
and FontAwesomeLayers
.
With our FontAwesomeIcon component imported, let’s go ahead and update our template:
Inside the container div
, we’ve added a new tag: font-awesome-icon
. This is our FontAwesomeIcon
component, and when Vue renders our app it will take this component tag and replace it with the component’s template, just as the HelloWorld
component is being rendered into our default application. We’ve given it a class of “star”, and we’ve also set a property named icon
, its value set to “star” as well. This icon property is a prop we are passing down to the Vue component, telling it which icon we’d like it to render.
The official Vue style guide strongly recommends naming custom components using multiple words, as well as naming your components using either all Pascal-case (FirstLetterCapitalized) or kebab-case (dashes-between-words), and I agree with their reasons. For our tutorial, I’ve chosen to use kebab-case, but ultimately it’s just a matter of preference.
Let’s go ahead and render our new component into the App.vue
component file:
We’ve added an import for our StarRating component before the other imports (imports are asynchronous, so JavaScript will not execute any code until all imports have been processed, which means we don’t have to import StarRating
after our FontAwesome imports). We’ve also removed the import for HelloWorld
because we won’t be needing it. In components
, we replace HelloWorld
with StarRating
. Finally, in template
, we again swap HelloWorld
for star-rating
. If all goes well, upon save you should see a single black star rendered just below the Vue logo.
Calculating How Many Stars to Render #
If you played around with the final result at the beginning of the tutorial, you may have noticed three different types of stars: “full” stars, “half” stars, and “empty” stars. This is how I’ve chosen to represent the star ratings for this project.
Before we can render any of that, though, we need to calculate four things:
- What is the maximum number of stars we should render?
- How many full stars should we render?
- How many half stars should we render?
- How many empty stars should we render?
You may be wondering why it’s necessary to calculate a “maxStars” value. Sure, we could simply calculate the full, half, and empty star values without calculating a separate max value, but I’ve found that it makes the math simpler to just figure out the maximum number ahead of time and use it in calculations later, so that is what I’ve chosen to do.
Let’s update the StarRating component with the necessary code to perform our calculations:
To briefly explain each calculation:
- maxStars(): We take the maxRating and divide it by the starRatio, then round the answer up to the next integer.
- fullStars(): We take the rating and divide it by the starRatio, then round the answer down to the next integer.
- halfStars(): We take the modulus (or remainder) of rating and starRatio, get half the value of starRatio, and compare the two using a ternary operator. If `x` is greater than or equal to `i`, we return one half-star; otherwise, we return no half-stars.
- emptyStars(): We take maxStars and subtract from it fullStars and halfStars.
Note where we have placed these methods. These are what Vue calls “computed properties”. In other words, when you access these properties, whether in your template or other parts of your code, Vue will return the result of the function you’ve assigned to that computed property.
If you examine the code used for the emptyStars
method, you can see that we’re using the other three computed properties to calculate its return value. The other three computed properties, in turn, are calculated based on the value of the props passed in to the StarRating
component. If any of the props should change, Vue will recalculate the values of these computed properties, and any rendered data using these computed properties will also be updated; it all happens automatically.
Rendering the Stars #
Let’s see the computed properties in action. Modify the template thusly:
What we’ve done is add a directive to our font-awesome-icon
component called v-for. This directive is telling Vue to render a component this many times, just like looping through a list of items. The text inside of the v-for
directive is actually specific syntax that Vue will interpret similarly to a for-in loop. Vue lets you enumerate objects and arrays with this syntax, as well as ranges (which is what we’re using in this case).
Be sure to check out Vue’s official documentation on this subject, which shows you more specifically how to work with arrays and objects using
v-for
.
We’ve also added a key
attribute, and you might have noticed the colon in front of it. What is this colon for? This is a shortcut for the directive v-bind, which binds an HTML attribute to a JavaScript expression. The value of key
equals "`fs${fs}`"
, which is a template literal expression. To use ES5, "fs" + fs
(which would also work here; I just prefer using ES6 when possible). When Vue reads this, it will parse the expression bound to :key
as actual JavaScript.
You can read more about attributes here. If you follow the link, you’ll notice that the Vue examples use
v-bind:
instead of just the colon. That is the “full” way to write out binding syntax, but Vue allows you to use just the colon as a shortcut.
Why do we need key
? Vue strongly recommends that you add a unique key to each item rendered by a v-for
loop because it helps Vue track each item in its virtual DOM, which ultimately improves performance. The expression set in key
will generate a string with the incrementing value of the variable fs
(set in the value of v-for
), which is enough to make this key unique. Since, technically, it isn’t required, we could choose to leave the key out, but it’s good practice to always add a key
attribute when using v-for
.
So what does all this actually do? If you’ve saved this code, you might have already noticed that the demo app in the browser has already updated to show two black stars instead of one. This is because of our v-for
directive, fs in fullStars
. The default prop value for rating
is 5, and our default starRatio
is 2. Those two props are used by the fullStars
computed property to calculate its value, and in this case the result is 2. v-for
then loops n in 2
, resulting in rendering the font-awesome-icon
component twice. Again, this happens automatically. You don’t have to write a separate function to handle the looping and rendering for you. Vue just takes care of it because you told it to via the v-for
directive.
Now that we know how this works, let’s go ahead and add the template code to render our half stars and our empty stars:
The process for rendering our half stars and empty stars is nearly identical to the process for rendering full stars. We just add a FontAwesome component and use v-for
to render as many of each component as dictated by the related computed properties. There are a few differences, however, which I’ll go over briefly.
In both cases, we need to generate unique keys for the key
attribute, and to accomplish this we just change the variable to match what we use in the v-for
directive and alter the string prefix. For the empty stars, "`es${es}`"
, and for half stars, "`hs${hs}`"
.
For the empty stars icon component, instead of just passing in an icon
attribute, we’re binding the attribute to a JavaScript array: ['far', 'star']
. This is because of the font-awesome-icon
component’s interface. By default, passing in a simple icon
attribute will render an icon from the solid
library of icons, which is what we used to render our full stars. For empty stars, however, we want to render a different icon; specifically, one from the regular
library that looks like the empty outline of a star. In order to tell the font-awesome-icon
component to render an icon from the regular
library, FontAwesomeVue expects us to pass in an array with two members. The first item is the abbreviation of the library we want to pull the icon from, in this case far
; the second item is the name of the icon we want, in this case star
. Since we’re passing in an actual array, that means we need to bind icon
so we can send the configuration array to the component.
You could use
['fas', 'star']
to render our full star icons, instead of relying on the component’s default assumption, if you prefer.
The most significant difference is with how we’re rendering the half stars. Instead of a single font-awesome-icon
, we’re rendering two, and we’ve wrapped both components with a font-awesome-layers
component. This is because FontAwesome 5 (the version this tutorial is using) doesn’t have a “half-filled” star icon; instead, it has a literal half of a star: one that is “filled” (from solid
), and one that is “empty” (from regular
).
To get the half-filled star effect we’re looking for, we need to combine those two icons into one. The official way to do this in FontAwesome is to use Layering; in vue-fontawesome
, this means using the font-awesome-layers
component. Anything within the font-awesome-layers
tags will get combined into the same space and be shown as though it were one icon.
Thus, we’ve set the two font-awesome-icon
components to use the half-star icons; additionally, we pass in a flip
attribute to the empty half-star so it’s reversed. The end result is something that looks like a half-filled star. Excellent!
After I started writing this tutorial, FontAwesome did come out with a half-filled star icon, so we could use that instead of my original solution. I’m not going to change this solution, however, because it also serves to teach about Vue slots — which is coming right up!
Vue Slots #
Let’s take a moment to further examine font-awesome-layers
. Up until now, we’ve been using self-closing tags for our components, because it’s simpler to read and we had no need to use opening/closing tag syntax. With font-awesome-layers
, however, we’re not only using opening and closing tags, we’re including other components inside of the tags. This is using an aspect of Vue components called slots. To explain what slots do, let’s use a quick example:
The above code (assuming path names and whatnot) results in rendering the following:
In the first component, called ExampleComponent
, we basically have two parts: an h2
with the text “These are my children:”, and a ul
containing a slot
tag. What is this slot tag for? This is telling Vue that anything which is slotted into this component should go here. What does slotting mean, though? To answer that, let’s look at the second component.
The second component is called ExampleParent
. In it, we have an h1
with the text “I’m the Parent”. What comes next is our first component, example-component
, but in between the opening and closing tags of example-component
are three li
tags, each containing a human name. If you look at the results image, you’ll see that it renders “I’m the Parent” (from ExampleParent
), followed by “These are my children” (from ExampleComponent
), and lastly by the names (from ExampleParent
). Vue is taking the li
tags we enclosed within the example-component
tags and inserting them in place of the slot
tag we put in ExampleComponent
. That is slotting.
If you’d like to follow along, you can copy-paste the code snippets into your
components
directory as separate Vue files, and then importExampleParent
intoApp
, just like we did withStarRating
. Don’t forget to remove it when you’re done with it!
Now that we have a better idea of what slots are, let’s look at the font-awesome-layers
component again:
We are slotting two font-awesome-vue
icons inside of the font-awesome-layers
component. That layers component will then render FontAwesome’s layers container (basically a div
container and some classes), so we can make those two half-star pieces look like a half-filled star.
Finishing the First Component #
Alright, we’ve added the code necessary to render all of our stars: full, half, and empty. The last thing we’ll do (for now) is add some styling in the style tags at the bottom of the StarRating
component. The finished* code should look like this:
Following the steps outlined so far, you should now see the Vue logo with two and a half gold stars rendered beneath it. If you do, congratulations on making it this far! If not, you should review your code and compare it with what I’ve posted. Hopefully that should expose your problem and get you back on the right track. If not, feel free to submit an issue in the project repository!
Did you notice the asterisk on “finished”? There’s one more thing we’re going to take care of later, but we don’t need to worry about it right now.
Assuming things are working, let’s go ahead and pass a prop down to our component. From App.vue
:
When you save the file, you should see the rendered stars change. Assuming you passed in a “7”, like I did, there should be three and a half stars rendered — three full, one half, and one empty. Vue took the rating number we passed down to the StarRating
component and converted it to the star rating we now see. We didn’t have to do anything other than pass down that prop.
Of course, at the moment we can only change the rating by manually editing our code to change the value of the rating
prop. That obviously isn’t a very user-friendly way to change the star rating, so we’re going to want to have an interface for us to enter the prop values we want and have StarRating
update in response, as shown in the final result. To do that, we need to build a second component specifically for handling this interface…a RatingInputs
component!
Introducing a Second Component #
To begin, let’s create a new file in the components
directory, and call it RatingInputs.vue
. Add the following code:
First, in the template
, we have a single input
for the rating, as well as a label for that input, all wrapped in a container div
. Then, in the script
section, we have the component’s name and our prop definitions (like we had in the StarRating
component). Finally, we have the empty style tags at the bottom.
Next, we’ll take advantage of Vue’s form input bindings and wire up our input element to the component’s data:
We’ve added a directive to the input, v-model
, and set it equal to rating_
. In the script
section, we’ve added a data
function, which returns an object containing one property: rating_ = this.rating
. What this is doing is initializing rating_
with the value of rating
, a prop this component expects to be passed down (and if it isn’t, then it defaults to 5, as we set up in the props
section). Because we’ve told Vue to “model” the value of rating_
on our rating input, Vue sets the input element’s value equal to the value of rating_
; when we change the value in the input element, Vue then updates rating_
to equal the new value. Thus, Vue gives us two-way data binding without having to do extra work.
Why not just set the input’s value to be rating
directly? Technically, Vue would allow us to do that; however, this is considered bad practice. The reason is because anything defined as a prop will have its value changed any time a new value is passed down to the prop via the parent component. Thus, as best practice, if we want to modify a prop’s value from the child component, we should instead create a property in the data
object and initialize it with the prop’s value, then modify that data property instead of the prop.
There is no established nomenclature for how you should name your props and data values in cases like these. I’ve chosen to use
rating
as the prop name for easier reading when passing it in from the parent, andrating_
because Vue doesn’t allow you to start data fields with underscores, and this seemed the next best thing.
Rendering the Input #
Although our RatingInputs
component is wired up with the rating
input, we still don’t have a way to tell the StarRating
component what that input’s value is. To do that, we’re going to need to use Vue’s events system. We’ll start by updating our RatingInputs
component:
We’ve added another new directive to the input element: v-on
. This tells Vue to listen for an event (it can be a browser or custom event) emitted by this element. :input
tells Vue which event to listen for, and "handleRating"
is the callback we want Vue to run on this event. In the script
section, we’ve added a new property to the component object, called methods
. This is where you store any functions you want your component to have access to.
You should use regular functions here instead of arrow functions. Vue automatically handles binding a component’s
this
to its methods, but it can’t do so for arrow functions because those use thethis
value of the calling function — which, in this case, won’t be theRatingInputs
component, but Vue’s handler for methods.
In methods
, we’ve added one function: handleRating
, which does two things: first, it gets the value for rating_
from the component via destructuring assignment; second, we call this.$emit
, which is a special component function to emit a custom event that only a direct parent component will be able to read. This $emit
function takes, as its arguments, a string “rating-update” and an object literal set to {rating: rating_}
. rating-update
is the name of our custom event, and the object literal is an argument that will be passed in to any callbacks the parent App
component has listening for the rating-update
event.
Now, in App
, let’s make some changes to listen for our custom event:
In the template
section, we’ve removed the hard-corded value for :rating
and changed it to be the value of the rating
data field, which we add in the script
section. We’ve also added the rating
prop to our RatingInputs
component, as well as @rating-update
, the custom event that we’re listening for, and a handleRatingUpdate
callback.
@
is Vue shorthand syntax forv-on:
We’ve added a methods
object to the App
component object, as well as the function handleRatingUpdate
. It takes one argument, data
, which is equal to the object literal we passed into the $emit
function in the RatingInputs
component. From data
, we extract the rating
property, and then set this.rating
(the data field in App
) to equal rating
. Upon saving the file, we should now be able to edit the value of rating
input element and update the rendered stars in real-time. If you’re able to do so, congratulations!
Adding the Other Inputs #
While playing with the ratings input, you’ll likely notice that you can add a higher input than 10, and a lower input than 0. While this doesn’t seem to affect anything visually, this is not ideal behavior. We want to limit the input so that the user can’t add a higher rating than the maximum, or a lower rating than the minimum. Also, it would be nice to be able to change the star ratio, or how many stars get rendered per rating point (e.g. render one star per rating point, rather than one-half). Let’s get on that.
Let’s start by adding additional inputs for the values outlined above: maxRating
, minRating
, and starRatio
. The code for both RatingInputs
and App
is as follows:
As you can see, the majority of we needed to do is replicate the code we added to wire up rating
, changing only the names. We use the same handleRating
callback to process any change to the inputs and pass them up to the parent, and likewise handleRatingUpdate
updates all the data fields at once. We then pass back the data in App
as props to both StarRating
and RatingInputs
, and each component updates its own data accordingly.
There are a couple of additional changes. First, all of the input elements have min
and max
attributes set, which will show an error highlight should any of the values go below the minimum or above the maximum. There’s also a limit
property, which is used to constrain minRating
and maxRating
; this is mainly to control how many stars can actually be rendered, as rendering more than a thousand begins to negatively impact performance.
Better Validation #
We still haven’t solved the problem of preventing someone from inputing illegal values into our data. There are a variety of conditions we’ll need to check, and we’re going to need to check them for both StarRating
and RatingInputs
. It seems like it would be a good idea to write a separate file that contains all this validation code, which we can import into whatever files need to use it.
Conveniently, I’ve already written such a library for the React version of this project. Since we’re validating the exact same set of data, we can just take that file as-is and plop it straight into our Vue project.
In your src
directory, create a new directory and name it lib
, then create a file called validate.js
and copy-paste the following code into it:
The function we’re most interested in is the default export, which is a function that runs all the other validation functions in the file, returning false if any one of the checks fail, and only returning true if all the checks pass. All we have to do is pass in our data values.
We have the validation library, but now we need to integrate it with our app. Let’s start with the RatingInputs
component.
The only place where we needed to make changes was the script file. First, we import the library. Then, in the methods
section of the component, we’ve added a new method: anyAreEmpty
. All this does is check any arguments passed into it and return true if any of them are equal to an empty string. In our handleRating
method, we then use anyAreEmpty
to check each of our input values to make sure they aren’t empty strings. Why did we do this? A more detailed explanation can be found in this section of the React tutorial, but the gist is that without doing this, there are scenarios where Vue (and React) won’t let you update the inputs at all.
Finally, we’ve wrapped our call to $emit
inside of our validation function, inputIsValid
, which takes all of our data values as an argument. We’ve also moved our number conversion to occur before we run the validation function; otherwise, we’d be passing strings into the validation functions, which are expecting numeric values. Only when the validation function returns true
do we emit the rating-update
event.
With RatingInputs
taken care of, let’s turn to StarRating
and make a small change there (this is what the asterisk from earlier was for).
Here, we’ve added a new property to the component, called beforeMount
. This is one of Vue’s lifecycle hooks, which are basically functions Vue calls at specific stages of a component’s life. In this case, beforeMount
will be called once, right before the component is rendered for the first time. We’re checking to see if our default props, being passed in from App
, are valid, and if they aren’t we throw an error to let the developer know that they need to check the values being passed as props.
You might be wondering: Why bother with this validation at all? After all, this validation check isn’t what’s showing the errors to the user; the browser is handling that. All this is doing is preventing the rating-update
event from being emitted when an illegal value is entered. But that’s the point: without this validation, Vue would emit the event, try to process the illegal values, and then error out in the process. We’d be wasting processing effort doing something we already know is not going to be valid. While that’s not as significant an issue for a small demo app such as this, there are plenty of cases where this would be a costly error. For example, say our update event triggered an AJAX request; we wouldn’t want to send an HTTP request whenever an illegal value is entered. By making sure we only update when our input values are legal, we ensure our code isn’t wasting resources and processing power.
While I’m not going to do this here, we could also use our validation library to trigger more specific error messages in response to the illegal inputs. Feel free to add this yourself, if you’d like!
Final Touches #
With this, all that’s really left to do for RatingInputs
is add some styling. If you want to fully match the way my version of the app looks, add the following style section to RatingInputs
:
We should also change the styling on App
so that the visual structure looks better (up until now we’ve been using the default styles that were generated by Vue when we created the app). I also made slight changes to the App
template. To match my version of the app, add the following style
section to App
and modify the App
template to match mine:
Of course, if you want to style things differently, feel free to do so!
Conclusion #
Hopefully this was a fun app to build. I liked making a demo app that wasn’t a todo list. Vue felt intuitive to me, as a web developer, and I loved being able to use template files to keep everything relating to a single component in one file. In exchange, you do have to structure your app in a more specific way, but since I mostly agree with how Vue wants you to do things, I don’t consider this a bad thing.
If you want to view the entirety of my code, look up the project on my Github repo.
Questions? Feedback? Leave a comment!