One place for hosting & domains

      Single

      How To Generate a Vue.js Single Page App With the Vue CLI


      The author selected Open Sourcing Mental Illness to receive a donation as part of the Write for DOnations program.

      Introduction

      Vue.js is a popular JavaScript framework for creating user interfaces. Created in 2014 by Evan You (formally of Google), Vue.js is often described as a combination of React and Angular, borrowing the prop-driven development of React and the templating power of Angular. This makes Vue an accessible framework for beginners to pick up, especially since it focuses on traditional HTML and CSS, rather than being a CSS-in-JS framework like React or relying on TypeScript (a superset of JavaScript) like Angular does.

      When starting a new project, it’s best to familiarize yourself with the technology’s tools and features. One important tool for Vue.js development is its command line interface (CLI) known as Vue CLI 3. The Vue CLI offers a number of useful features that enhance the Vue development experience, but the main feature is its ability to generate and pre-configure a new single-page application with the vue create command.

      By the end of this tutorial, you will have a working Vue.js application running on a local Node server. This local server uses hot module reloading via Webpack to provide immediate feedback, rendered in-browser as you work. Along the way, you will create .vue single-file components (SFC), such as a header and a footer. All of this you can save as a solid foundation for any future Vue projects.

      Prerequisites

      To follow this tutorial, you will need the following:

      Step 1 — Downloading Vue CLI 3

      To download Vue CLI 3, you will need to run a command either via npm or Yarn, whichever you prefer. npm or Node Package Manager is a way to download and manage other people’s code to use in your project as a dependency. Yarn, on the other hand, executes NPM commands under the hood but provides additional features like caching. It’s up to personal preference regarding which one to use. However, it is important to note that it’s not recommended to mix commands. It’s best to be consistent with one or the other for the duration of your project.

      Moving forward, this tutorial will use npm commands. The following command will download the necessary Vue CLI files from the registrar, which in this case is the npm (Node Package Manager) service:

      npm i -g @vue/cli
      

      Note: On some systems, installing an npm package globally can result in a permission error, which will interrupt the installation. Since it is a security best practice to avoid using sudo with npm install, you can instead resolve this by changing npm’s default directory. If you encounter an EACCES error, follow the instructions at the official npm documentation.

      You install this globally in order to use the CLI anywhere on your machine. If you don’t install this globally, it will only work in the directory that you installed it at. In the case of the command options, i means “install” and -g is a flag to install the code globally on your computer.

      To verify if Vue CLI 3 was properly installed, run the following:

      vue --version
      

      You will receive the following output with a version number. Your version number may differ, but if you receive a response with a version number, you’ve properly installed Vue CLI 3:

      Output

      @vue/cli 4.5.6

      To update Vue CLI 3, run the previous commands in this section, and the latest version will be installed.

      At this point, you have successfully downloaded npm globally along with the Vue CLI tool that you will use in the next section to create a generated Vue.js project.

      Step 2 — Generating a Single-Page Application

      As you develop Vue.js applications, you may find that manually configuring a project is not the most productive use of your time, since configuring a new Vue.js project from scratch can take hours. This is the true power of Vue CLI: It provides you with a pre-generated template that is based on your specifications. Because of this, it’s already configured so you can start developing your website or application right away. Vue CLI 3 will ask you a few questions via a command line prompt about your project, download the required files, and pre-configure it for you so you are ready to work as soon as it’s done.

      To generate a single-page application, navigate to the directory you’d like your Vue project in, then run the following:

      vue create vue-starter-project
      

      The highlighted section of the command is the name of the root directory of the project. This will be the name of the folder that contains all of your Vue.js project files. This can be whatever you’d like, but in the case of this tutorial, you will use vue-starter-project.

      Once you type out that command, continue by pressing Enter. You will then receive the following prompt:

      Vue CLI v4.5.6
      ? Please pick a preset: 
        Default ([Vue 2] babel, eslint) 
        Default (Vue 3 Preview) ([Vue 3] babel, eslint) 
      ❯ Manually select features 
      

      If you do not want to configure your project and opt for the defaults, you can do so with either Vue 2 or Vue 3. For the purpose of this tutorial though, it’s recommended to manually select your features. By selecting specific features, you will see how each option you selected was installed by the CLI.

      Select Manually select features with ENTER. Immediately you’ll receive a number of different options, including: Choose Vue version, TypeScript, Router, and Vuex. Notice that some of these items are already selected (the bubble is filled in). You may select as many or as few as you’d like. However, for this tutorial, select the following by pressing <space> on the entry:

      ...
       ◉ Choose Vue version
       ◉ Babel
       ◉ TypeScript
       ◯ Progressive Web App (PWA) Support
       ◉ Router
       ◉ Vuex
       ◉ CSS Pre-processors
       ◉ Linter / Formatter
      ❯◯ Unit Testing
       ◯ E2E Testing
      

      Once you have your options selected, press the ENTER key. The CLI will ask you further questions regarding each of the features you selected for your project, in order. The first question will ask which version of Vue you’d like to use: 2.x or 3.x. You’ll use Vue 3 for this tutorial, but you may want to use Vue 2 if you want greater support from the Vue Community:

      ...
      ? Choose a version of Vue.js that you want to start the project with 
        2.x 
      ❯ 3.x (Preview)
      

      The next question is regarding TypeScript integration. If you are not familiar with TypeScript, that’s alright. This option was intentionally selected to illustrate how Vue CLI 3 downloads what you defined as required for unique project. This tutorial will not use the class-style syntax, but will use Babel alongside TypeScript.

      When encountering the following, enter N:

      ...
      Use class-style component syntax? (y/N) N
      

      In the following prompt, enter Y:

      ...
      ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) Y
      

      Next, Vue CLI will ask about history mode. History mode will make each route its own URL. This means you will not have the /#/ (hash) in your application’s URL. If you do use history mode, you will need a Node server to run your project. This is not a problem, because Vue CLI 3 provides you with a Node server.

      Type Y to answer yes to history mode:

      ...
      ? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) Y
      

      The next question is regarding CSS pre-processors such as Sass or LESS. A CSS pre-processor is CSS with added features like nesting and variables. The browser cannot read this, so when the project is built, Node will compile all of your SCSS or LESS code to traditional CSS. Since you are using Node to build your project, it’s recommended to select Sass/SCSS (with node-sass) as your pre-processor. Later on, you’ll add lang attributes in your .vue components to enable SCSS on a per component basis:

      ...
      ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): 
        Sass/SCSS (with dart-sass) 
      ❯ Sass/SCSS (with node-sass) 
        Less 
        Stylus 
      

      After that, you will receive some questions regarding the linter style. A linter is a program that evaluates your code as you develop your application. This linter can enforce a number of syntactical rules during development. In addition to this, your integrated development environment (IDE) can read this configuration file and format your code on save. This will keep your code consistent no matter who works on your project and what operating system or IDE a developer is using.

      For this tutorial, choose ESLint + Standard config:

      ...
      ? Pick a linter / formatter config: 
        ESLint with error prevention only 
        ESLint + Airbnb config 
      ❯ ESLint + Standard config 
        ESLint + Prettier 
        TSLint (deprecated) 
      

      This selects a set of rules for ESLint to enforce. These configurations include options like the use of trailing commas, semi-colons at the end of a line, or using const over var in JavaScript.

      The next option is selecting when you want ESLint to format your code. This can be either on save or when you commit your code to a service like GitHub, GitLab, or BitBucket. It’s recommended to select Lint on save so you can review any changes before committing to version control:

      ...
      ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)
      ❯◉ Lint on save
       ◯ Lint and fix on commit
      

      Once you select your lint features, Vue CLI will ask you about how you want to store these configurations, whether in dedicated files or in the package.json. It’s considered standard practive to store configurations in their own files for a few reasons. One, configurations are easier to share between projects this way, and two, you’ll be keeping your package.json as legible as possible by only defining the important information about your app:

      ...
      ? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
      ❯ In dedicated config files 
        In package.json
      

      Once you are done, the CLI tool will ask if you want to save this selection as a preset for future projects. This is useful if you are generating projects for your employer and you want everything to stay consistent.

      Go ahead and save this configuraion as a preset; Vue CLI will ask you to rename it. Name it DigitalOcean Vue Tutorial Series:

      ...
      ? Save this as a preset for future projects? Yes
      ? Save preset as: DigitalOcean Vue Tutorial Series
      

      Now you can use these exact same settings for a future project.

      At this point, you will have something along the lines of this in your terminal summarizing all of your options:

      ? Please pick a preset: Manually select features
      ? Check the features needed for your project: Choose Vue version, Babel, TS, Router, Vuex, CSS Pre-processors, Linter
      ? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
      ? Use class-style component syntax? No
      ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
      ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
      ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
      ? Pick a linter / formatter config: Standard
      ? Pick additional lint features: Lint on save
      ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
      ? Save this as a preset for future projects? (y/N) 
      

      Press ENTER, and Vue CLI will start creating your app.

      When completed, cd (change directory) in your project name (vue-starter-project):

      Next, start the application with npm run serve. This will run your project at a port on your localhost, usually :8080. If it’s a different port, the CLI will tell you:

      npm run serve
      

      You do not need to download dependencies, as the CLI already did that for you. To view your generated project, open your browser of choice and visit localhost:8080 in the URL bar. You will find a welcome screen with the Vue logo and the technologies you’ve selected in the previous steps.

      Vue template screen

      You can leave this server running throughout this tutorial to monitor your changes.

      In this section, you selected a number of options specific to the project you are creating. Vue CLI downloaded all of the code needed and pre-configured it for you. You can now start working in the generated code base, including creating your own single-file .vue components.

      Now that you have a single-page application running on a Node server, make some changes to this by creating a standard header and footer. These AppHeader.vue and AppFooter.vue components will be imported in such a way that they’ll be present on each route.

      In a new terminal window, navigate into the root of your vue-starter-project folder and list out the files with the following command:

      You will receive the following output:

      Output

      README.md babel.config.js node_modules package-lock.json package.json public src tsconfig.json

      You can also view the contents of your project by opening your project in your editor of choice, such as Visual Studio Code. In either case, you will have access to a number of different files and directories. These config files in the root directory have been created because of the selections made during the intial generation of this project. The option In dedicated config files told Vue CLI to create config.js files for each service you are using, such as Babel, TypeScript, and ESLint. In addition to these files, there are a number of directories. This tutorial will go over these as you get to them.

      First, create a .vue file in the components directory and name it AppHeader.vue. You can do this right-clicking in the components directory and creating a new file in IDEs like VS Code. If you prefer terminal commands, you can do this in your computer’s terminal with the bash command touch:

      touch src/components/AppHeader.vue
      

      In this step, you are creating a single-file component that will contain all of the HTML, JavaScript, and SCSS that this chunk of code needs. Every .vue component contains three basic concerns or sections: <template>, <script>, and <style>. In this case, template is the component’s HTML.

      Open up the new file in your text editor.

      In this file, create a header by using the <header> tag inside of <template>. Inside of this <header>, add the Vue.js logo and a <p> element with the content My Vue.js Application:

      vue-starter-project/src/components/AppHeader.vue

      <template>
        <header>
          <img alt="Vue logo" src="https://www.digitalocean.com/community/tutorials/assets/logo.png" height="50">
          <p>My Vue.js Application</p>
        </header>
      </template>
      

      Keep your development server running throughout development. If you close or cancel the server process, you will not be able to view your application in the browser.

      Save the file.

      At this point, when you open your browser, you will not see the HTML rendered. That is because you need to import the newly created AppHeader.vue component into a component that is already mounted. Since App.vue is your main entry point, it’s best to import it there so our header appears on every route.

      Open up the App.vue file in your text editor, then delete the div with the ID of nav and add the following highlighted code:

      vue-starter-project/src/App.vue

      <template>
        <app-header />
        <router-view/>
      </template>
      
      <script>
      import AppHeader from '@/components/AppHeader.vue'
      
      export default {
        components: {
          AppHeader
        }
      }
      </script>
      

      When you import using ES6, you are essentially creating a variable to later reference in your code. In this case, you are storing the code from AppHeader.vue into a variable called AppHeader. You need to register it via the components property before you can use it.

      Once it’s imported, you deleted the #nav in the template and added <app-header /> before the <router-view />. This renders the component in the HTML.

      After completing this step, save any unsaved file and open your browser back to localhost:8080. Thanks to hot module reloading, you will now find your newly created header at the top of the page:

      Vue template with new header

      You’ve now created a single-file Vue component, used import to bring it into a mounted component, and monitored the change with hot module reloading (HMR). Moving forward, you will extend the functionality of components through the use of child components. You will also use SCSS (the pre-processor you selected earlier) on a per component basis with the lang attribute.

      Now that the header is imported properly into the application, return to AppHeader.vue in your text editor. Add navigation underneath <p>My Vue.js Application</p> by adding the following highlighted code:

      vue-starter-project/src/components/AppHeader.vue

      <template>
        <header>
          <img alt="Vue logo" src="https://www.digitalocean.com/community/tutorials/assets/logo.png" height="50">
          <p>My Vue.js Application</p>
          <nav>
            <ul>
              <li><router-link to="/">Home</router-link></li>
              <li><router-link to="/about">About</router-link></li>
            </ul>
          </nav>
        </header>
      </template>
      

      Now, style this to make it look more like a traditional navigation bar. Create a <style> tag at the end of the file.

      vue-starter-project/src/components/AppHeader.vue

      <template>
        <header>
          <img alt="Vue logo" src="https://www.digitalocean.com/community/tutorials/assets/logo.png" height="50">
          <p>My Vue.js Application</p>
          <nav>
            <ul>
              <li><router-link to="/">Home</router-link></li>
              <li><router-link to="/about">About</router-link></li>
            </ul>
          </nav>
        </header>
      </template>
      
      <style lang="scss">
      </style>
      

      During the inital setup, you selected the Sass/SCSS (with node-sass) option. This is why you added on the lang="scss" attribute to your style tag. If you are unfamiliar with SCSS, it’s recommended to view their official documentation for specifics on when to use nesting or variables.

      This lang attribute will give you the ability to write SCSS in your single-file component. Add the following highlighted contents in the style element:

      vue-starter-project/src/components/AppHeader.vue

      ...
      <style lang="scss">
        header {
          display: flex;
          border-bottom: 1px solid #ccc;
          padding: .5rem 1rem;
      
          p {
            margin-left: 1rem;
          }
        }
      
        nav {
          margin-left: auto;
      
          ul {
            list-style: none;
          }
      
          ul li {
            display: inline-flex;
            margin-left: 1rem;
          }
        }
      </style>
      

      This SCSS creates a horizontal navigation bar with declarations such as display: inline-flex (using Flexbox) and spaces each item out with margin-left auto. To separate the header from the rest of the content, some padding is applied with padding: .5rem 1rem along with a bottom border using border-bottom: 1px solid #ccc. You may notice that the p styles are inside of the header SCSS block. In traditional CSS, that is not allowed, but thanks for SCSS, you can do that. This is refered to as nesting. In this case, the p inside the header is the same as selecting header p in traditional CSS.

      Save your file and navigate to localhost:8080 in your browser to find the new style:

      New style for header in Vue template

      You now have created and styled your header component. Next, you will create the footer component.

      Now that you have a header, you will complete your example application with a footer. In the same components directory, create a new file with the name AppFooter.vue. The process of creating this component is the same as creating the AppHeader.vue. You can create the file in your editor or through the touch base command.

      touch src/components/AppFooter.vue
      

      As before, import this into your App.vue file. Open up App.vue and add the following highlighted code:

      vue-starter-project/src/App.vue

      <template>
        <app-header />
        <router-view/>
        <app-footer />
      </template>
      
      <script>
      import AppHeader from '@/components/AppHeader.vue'
      import AppFooter from '@/components/AppFooter.vue'
      
      export default {
        components: {
          AppHeader,
          AppFooter
        }
      }
      </script>
      ...
      

      This time, you’re importing the component after the router-view tag.

      Save the file, then open up AppFooter.vue. In your AppFooter.vue file, use the <footer> HTML tag with a paragraph:

      vue-starter-project/src/components/AppFooter.vue

      <template>
        <footer>
          <p>Copyright &copy; "current year" </p>
        </footer>
      </template>
      

      You now have a basic footer. Continue to expand on this to include the current year programmatically. This will be dynamic depending on what the year is. To achieve this, you will create a computed property. A computed property is a reactive value calculated with JavaScript.

      In Vue 3, you need to wrap your properties in the setup() function and return these values. Essentially, you are telling Vue to construct this component and provide the template to these reactive computed properties.

      To create a computed property, you’ll first need to deconstruct the computed function from the vue library. You will store this computed function and its value into a const. In Vue 3, you need to pass an anonymous function that returns a value:

      const year = computed(() => new Date().getFullYear())
      

      To add the setup function to your file, add the following script to the end of AppFooter.vue:

      vue-starter-project/src/components/AppFooter.vue

      ...
      <script>
      import { computed } from 'vue'
      
      export default {
        setup () {
          const year = computed(() => new Date().getFullYear())
        }
      }
      </script>
      

      After that, you will need to provide access to the computed property you created for the <template> to consume and render. Return an object with the year property and value in your setup() function:

      /vue-starter-project/src/components/AppFooter.vue

      ...
      setup () {
        const year = computed(() => new Date().getFullYear())
        return { year }
      }
      ...
      

      To use this value in the <template>, use interpolation with double curly braces. This is sometimes referred to the “moustache syntax”:

      /vue-starter-project/src/components/AppFooter.vue

      <template>
        <footer>
          <p>Copyright &copy; {{ year }}</p>
        </footer>
      </template>
      

      Save the file. You will now have the current year in your footer:

      Sample vue application with a computed header

      Conclusion

      In this tutorial, you downloaded the Vue CLI and created your own single-file components with AppHeader.vue and AppFooter.vue. You successfully generated a Vue.js Single Page Application (SPA) with selected features from the initial setup, and learned how all of those pieces come together. In addition, you’ve now reviewed the basic architecture of most SPAs and can use that knowledge to futher expand this project.

      Vue.js is a growing ecosystem with a number of tools at your disposable. These tools can help you quickly get started and save time by storing options as a preset. This is just the start of what Vue.js has to offer, but the CLI is perhaps one of the most important tools you will use in your Vue.js journey.

      For more information on Vue.js and Vue CLI 3, it’s recommended to read through their documentation. The CLI tool specifically has many additional features that weren’t covered in this tutorial. For more tutorials on Vue, check out the Vue Topic Page.



      Source link

      Creating Single File Components in VueJS – A Tutorial


      Updated by Linode Contributed by Pavel Petrov

      When first learning VueJS, and when using it for smaller projects, you will likely use regular, globally-defined components. Once your project grows and you start needing more structure and flexibility, single file components can be a better option.

      Below you can see an example of a barebones single file component, which we will examine part-by-part later in the guide:

      SkeletonComponent.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      
      <template>
      <h1>{{ greeting }}</h1>
      </template>
      
      <script>
      export default {
          name: 'SkeletonComponent',
          data: function() {
              return {
                  greeting: 'Hello'
              };
          },
          props: [],
          methods: {
          },
          created: function(){
          }
      }
      </script>
      
      <style scoped>
      h1 {
          font-size: 2em;
          text-align: center;
      }
      </style>

      In this guide, you will learn:

      Note

      Before You Begin

      If you haven’t read our Building and Using VueJS Components already, go take a look.

      Make sure you have Node.js installed. If you don’t, our How to Install Node.js guide outlines different installation options.

      What are Single File Components

      Single file components are similar to regular components, but there are a few key differences which can make single file components the better tool for your project:

      • They can be defined locally, instead of globally.

      • You can define your component’s <template> outside of your JavaScript, which allows for syntax highlighting in your text editor, unlike with string templates.

      • CSS/styling information is included in the component definition.

      Inspecting a Single File Component

      Single file components are contained in files with the .vue extension. Each .vue file consists of three parts: template, script, style. Let’s revisit our barebones component:

      SkeletonComponent.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      
      <template>
      <h1>{{ greeting }}</h1>
      </template>
      
      <script>
      export default {
          name: 'SkeletonComponent',
          data: function() {
              return {
                  greeting: 'Hello'
              };
          },
          props: [],
          methods: {
          },
          created: function(){
          }
      }
      </script>
      
      <style scoped>
      h1 {
          font-size: 2em;
          text-align: center;
      }
      </style>
      • Lines 1-3 of the component define the <template>, where we specify the HTML template of our component. In comparison, a regular component’s template is represented with a string property inside the component’s JavaScript. This can become increasingly confusing for complex components, because there is no syntax highlighting within the string.

        Another benefit for your templates is that you do not have to adjust the {{ }} mustache tag delimiters to [[ ]] or something else if you are working with another framework that already uses them.

        Note

        For example, Symfony developers using VueJS would have to update their delimiter configuration, because Twig already uses mustache delimiters for its rendering methods. Even though this might be a fairly trivial task, using single file components eliminates that need entirely.
      • The script section of the component (lines 5-19) defines the component’s properties and business logic. This is similar to how regular components are defined, but instead everything is within an export statement.

      • The style section, on lines 21-26, uses the scoped attribute to create component-specific CSS. If you were instead using regular components, you would have no way of adding component-specific CSS, and thus you would have to define your styles globally.

        This makes your components completely independent, so you can now not only use them in your current project, but reuse them among other projects as well. Finally, you can use preprocessors like SASS and Babel for the styling information in your component.

      Prepare your Development Environment

      One drawback of single file components for beginners is that they require webpack or Browserify to build. These tools bundle your application’s dependencies, but they can add to the learning curve. Vue provides a CLI package that’s built on top of webpack and which simplifies managing your project.

      We’ll use this tool throughout this guide; to install it, run:

      sudo npm install -g @vue/cli
      

      The Vue CLI will now be available globally on your workstation (because the -g flag was used).

      Note

      If you’re using NVM, you can install Vue CLI without sudo:

      npm install -g @vue/cli
      

      Create your Project

      All of the examples in this guide will live under a single project. Run the vue create command to create a directory for this project and have Vue CLI build the project skeleton for you:

      vue create single-file-components --packageManager=npm
      
        
      Vue CLI v4.3.1
      ? Please pick a preset: (Use arrow keys)
      ❯ default (babel, eslint)
      Manually select features
      
      

      Note

      You can specify --packageManager=yarn if you prefer yarn to npm.

      The CLI uses pretty sensible defaults, so if you’re a beginner you can just press enter and the Vue CLI will build your first project and install the needed dependencies. If you haven’t done this before, it might take a while to fetch the needed dependencies.

      Now let’s test:

      cd single-file-components && npm run serve
      
        
      DONE Compiled successfully in 3398ms
      
      App running at:
      
      -   Local: http://localhost:8080/
      -   Network: unavailable
      
      Note that the development build is not optimized.
      To create a production build, run npm run build.
      
      

      What npm run serve does is run the development server, but the cool thing is that while you make changes the dev server automatically rebuilds the project and injects the changes in the browser, so you don’t even have to refresh.

      Now, if everything is fine, you should be able to open http://localhost:8080 in your browser and you will see the VueJS welcome screen:

      VueJS Welcome Screen

      Let’s look at the directory structure of the default application and go through each folder:

      tree -I node_modules
      
        
      .
      ├── babel.config.js
      ├── package.json
      ├── package-lock.json
      ├── public
      │   ├── favicon.ico
      │   └── index.html
      ├── README.md
      └── src
          ├── App.vue
          ├── assets
          │   └── logo.png
          ├── components
          │   └── HelloWorld.vue
          └── main.js
      
      

      Note

      The -I node_modules option will tell tree to ignore your node_modules/ directory, which is where all of the node dependencies reside.

      The public Folder and index.html

      Files in the public folder will not be bundled by webpack. When your project is created, this folder will contain an index.html file:

      index.html
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="utf-8" />
          <meta http-equiv="X-UA-Compatible" content="IE=edge" />
          <meta name="viewport" content="width=device-width,initial-scale=1.0" />
          <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
          <title><%= htmlWebpackPlugin.options.title %></title>
      </head>
      <body>
          <noscript>
              <strong>
                  We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't
                  work properly without JavaScript enabled. Please enable it to
                  continue.
              </strong>
          </noscript>
          <div id="app"></div>
          <!-- built files will be auto injected -->
      </body>
      </html>

      On lines 7, 8, and 13 you will notice the <%= %> syntax where the favicon link and page title are embedded; this is part of the lodash template syntax, which the index file is written in. While your index file isn’t included in webpack’s dependency bundle, it will be processed by the html-webpack-plugin, which does a few useful things:

      • It populates the variables that you embed using the template syntax. You can see more about the default variable values exposed by webpack here.
      • It automatically connects your index to the app bundle that webpack compiles: on line 19, you’ll see a comment that says the files built by webpack are auto-injected by the build procedure.

        More about the build procedure for index.html

        This is an example of what the file will look like after the build procedure:

        index.html
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        
        <!DOCTYPE html>
        <html lang=en>
        <head>
            <meta charset=utf-8>
            <meta http-equiv=X-UA-Compatible content="IE=edge">
            <meta name=viewport content="width=device-width,initial-scale=1">
            <link rel=icon href=/favicon.ico> <title>single-file-components</title>
            <link href=/css/app.fb0c6e1c.css rel=preload as=style>
            <link href=/js/app.ae3090b2.js rel=preload as=script>
            <link href=/js/chunk-vendors.b4c61135.js rel=preload as=script>
            <link href=/css/app.fb0c6e1c.css rel=stylesheet>
        </head>
        <body>
            <noscript>
                <strong>
                    We're sorry but single-file-components doesn't work properly without JavaScript enabled. Please
                    enable it to continue.
                </strong>
            </noscript>
            <div id=app></div>
            <script src=/js/chunk-vendors.b4c61135.js></script>
            <script src=/js/app.ae3090b2.js></script>
        </body>
        </html>

        Notice that your app’s script and CSS dependencies have been added to the file on lines 21 and 22, and that these files have random hash appended their names (e.g. app.ae3090b2.js). These hashes will change over time for subsequent builds of your app, and the html-webpack-plugin will keep the hash updated in your index. Without this feature, you would need to update those lines for each build.

      The rest of the body contains these elements:

      • The noscript tag, which is in place to warn users with disabled JS that the app will not work unless they enable it.
      • The <div id="app"></div> container where our VueJS app will be bound.

      The src Folder

      The src/ folder is where most of your work will be done. The src/main.js file will serve as the entry point for webpack’s build process:

      src/main.js
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      import Vue from 'vue'
      import App from './App.vue'
      
      Vue.config.productionTip = false
      
      new Vue({
          render: h => h(App),
      }).$mount('#app')
      

      This file imports VueJS (line 1), imports the App component from the src folder (line 2), and binds the App component to the container with the id property set to app (lines 6-8).

      Now to the interesting part: src/App.vue:

      src/App.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      
      <template>
          <div id="app">
              <img alt="Vue logo" src="./assets/logo.png" />
              <HelloWorld msg="Welcome to Your Vue.js App" />
          </div>
      </template>
      
      <script>
      import HelloWorld from "./components/HelloWorld.vue";
      export default {
          name: "App",
          components: {
              HelloWorld,
          },
      };
      </script>
      
      <style>
      #app {
          font-family: Avenir, Helvetica, Arial, sans-serif;
          -webkit-font-smoothing: antialiased;
          -moz-osx-font-smoothing: grayscale;
          text-align: center;
          color: #2c3e50;
          margin-top: 60px;
      }
      </style>

      This is a simple single file component relatively similar to the example we discussed above, but this example shows how to import and use components:

      • On line 9, the HelloWorld component is imported.
      • On lines 12-14, the HelloWorld component is locally registered for use within the App component. The registered component can only be used in the template of the parent component that registered it. Contrast this with the components in Building and Using VueJS Components, which were globally registered.

        Note

        Local registration is a valuable architectural feature for reusable components within big projects.

      • The HelloWorld component is used within the App component’s template on line 4.

      Building your First Single File Components

      Now that we’ve covered the basic structure of the project created by Vue CLI, let’s build our own components on top of that. As in Building and Using VueJS Components, we will again be building a rating application, but this time it will be a little more sophisticated.

      This is what your rating app will look like:

      Rating App - Finished Product

      This is how it will behave:

      • Clicking on a star on the left side will register a vote for that star.

      • The left side will interactively change when a user hovers over the stars.

      • It will allow the user to rate only once on each visit to the page. If the page is refreshed, or if it is visited again later, the user can vote again.

      • It will keep score of votes between page visits in the browser’s local storage.

      Here’s how the app’s template will look in protocode; you do not need to copy and paste this:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      
      <div id="app">
          <div class="inner">
              <div class="ratingContainer">
                  <span class="bigRating"></span>
                  <div class="rating-stars">
                      <Star weight="1"></Star>
                      <Star weight="2"></Star>
                      <Star weight="3"></Star>
                      <Star weight="4"></Star>
                      <Star weight="5"></Star>
                  </div>
              </div>
              <Summary></Summary>
          </div>
      </div>

      We’ll make each star a separate component (named Star), and we’ll also create a Summary component which will hold the summary of the votes.

      App.vue

      To start, replace the content of your App.vue with this snippet:

      src/App.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      
      <template>
          <div id="app">
              <div class="inner">
                  <div class="ratingContainer">
                      <span class="bigRating" v-html="bigRating"></span>
                      <div>
                          <Star
                              v-for="index in 5"
                              v-bind:key="index"
                              v-bind:weight="index"
                              v-bind:enabled="enabled"
                              v-bind:currentRating="currentRating"
                          ></Star>
                      </div>
                  </div>
                  <Summary v-bind:ratings="ratings"></Summary>
              </div>
          </div>
      </template>
      
      <script>
      import Star from "./components/Star.vue";
      import Summary from "./components/Summary.vue";
      
      export default {
          name: "App",
          components: { Star, Summary },
          data: function () {
              return {
                  currentRating: 0,
                  bigRating: "&#128566;", // Emoji: 😶
                  enabled: true,
                  ratings: [
                      {
                          weight: 1,
                          votes: 0,
                      },
                      {
                          weight: 2,
                          votes: 0,
                      },
                      {
                          weight: 3,
                          votes: 0,
                      },
                      {
                          weight: 4,
                          votes: 0,
                      },
                      {
                          weight: 5,
                          votes: 0,
                      },
                  ],
              };
          },
          methods: {},
          created: function () {
              if (localStorage.ratings) {
                  this.ratings = JSON.parse(localStorage.ratings);
              }
          },
      };
      </script>
      
      <style>
      @import url(https://fonts.googleapis.com/css?family=Roboto:100, 300, 400);
      @import url(https://netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css);
      #app {
          width: 400px;
      }
      .ratingContainer {
          float: left;
          width: 45%;
          margin-right: 5%;
          text-align: center;
      }
      .bigRating {
          color: #333333;
          font-size: 72px;
          font-weight: 100;
          line-height: 1em;
          padding-left: 0.1em;
      }
      </style>

      This is the main component, but there are no methods set on it yet, so for now it doesn’t have any functionality. Here are some notable parts of the code:

      • <template>:

        • On lines 7-13, all five Star components are rendered from a single <Star> declaration with the v-for="index in 5" syntax. A weight is assigned to each Star by the v-bind:weight="index" syntax. The key attribute is also bound to the index. The enabled and currentRating props will be bound to values that are described in the <script> section.

          Note

          The v-for syntax is similar to the following for loop: for(let index=1;index<=5;index++).

        • On line 16, the Summary component is rendered. It will display data from the bound ratings property.

      • <script>

        • Lines 22 and 23 import the Star and Summary components, which are then registered on line 27. These will be created separately in the next section.

        • The data function is declared on lines 28-56, and it contains the following variables which will control the functionality of the app once the methods are added later:

          • currentRating: As we hover over the stars, we will use this variable to store the rating of the hovered star.

          • bigRating: This will be set to an emoticon that represents the currentRating.

          • enabled: This will be used to disable the rating application once the user has cast a vote.

          • ratings: This is a structure for the votes that have been cast. We set the default value in the data function, and if there are any votes saved in the browser’s localStorage, then we overwrite the defaults, which imitates a persistence layer. In the created hook (lines 58-62) you can see how we fetch the saved cast votes.

      Star.vue and Summary.vue

      In your src/components/ directory, create two files named Star.vue and Summary.vue and paste these snippets into them:

      src/components/Star.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      <template>
          <i class="icon-star"></i>
      </template>
      
      <script>
      export default {
          name: "Star",
          props: ["weight", "enabled", "currentRating"]
      };
      </script>
      
      <style scoped>
      i.icon-star {
          font-size: 20px;
          color: #e3e3e3;
          margin-bottom: 0.5em;
      }
      </style>
      src/components/Summary.vue
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      
      <template>
          <div class="summaryContainer">
              <ul>
                  <li v-for="rating in ratings" v-bind:key="rating.weight">
                      {{ rating.weight }}<i class="icon-star"></i>: {{ rating.votes }}
                  </li>
              </ul>
          </div>
      </template>
      
      <script>
      export default {
          name: "Summary",
          props: ["ratings"]
      };
      </script>
      
      <style scoped>
      .summaryContainer {
          float: left;
          width: 50%;
          font-size: 13px;
      }
      </style>

      Here are some notable parts of the code:

      • In both components, the Font Awesome icon-star is used. On lines 13-17 of Star.vue, some styling is set for the icons in the Star component, including setting the color to light grey.

        Because this style section uses the scoped attribute, these styles are limited to the Star component. As a result, the icons in the Summary component are not also styled in this way.

      • On lines 4-6 of Summary.vue, the v-for syntax is used again to display the rating votes.

      After creating Star.vue and Summary.vue, the application can be viewed in the browser. Head to http://127.0.0.1:8080 and you will see the following:

      Rating App - No Votes, Noninteractive

      Because there are no methods set on the components yet, it will not be interactive.

      Note

      If you’re not still running npm run serve in your terminal, you’ll need to re-run it from inside your project.

      Adding Methods to the Components

      The application right now is a skeleton, so now we’ll make it work. These three custom events will be handled:

      • When you hover over a star, all previous stars will be highlighted in yellow. For example, if you hover over the star number 4, stars 1-3 also get highlighted.

      • When your mouse moves away, the highlight will be removed.

      • When you click on a star, a vote is cast and you no longer can vote until you visit the page again.

      Updating App.vue

      1. Update the Star component declaration in the <template> of src/App.vue to match this snippet:

        src/App.vue
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        
        <!-- ... --->
        <Star
            v-for="index in 5"
            v-bind:key="index"
            v-bind:weight="index"
            v-bind:enabled="enabled"
            v-bind:currentRating="currentRating"
            v-on:lightUp="lightUpHandler"
            v-on:lightDown="lightDownHandler"
            v-on:rate="rateHandler"
        ></Star>
        <!-- ... --->

        The new additions to this declaration are the v-on directives, which set methods as event handlers for the custom lightUp, lightDown, and rate events.

        Note

        The Star component will be updated in the next section to emit those events.

      2. Next, replace the methods object in the component with the following snippet. These are the event handlers:

        src/App.vue
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        
        // ...
        methods: {
            lightUpHandler: function (weight) {
                this.currentRating = weight;
        
                // Display different emojis based on the weight
                if (weight <= 2) {
                    this.bigRating = "&#128549;"; // Emoji: 😥
                }
                if (weight > 2 && weight <= 4) {
                    this.bigRating = "&#128556;"; // Emoji: 😬
                }
                if (weight > 4) {
                    this.bigRating = "&#128579;"; // Emoji: 🙃
                }
            },
            lightDownHandler: function () {
                // Reset on mouse away
                this.currentRating = 0;
                this.bigRating = "&#128566;"; // Emoji: 😶
            },
            rateHandler: function (weight) {
                this.currentRating = weight;
        
                // Finding the relevant rating and incrementing the cast votes
                let rating = this.ratings.find((obj) => obj.weight == weight);
                rating.votes++;
        
                // Disabling from voting again
                this.enabled = false;
        
                // Saves the votes to the browser localStorage
                localStorage.setItem("ratings", JSON.stringify(this.ratings));
            },
        },
        // ...
        
        • The lightUpHandler and rateHandler methods receive a weight from the Star component that emitted the corresponding event. These methods set the weight as the currentRating.

        • At the end of the rateHandler method, the component’s ratings are converted to a JSON object and saved so we can use them as a starting point the next time the page loads (line 33).

        Full contents of App.vue

        At this point, your App.vue should be the same as this snippet:

        src/App.vue
          1
          2
          3
          4
          5
          6
          7
          8
          9
         10
         11
         12
         13
         14
         15
         16
         17
         18
         19
         20
         21
         22
         23
         24
         25
         26
         27
         28
         29
         30
         31
         32
         33
         34
         35
         36
         37
         38
         39
         40
         41
         42
         43
         44
         45
         46
         47
         48
         49
         50
         51
         52
         53
         54
         55
         56
         57
         58
         59
         60
         61
         62
         63
         64
         65
         66
         67
         68
         69
         70
         71
         72
         73
         74
         75
         76
         77
         78
         79
         80
         81
         82
         83
         84
         85
         86
         87
         88
         89
         90
         91
         92
         93
         94
         95
         96
         97
         98
         99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        113
        114
        115
        116
        117
        118
        119
        120
        121
        
        <template>
            <div id="app">
                <div class="inner">
                    <div class="ratingContainer">
                        <span class="bigRating" v-html="bigRating"></span>
                        <div>
                            <Star
                                v-for="index in 5"
                                v-bind:key="index"
                                v-bind:weight="index"
                                v-bind:enabled="enabled"
                                v-bind:currentRating="currentRating"
                                v-on:lightUp="lightUpHandler"
                                v-on:lightDown="lightDownHandler"
                                v-on:rate="rateHandler"
                            ></Star>
                        </div>
                    </div>
                    <Summary v-bind:ratings="ratings"></Summary>
                </div>
            </div>
        </template>
        
        <script>
        import Star from "./components/Star.vue";
        import Summary from "./components/Summary.vue";
        
        export default {
            name: "App",
            components: { Star, Summary },
            data: function () {
                return {
                    currentRating: 0,
                    bigRating: "&#128566;", // Emoji: 😶
                    enabled: true,
                    ratings: [
                        {
                            weight: 1,
                            votes: 0,
                        },
                        {
                            weight: 2,
                            votes: 0,
                        },
                        {
                            weight: 3,
                            votes: 0,
                        },
                        {
                            weight: 4,
                            votes: 0,
                        },
                        {
                            weight: 5,
                            votes: 0,
                        },
                    ],
                };
            },
            methods: {
                lightUpHandler: function (weight) {
                    this.currentRating = weight;
        
                    // Display different emojis based on the weight
                    if (weight <= 2) {
                        this.bigRating = "&#128549;"; // Emoji: 😥
                    }
                    if (weight > 2 && weight <= 4) {
                        this.bigRating = "&#128556;"; // Emoji: 😬
                    }
                    if (weight > 4) {
                        this.bigRating = "&#128579;"; // Emoji: 🙃
                    }
                },
                lightDownHandler: function () {
                    // Reset on mouse away
                    this.currentRating = 0;
                    this.bigRating = "&#128566;"; // Emoji: 😶
                },
                rateHandler: function (weight) {
                    this.currentRating = weight;
        
                    // Finding the relevant rating and incrementing the cast votes
                    let rating = this.ratings.find((obj) => obj.weight == weight);
                    rating.votes++;
        
                    // Disabling from voting again
                    this.enabled = false;
        
                    // Saves the votes to the browser localStorage
                    localStorage.setItem("ratings", JSON.stringify(this.ratings));
                },
            },
            created: function () {
                if (localStorage.ratings) {
                    this.ratings = JSON.parse(localStorage.ratings);
                }
            },
        };
        </script>
        
        <style>
        @import url(https://fonts.googleapis.com/css?family=Roboto:100, 300, 400);
        @import url(https://netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css);
        #app {
            width: 400px;
        }
        .ratingContainer {
            float: left;
            width: 45%;
            margin-right: 5%;
            text-align: center;
        }
        .bigRating {
            color: #333333;
            font-size: 72px;
            font-weight: 100;
            line-height: 1em;
            padding-left: 0.1em;
        }
        </style>

      Updating Star.vue

      Let’s modify the Star component to emit the events:

      1. In the template of Star.vue, replace the <i> element with this snippet:

        src/components/Star.vue
        1
        2
        3
        4
        5
        6
        7
        8
        
        <!-- ... --->
        <i
            v-bind:class="getClass()"
            v-on:mouseover="mouseoverHandler"
            v-on:mouseleave="mouseleaveHandler"
            v-on:click="clickHandler"
        ></i>
        <!-- ... --->
        • The CSS classes of the icon will now be dynamically generated by a getClass method on the component. This change is made so that the hover highlight effect can be toggled by a CSS class.

        • The mouseover, mouseleave, and click DOM events are associated with new handler methods that will also be added to the component.

      2. In the script section, add this data function to the component:

        src/components/Star.vue
        1
        2
        3
        4
        5
        6
        7
        8
        
        // ...
        data: function () {
            return {
                hover: false,
            };
        },
        // ...
        

        The hover variable will maintain the hover state of the component.

      3. Also in the script section, add this methods object to the component:

        src/components/Star.vue
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        
        // ...
        methods: {
            getClass: function () {
                var baseClass = "icon-star";
        
                // Adds the hover class if you're hovering over the component or you are hovering over a star with greater weight
                if (this.hover || this.currentRating >= this.weight) {
                    baseClass += " hover";
                }
                return baseClass;
            },
            mouseoverHandler: function () {
                // Makes sure stars are not lighting up after vote is cast
                if (this.enabled) {
                    // Emits the lightUp event with the weight as a parameter
                    this.$emit("lightUp", this.weight);
                    // Enables hover class
                    this.hover = true;
                }
            },
            mouseleaveHandler: function () {
                // Makes sure stars are not lighting up after vote is cast
                if (this.enabled) {
                    // Emits the lightDown event
                    this.$emit("lightDown", this.weight);
                    // Removes hover class
                    this.hover = false;
                }
            },
            clickHandler: function () {
                // Makes sure you only vote if you haven't voted yet
                if (this.enabled) {
                    // Emits the rate event with the weight as parameter
                    this.$emit("rate", this.weight);
                } else {
                    alert("Already voted");
                }
            },
        },
        // ...
        
        • The mouseoverHandler, mouseleaveHandler, and clickHandler methods will emit the lightUp, lightDown, and rate custom events, respectively.

        • These methods also first check to see if enabled has been set to false; if false, then the methods do nothing, which means that the DOM events will result in no action.

        • In the getClass method, the currentRating prop is used to determine if a star icon should be highlighted. This prop was previously bound to the currentRating data property of the App component.

          Note

          The currentRating prop is not a particularly beautiful solution, but we will improve on that further in the guide.

      4. Finally, add this rule to the style section:

        src/components/Star.vue
        1
        2
        3
        4
        5
        6
        
        /* ... */
        i.icon-star.hover {
            color: yellow;
        }
        /* ... */
        

        Full contents of Star.vue

        At this point, your Star.vue should be the same as this snippet:

        src/components/Star.vue
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        
        <template>
            <i
                v-bind:class="getClass()"
                v-on:mouseover="mouseoverHandler()"
                v-on:mouseleave="mouseleaveHandler()"
                v-on:click="clickHandler()"
            ></i>
        </template>
        
        <script>
        export default {
            name: "Star",
            data: function () {
                return {
                    hover: false,
                };
            },
            props: ["weight", "enabled", "currentRating"],
            methods: {
                getClass: function () {
                    var baseClass = "icon-star";
        
                    // Adds the hover class if you're hovering over the component or you are hovering over a star with greater weight
                    if (this.hover || this.currentRating >= this.weight) {
                        baseClass += " hover";
                    }
                    return baseClass;
                },
                mouseoverHandler: function () {
                    // Makes sure stars are not lighting up after vote is cast
                    if (this.enabled) {
                        // Emits the lightUp event with the weight as a parameter
                        this.$emit("lightUp", this.weight);
                        // Enables hover class
                        this.hover = true;
                    }
                },
                mouseleaveHandler: function () {
                    // Makes sure stars are not lighting up after vote is cast
                    if (this.enabled) {
                        // Emits the lightDown event
                        this.$emit("lightDown", this.weight);
                        // Removes hover class
                        this.hover = false;
                    }
                },
                clickHandler: function () {
                    // Makes sure you only vote if you haven't voted yet
                    if (this.enabled) {
                        // Emits the rate event with the weight as parameter
                        this.$emit("rate", this.weight);
                    } else {
                        alert("Already voted");
                    }
                },
            },
        };
        </script>
        
        <style scoped>
        i.icon-star {
            font-size: 20px;
            color: #e3e3e3;
            margin-bottom: 0.5em;
        }
        i.icon-star.hover {
            color: yellow;
        }
        </style>
      5. Head to http://localhost:8080/ in your browser, and you should see that your rating application now works. Try hovering over the stars and clicking on them to observe the interaction. If you refresh the page, you can vote again, and the votes will be tallied:

      Rating App - With Rating Interaction

      Communication between Components Via an Event Bus

      Notice how clumsy all of the v-on directives chained one after the other look:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      
      <Star
          v-for="index in 5"
          v-bind:key="index"
          v-bind:weight="index"
          v-bind:enabled="enabled"
          v-bind:currentRating="currentRating"
          v-on:lightUp="lightUpHandler"
          v-on:lightDown="lightDownHandler"
          v-on:rate="rateHandler"
      ></Star>

      This setup can be inelegant to scale: imagine having 10 of those on a single component, and then imagine you have 10 components. The directives would become hard to follow, so it’s worth exploring other ways to communicate between components.

      Fortunately, VueJS supports a publish-subscribe pattern called an event bus. You can easily implement it in your components to make things a bit more elegant.

      Event Bus Basics

      In VueJS, an event bus is a new Vue instance that is declared globally (in main.js, for example):

      src/main.js
      1
      2
      3
      4
      
      // ...
      export const eventBus = new Vue();
      // ...
      

      It is then imported in each component which accesses it:

      AnyComponent.vue
      1
      2
      3
      4
      
      // ...
      import { eventBus } from "../main.js";
      // ...
      

      Components can emit events to the event bus:

      SomeComponent.vue
      1
      2
      3
      4
      
      // ...
      eventBus.$emit("event", parameter);
      // ...
      

      Other components will register event handlers on the same event bus with the $on method:

      AnotherComponent.vue
      1
      2
      3
      4
      5
      6
      
      // ...
      eventBus.$on("event", (parameter) => {
          // Do stuff
      });
      // ...
      

      Basically, think of the event bus as a global communication layer between your components.

      Adding an Event Bus to your App

      Now let’s rebuild our example to take advantage of an event bus:

      1. Open main.js and replace its content with this snippet:

        src/main.js
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        
        import Vue from "vue";
        import App from "./App.vue";
        
        Vue.config.productionTip = false;
        
        export const eventBus = new Vue();
        
        new Vue({
            render: h => h(App)
        }).$mount("#app");
        

        This update adds an event bus declaration on line 6.

      2. Open App.vue and replace its content with this snippet:

        src/App.vue
          1
          2
          3
          4
          5
          6
          7
          8
          9
         10
         11
         12
         13
         14
         15
         16
         17
         18
         19
         20
         21
         22
         23
         24
         25
         26
         27
         28
         29
         30
         31
         32
         33
         34
         35
         36
         37
         38
         39
         40
         41
         42
         43
         44
         45
         46
         47
         48
         49
         50
         51
         52
         53
         54
         55
         56
         57
         58
         59
         60
         61
         62
         63
         64
         65
         66
         67
         68
         69
         70
         71
         72
         73
         74
         75
         76
         77
         78
         79
         80
         81
         82
         83
         84
         85
         86
         87
         88
         89
         90
         91
         92
         93
         94
         95
         96
         97
         98
         99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        
        <template>
            <div id="app">
                <div class="inner">
                    <div class="ratingContainer">
                        <span class="bigRating" v-html="bigRating"></span>
                        <div>
                            <Star
                                v-for="index in 5"
                                v-bind:key="index"
                                v-bind:weight="index"
                                v-bind:enabled="enabled"
                            ></Star>
                        </div>
                    </div>
                    <Summary v-bind:ratings="ratings"></Summary>
                </div>
            </div>
        </template>
        
        <script>
        import Star from "./components/Star.vue";
        import Summary from "./components/Summary.vue";
        
        import { eventBus } from "./main.js";
        
        export default {
            name: "App",
            components: { Star, Summary },
            data: function () {
                return {
                    bigRating: "&#128566;", // Emoji: 😶
                    enabled: true,
                    ratings: [
                        {
                            weight: 1,
                            votes: 0,
                        },
                        {
                            weight: 2,
                            votes: 0,
                        },
                        {
                            weight: 3,
                            votes: 0,
                        },
                        {
                            weight: 4,
                            votes: 0,
                        },
                        {
                            weight: 5,
                            votes: 0,
                        },
                    ],
                };
            },
            created: function () {
                if (localStorage.ratings) {
                    this.ratings = JSON.parse(localStorage.ratings);
                }
                eventBus.$on("lightUp", (weight) => {
                    // Display different emojis based on the weight
                    if (weight <= 2) {
                        this.bigRating = "&#128549;"; // Emoji: 😥
                    }
                    if (weight > 2 && weight <= 4) {
                        this.bigRating = "&#128556;"; // Emoji: 😬
                    }
                    if (weight > 4) {
                        this.bigRating = "&#128579;"; // Emoji: 🙃
                    }
                });
                eventBus.$on("lightDown", () => {
                    this.bigRating = "&#128566;"; // Emoji: 😶
                });
                eventBus.$on("rate", (weight) => {
                    // Finding the relevant rating and incrementing the cast votes
                    let rating = this.ratings.find((obj) => obj.weight == weight);
                    rating.votes++;
        
                    // Disabling from voting again
                    this.enabled = false;
        
                    // Saves the votes to the browser localStorage
                    localStorage.setItem("ratings", JSON.stringify(this.ratings));
                });
            },
        };
        </script>
        
        <style>
        @import url(https://fonts.googleapis.com/css?family=Roboto:100, 300, 400);
        @import url(https://netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css);
        #app {
            width: 400px;
        }
        .ratingContainer {
            float: left;
            width: 45%;
            margin-right: 5%;
            text-align: center;
        }
        .bigRating {
            color: #333333;
            font-size: 72px;
            font-weight: 100;
            line-height: 1em;
            padding-left: 0.1em;
        }
        </style>

        The following changes have been made in this updated file:

        • The eventBus instance is imported on line 24.
        • We removed the v-on directives from the Star component declaration in the template (lines 7-12).
        • The component’s methods have been removed, which previously served as the event handlers for the v-on directives.
        • Instead, we subscribe to the events in the created hook (lines 61-86). The logic that was in the component’s methods has been moved here.
        • We also no longer need the currentRating data property, so it has been removed. This is because the Star components will also subscribe to the event bus and can be directly notified of all lightUp and rate events.

        The template looks much leaner now, and you can easily spot the subscribed events by simply having a look in the created hook.

      3. Open Star.vue and replace its content with this snippet:

        src/components/Star.vue
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        
        <template>
            <i
                v-bind:class="getClass()"
                v-on:mouseover="mouseoverHandler"
                v-on:mouseleave="mouseleaveHandler"
                v-on:click="clickHandler"
            ></i>
        </template>
        
        <script>
        import { eventBus } from "../main.js";
        
        export default {
            name: "Star",
            data: function () {
                return {
                    hover: false,
                    active: false,
                };
            },
            props: ["weight", "enabled"],
            methods: {
                getClass: function () {
                    var baseClass = "icon-star";
                    if (this.active) {
                        baseClass += " active";
                    }
                    if (this.hover) {
                        baseClass += " hover";
                    }
                    return baseClass;
                },
                mouseoverHandler: function () {
                    // Makes sure stars are not lighting up after vote is cast
                    if (this.enabled) {
                        // Emits the lightUp event with the weight as a parameter
                        eventBus.$emit("lightUp", this.weight);
                    }
                },
                mouseleaveHandler: function () {
                    // Makes sure stars are not lighting up after vote is cast
                    if (this.enabled) {
                        // Emits the lightDown event
                        eventBus.$emit("lightDown");
                    }
                },
                clickHandler: function () {
                    // Makes sure you only vote if you haven't voted yet
                    if (this.enabled) {
                        // Emits the rate event with the weight as parameter
                        eventBus.$emit("rate", this.weight);
                    } else {
                        alert("Already voted");
                    }
                },
            },
            created: function () {
                eventBus.$on("lightUp", (targetWeight) => {
                    if (targetWeight >= this.weight) {
                        this.hover = true;
                    } else {
                        this.hover = false;
                    }
                });
                eventBus.$on("lightDown", () => {
                    this.hover = false;
                });
                eventBus.$on("rate", (targetWeight) => {
                    if (targetWeight >= this.weight) {
                        this.active = true;
                    }
                });
            },
        };
        </script>
        
        <style scoped>
        i.icon-star {
            font-size: 20px;
            color: #e3e3e3;
            margin-bottom: 0.5em;
        }
        i.icon-star.hover {
            color: yellow;
        }
        i.icon-star.active {
            color: #737373;
        }
        </style>

        The following changes have been made in this updated file:

        • The eventBus instance is imported on line 11.
        • The currentRating prop has been removed (line 21).
        • We’ve modified the handler methods to emit the events on the eventBus instance (lines 22-56)
        • We also subscribe to the same events from the created hook (lines 57-73), so that all Star components are aware of which component the user is currently hovering over without needing the currentRating prop.
        • We’ve added an active class to the component’s style (lines 86-88). This is enabled when a user enters a rating, and it sets a different highlight color for the stars. To enable the class, an active data property has been added to the component (line 18), and it is set to true within the rate event handling logic (line 70).

        Rating App - With Event Bus

      More Information

      You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

      This guide is published under a CC BY-ND 4.0 license.



      Source link