One place for hosting & domains

      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

      Working with Environment Variables in Vue.js


      While this tutorial has content that we believe is of great benefit to our community, we have not yet tested or
      edited it to ensure you have an error-free learning experience. It’s on our list, and we’re working on it!
      You can help us out by using the “report an issue” button at the bottom of the tutorial.

      In this post, we’ll learn how to work with distinct configurations between development and production mode for Vue.js projects that use the CLI’s webpack template.

      In a web app, we most likely have to access a backend API server through a URL. This URL can be something like http://localhost:8080/api while in development, and https://site.com/api in production when the project is deployed. Environment variables allow us for an easy way to change this URL automatically, according to the current state of the project.

      An easy way to use environment variables with Vue and the webpack template is through files with a .env extension. These files become responsible for storing information that’s specific to the environment (development, testing, production,…)

      The majority of this post applies to apps using v2.x of the Vue CLI, but environment variables are just as easy to manage in the Vue CLI v3.

      Using .env Files in Vue

      The simplest way to use .env files in Vue is to create an application that already supports environment files. Let’s use the vue-cli and the webpack template for that.

      With Node 8 or higher installed, run the following, where my-app is your app name:

      $ npx vue-cli init webpack my-app
      

      This command will create an application with several files ready for use. In this post, we’re focusing only on the environment configuration, which can be accessed in the config directory:

      Project file structure

      There are two files in the config directory: dev.env.js and prod.env.js, and you’ll also have a test.env.js file if you’ve configured tests while initiating the project. These files are used in development and production mode, or in other words, when you are running the application through the npm run dev command, the dev.env.js file is used, and when you compile the project for production with the npm run build command, the prod.env.js file is used instead.

      Let’s change the development file to:

      dev.env.js

      'use strict'
      const merge = require('webpack-merge')
      const prodEnv = require('./prod.env')
      
      module.exports = merge(prodEnv, {
        NODE_ENV: '"development"',
        ROOT_API: '"http://localhost/api"'
      })
      

      Our development environment file has an additional variable called ROOT_API, with the value http://localhost/api.

      Now let’s change the production file to:

      prod.env.js

      'use strict'
      module.exports = {
        NODE_ENV: '"production"',
        ROOT_API: '"http://www.site.com/api"'
      }
      

      Here we have the same ROOT_API variable, but with a different value, which should only be used in production mode. Note how string variables need the double quotes inside the single quotes.

      Using the Environment Files in Your Code

      After creating the ROOT_API variable, we can use it anywhere in Vue through the global process.env object:

      process.env.ROOT_API
      

      For example, open the src/components/HelloWorld.vue file and in the <script> tag add the following:

      mounted() {
        console.log(process.env.ROOT_API)
      }
      

      After running npm run dev, you will see the console.log information in the browser dev tools:

      Running the app

      If you run the npm run build command, the dist directory will be created with the application ready to be deployed to a production environment, and the variable ROOT_API will display the value http://www.site.com./api, as specified in prod.env.js.

      Thus, we can work with different variables for each different environment, using the ready-made configuration that the webpack template provides us. If you use another template, make sure you find an equivalent feature or use a library like dotenv to manage your environment variables.

      What About Vue CLI 3?

      If your app is using the new Vue CLI, you’ll want to instead have files like .env and .env.prod at the root of your project and include variables like this:

      .env

      VUE_APP_ROOT_API=http://localhost/api
      

      .env.prod

      VUE_APP_ROOT_API=http://www.site.com/api
      

      The VUE_APP_ prefix is important here, and variables without that prefix won’t be available in your app.



      Source link

      Building and Using VueJS Components – A Tutorial


      Updated by Linode Contributed by Pavel Petrov

      How to Build and Use VueJS Components

      What are VueJS Components

      In VueJS, components are a way to create custom VueJS instances which can easily be reused in your code. In order to properly explain what VueJS components are we will build a very simple rating-counter component.

      This guide is written for new VueJS users and will explain what are components are, how to build them, and how to use them. Basic knowledge of JavaScript and VueJS is important for following through with the tutorial.

      In this guide you learn how to:

      Note

      Prepare the Vue App

      In your text editor on your computer, create a new file called ratingcounter.html. Then, paste in the content from this snippet:

      ratingcounter.html
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      
      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      
      <div id="app">
      </div>
      
      <script>
      var app = new Vue({
        el: '#app'
      })
      </script>

      As this will be a simple VueJS application, the first thing we have to do is include the VueJS library in our document.

      • The easiest way that is done is by going to the Installation page at vuejs.org and copying the script tag specified under CDN.

      • As we are in the development stage of the application, we will use the development version of the VueJS library. This line is copied into line 1 of the HTML snippet.

      After the VueJS library is included, a div with id set to app is created on lines 3-4. On lines 6-10, a barebones VueJS app is created and linked to this element.

      So far this new app does nothing at all, but we will use it for the skeleton of our example application.

      Creating your First Component

      The component we’ll be developing is a simple reusable rating counter that will illustrate how VueJS components work. We’ll explain each part of the component along the way. Let’s get started:

      Define the Component

      In your ratingcounter.html, update the second <script> section (currently on lines 6-10 of your file) to include the Vue.component() function from lines 7-18 of this snippet:

      ratingcounter.html
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      
      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      
      <div id="app">
      </div>
      
      <script>
      Vue.component('rating-counter', {
          data() {
              return {
                  count: 0
              }
          },
          template:   `<div>
                          <button v-on:click="count--">Thumbs Down</button>
                          {{ count }}
                          <button v-on:click="count++">Thumbs Up</button>
                      </div>`
      })
      
      var app = new Vue({
        el: '#app'
      })
      </script>

      Let’s go through each part of the component:

      • The first parameter of the Vue.component() function is the component’s name, in this case rating-counter (line 7). You will be referring to this component by its name in order to render it (described in the next section: Use the Component).

      • The second argument to the Vue.component() function is the component’s options (lines 8-18). We only use two options: the data function and the template.

      • The component’s data must be a function. This is because each instance of the component must have a separate and independent copy of the data object. Otherwise, each time we reuse a component it would inherit the data from the other instances of the component.

      • The template contains the HTML this component will render:

        • VueJS uses mustache tags to render strings. In our example, {{ count }} (line 19) will render the count from the component’s data.

          What is interesting is that as VueJS is reactive, when you change your variable the view automatically updates it, and you don’t have to add any code to make this work.

        • Another thing you probably noticed in the template is the v-on:click attribute of the buttons (lines 14 and 16). Similar to jQuery’s .on(‘click’, func), you can use this feature to attach a function to the on-click event of the element.

          This can either be pointed to a function, or you can use JavaScript operators. This example uses the increment ++ and decrement -- operators directly in the attribute itself.

      At this point, we’ve built our first component, but it won’t be visible yet if you load ratingcounter.html in your browser.

      Use the Component

      Let’s try the new component out. In your ratingcounter.html, update the app div as follows:

      ratingcounter.html
      1
      2
      3
      4
      5
      
      <div id="app">
         <rating-counter></rating-counter>
         <rating-counter></rating-counter>
         <rating-counter></rating-counter>
      </div>

      This will render three rating counters which work independently from one another:

      First component

      Awesome. Let’s say however that we need to pass arguments from the parent application to the component. The option for that is called props.

      Using Component Props

      Props are VueJS’s method for adding custom properties to your component. To demonstrate this concept, let’s modify our component a little bit:

      1. In ratingcounter.html, update your Vue.component() declaration as follows:

        ratingcounter
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        
        Vue.component('rating-counter', {
            props: ['title'],
            data() {
                return {
                    count: 0
                }
            },
            template:   `<div>
                            <h1>{{ title }}</h1>
                            <button v-on:click="count--">Thumbs Down</button>
                            {{ count }}
                            <button v-on:click="count++">Thumbs Up</button>
                        </div>`
        })

        The props option has been added on line 2 of this snippet, and you can access its values from the template with the mustache syntax, just like a regular data parameter. In line 9 of this snippet, the reference to {{ title }} has been added to the template.

      2. How can you actually pass data to a prop? Just pass title as an attribute to your component’s opening tag. Whatever you pass there would be accessible by your component.

        In your ratingcounter.html, update the app div as follows:

        ratingcounter.html
        1
        2
        3
        4
        5
        
        <div id="app">
           <rating-counter title="Rating 1"></rating-counter>
           <rating-counter title="Rating 2"></rating-counter>
           <rating-counter title="Rating 3"></rating-counter>
        </div>
      3. If you reload the file in your browser, you’ll now see a title rendered for each component instance. Voila:

        Component props

      Sharing Data Between Components and the Parent App

      Just as a Vue component can keep track of data used in that component, the Vue app itself can also maintain its own data object. This section will show how props can also be used to share that data with your components.

      Replace the contents of your ratingcounter.html with the following snippet:

      ratingcounter.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
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      
      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      
      <div id="app">
         {{ parentHeader.label }}{{ parentHeader.globalCount }}
         <hr />
         <rating-counter title="Rating 1" v-bind:parent="parentHeader"></rating-counter>
         <rating-counter title="Rating 2" v-bind:parent="parentHeader"></rating-counter>
         <rating-counter title="Rating 3" v-bind:parent="parentHeader"></rating-counter>
      </div>
      
      <script>
      Vue.component('rating-counter', {
          props: ['title', 'parent'],
          data() {
              return {
                  count: 0
              }
          },
          template:   `<div>
                          <h1>{{ title }}</h1>
                          <button v-on:click="count--;parent.globalCount--;">Thumbs Down</button>
                          {{ count }}
                          <button v-on:click="count++;parent.globalCount++;">Thumbs Up</button>
                      </div>`
      })
      
      new Vue({
          el: '#app',
          data: {
              parentHeader: {
                  label: "Counter is at: ",
                  globalCount: 0
              }
          }
      })
      </script>

      Load the file in your browser and start clicking the buttons. You’ll now see a label at the top of the page that counts up the total from each of your components:

      Component with bound data properties

      Let’s break down the updated parts of the file:

      • The parent app’s data is set on lines 29-34. The app now keeps track of an object called parentHeader.

      • The data from this object is rendered on line 4.

      • On line 13, we’ve added another prop to the component, called parent.

      • On lines 6-8, the value for this prop is assigned with the v-bind:parent attribute. By using the v-bind syntax, you’re telling VueJS to bind the parent attribute of the component to whichever data property you supply, in this case the parentHeader object.

      • On lines 21 and 23, the on-click actions for each button will increment or decrement the globalCount property of the parent prop, which corresponds to the globalCount property of the parentHeader object in your app’s data.

      • Because props are reactive, changing this data from the component will cascade the changes back to the parent, and to all other components that reference it.

      Using Slots

      Slots are another very clever way to pass data from parent to components in VueJS. Instead of using attributes as you did before, you can pass data within the component’s opening and closing HTML tags. Let’s take a look at the below example:

      Replace the contents of your ratingcounter.html with the following snippet:

      ratingcounter.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
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      
      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      
      <div id="app">
         {{ parentHeader.label }}{{ parentHeader.globalCount }}
         <hr />
         <rating-counter v-bind:parent="parentHeader"><h1>Rating 1</h1></rating-counter>
         <rating-counter v-bind:parent="parentHeader"><h1>Rating 2</h1></rating-counter>
         <rating-counter v-bind:parent="parentHeader"><h1>Rating 3</h1></rating-counter>
      </div>
      
      <script>
      Vue.component('rating-counter', {
          props: ['parent'],
          data() {
              return {
                  count: 0
              }
          },
          template:   `<div>
                          <slot></slot>
                          <button v-on:click="count--;parent.globalCount--;">Thumbs Down</button>
                          {{ count }}
                          <button v-on:click="count++;parent.globalCount++;">Thumbs Up</button>
                      </div>`
      })
      
      new Vue({
          el: '#app',
          data: {
              parentHeader: {
                  label: "Counter is at: ",
                  globalCount: 0
              }
          }
      })
      </script>

      When loaded in a browser, the page should appear identical to the example in the previous section.

      On lines 6-8, notice how instead of passing the title prop with an argument, we pass it within the component’s open and close tags:

      <rating-counter v-bind:parent="parentHeader"><h1>Rating 1</h1></rating-counter>

      On line 20, the slot is referenced with the <slot></slot> syntax. As shown in this example, slots support HTML. As well, they have access to the parent’s scope (not demonstrated here), and they even support nesting more components.

      About slot scope

      Slot scope is an important concept to grasp when working with slots. Even though the content you are passing from the parent is intended for the component, you are still within the context of the parent.

      For example, trying to access the count data of the rating-counter component like this would fail:

      <rating-counter v-bind:parent="parentHeader">
          <h1>{{ count }}</h1>
      </rating-counter>

      However, as you are within the scope of the parent app, you can access the parentHeader object (or any other app data):

      <rating-counter v-bind:parent="parentHeader">
          <h1>{{ parentHeader.label }}</h1>
      </rating-counter>

      Using the parentHeader.label string here wouldn’t make much sense anyway, so this would only serve to demonstrate the scope concept.

      Nesting Slots

      The most important feature of slots might be the ability to use components within components. This is especially useful when creating structure for your apps.

      Replace the contents of your ratingcounter.html with the following snippet:

      ratingcounter.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
      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
      
      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      
      <div id="app">
          {{ parentHeader.label }}{{ parentHeader.globalCount }}
          <hr />
      
          <rating-counter v-bind:parent="parentHeader">
              <rating-title>Rating 1</rating-title>
          </rating-counter>
      
          <rating-counter v-bind:parent="parentHeader">
              <rating-title>Rating 2</rating-title>
          </rating-counter>
      
          <rating-counter v-bind:parent="parentHeader">
              <rating-title></rating-title>
          </rating-counter>
      </div>
      
      <script>
      Vue.component('rating-counter', {
          props: ['parent'],
          data() {
              return {
                  count: 0
              }
          },
          template:   `<div>
                          <slot></slot>
                          <button v-on:click="count--;parent.globalCount--;">Thumbs Down</button>
                          {{ count }}
                          <button v-on:click="count++;parent.globalCount++;">Thumbs Up</button>
                      </div>`
      })
      
      Vue.component('rating-title', {
          template:   `<div>
                          <h1>
                              <slot>Default Rating Title</slot>
                          </h1>
                      </div>`
      })
      
      new Vue({
          el: '#app',
          data: {
              parentHeader: {
                  label: "Counter is at: ",
                  globalCount: 0
              }
          }
      })
      </script>

      We’ve created another component called rating-title to illustrate slot nesting. This component will wrap a title that you set inside a pair of <h1> tags:

      Components with nested slots

      Let’s explore the code for this component:

      • The template for the new component is defined on lines 37-41. The <slot> tag has been added to this template, but this time the slot is not empty.

      • You can specify the default value for slots by adding it between the open and close slot tags: <slot>Default Rating Title</slot>

      • You can see how this default value is referenced for the new rating-title component on line 16. Whenever nothing is included between the <rating-title></rating-title> tags, the default value is used.

      • Compare this with how the component is used on lines 8 and 12. Because a title is included between the component’s tags, the default text is is not rendered.

      Named Slots

      To allow even more structure, you can use multiple slots in a template by using slot naming. Lets overengineer our simple example a little bit to see how it works.

      Replace the contents of your ratingcounter.html with the following snippet:

      ratingcounter.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
      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
      
      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      
      <div id="app">
          {{ parentHeader.label }}{{ parentHeader.globalCount }}
          <hr />
      
         <rating-counter v-bind:parent="parentHeader">
             <template v-slot:title>
                 <rating-title></rating-title>
             </template>
             <template v-slot:subtitle>
                 Subtitle
             </template>
         </rating-counter>
      </div>
      
      <script>
      Vue.component('rating-counter', {
          props: ['parent'],
          data() {
              return {
                  count: 0
              }
          },
          template:   `<div>
                          <slot name="title"></slot>
                          <h2>
                              <slot name="subtitle"></slot>
                          </h2>
                          <button v-on:click="count--;parent.globalCount--;">Thumbs Down</button>
                          {{ count }}
                          <button v-on:click="count++;parent.globalCount++;">Thumbs Up</button>
                      </div>`
      })
      
      Vue.component('rating-title', {
          template:   `<div>
                          <h1>
                              <slot>Default Rating Title</slot>
                          </h1>
                      </div>`
      })
      
      new Vue({
          el: '#app',
          data: {
              parentHeader: {
                  label: "Counter is at: ",
                  globalCount: 0
              }
          }
      })
      </script>

      When loaded in a browser, the file will look like:

      Components with named slots

      • You can see that within our rating-counter component (on lines 26-29) there are now two slots. This time they have name attributes as well: title and subtitle.

      • This takes care of the creation of the named slots, but how do we use them? You can reference a named slot by using the <template v-slot:slotname></template> syntax within your parent component. The content inside the <template> tags will then be inserted where the <slot> with the same name appears in your component’s template.

      Using Component Events

      Events in components are essential for component-to-parent communication in VueJS. Whenever a component needs to communicate back to its parent, this is the safe way of doing that.

      For an example of how this works, replace the contents of your ratingcounter.html with the following snippet:

      ratingcounter.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
      25
      26
      
      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      
      <div id="app">
          {{ parentHeader.label+" "+parentHeader.globalCount }}
          <hr />
      
          <rating-counter v-bind:parent="parentHeader" v-on:increment="parentHeader.globalCount++"></rating-counter>
      </div>
      
      <script>
      Vue.component('rating-counter', {
          template:   `<div>
                          <br><button v-on:click="$emit('increment');">Thumbs Up</button>
                      </div>`
      })
      
      new Vue({
          el: '#app',
          data: {
              parentHeader: {
                  label: "Counter is at: ",
                  globalCount: 0
              }
          }
      })
      </script>

      When loaded in a browser, the file will look like:

      Component with an event

      The example is similar to the previous examples, but we’ve only left what is essential to the events:

      • In the v-on:click event of our component (line 13), you can see that this time we’re using VueJS’s $emit method instead of changing variables manually. The $emit() method takes a custom event name as an argument (increment in this example).

      • This fires an empty increment event, which our parent is subscribed to on line 7. Notice the v-on:increment="parentHeader.globalCount++" attribute; this is our event subscriber.

      • The subscriber can call a method of the VueJS app, or in this example, just directly use the increment JavaScript ++ operator to increase the counter.

      • Because the component no longer directly manipulates the parent’s globalCount data, the parent prop for the component can be removed.

      Passing a Parameter with Events

      How about if you want to pass a parameter with the event? We’ve got you covered.

      For an example of how this works, replace the contents of your ratingcounter.html with the following snippet:

      ratingcounter.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
      25
      26
      27
      28
      
      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      
      <div id="app">
          {{ parentHeader.label+" "+parentHeader.globalCount }}
          <hr />
      
          <rating-counter v-bind:parent="parentHeader" v-on:increment="parentHeader.globalCount+=$event"></rating-counter>
      </div>
      
      <script>
      Vue.component('rating-counter', {
          template:   `<div>
                          <button v-on:click="$emit('increment', -1);">Thumbs Down</button>
                          <button v-on:click="$emit('increment', 1);">Thumbs Up</button>
                      </div>`,
      
      })
      
      new Vue({
          el: '#app',
          data: {
              parentHeader: {
                  label: "Counter is at: ",
                  globalCount: 0
              }
          }
      })
      </script>

      When loaded in a browser, the file will look like:

      Component with an event and parameter

      • This example introduces a second parameter of the $emit() function (lines 13-14).

      • Instead of simply incrementing by one, our parent event subscriber can make use of this argument with the $event variable (line 7).

      • The template for the component now has a Thumbs Down button again, and the argument for this button’s event is -1 (line 13).

      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