VueJs Components: Reusing Code in Vue

Created on April 10, 2022.


When listing items inside the Vue template, there are two ways to go about it. We can list the items, one after the other, using an unordered list.

Let’s list numbers from 1 to 3.

<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

What happens when we need to list numbers from 1 to 100. Well, the above method works but it would be a time-consuming task, and as we know, the idea of writing a program is to make something less work. Thus, in such a case, we’ll opt for list rendering using v-for.

<ul>
<li v-for="(num, key) in 100)" :key="key"> {{ num }}</li>
</ul>

What about when we are dealing with HTML blocks that have multiple other HTML blocks nested within, constructing beautifully crafted product listings in e-commerce or to-do list apps?

Well, we could use v-for on a single card that can be rendered repeatedly formulating a list. But still, we’ll have the problem of cluttering both our templates and our Vue controller logic, especially when these blocks need methods, watchers, computed properties, and states of their own.

As the app keeps on growing we’ll find our web application hard to maintain and prone to bugs, so how do we fix this?

Components

Vue gives us a solution to this with components. They follow the same logic you will find in most programming languages. It is like breaking down a long JavaScript source code into function blocks to avoid repetition and maintain clean code but on steroids.

Components have the same structure as the examples that we have been working with up to this moment, i.e. a template part containing HTML markup and a controller part containing the Vue app’s logic. The structure we’ve been using in all previous examples is itself a Vue component; we can refer to it as the root component.

Let’s see how to create a Vue component using the above listing example.

Practice this code!
<html>
  <div id="app">
    <ul>
      <list-item v-for="(num, key) in 100" :key="key" :item-name="num" ></list-item>
    </ul>
  </div>

  <script src="https://unpkg.com/[email protected]"></script>
  <script>
    let app = Vue.createApp({});
    app.component('list-item', {
      template: `<li> {{ itemName }} </li>`,
      props: ['itemName']
    })
    app.mount("#app");
  
</script>
</html>

Component Props

From the above example, we see that to create a Vue component, we need to call the app.component() function then pass the component name and an Object containing the template markup inside template literals together with the other Vue instance options.

Besides the Vue structure that we are familiar with, there is a new props option. The function of props is to enable us to pass data from a parent into a child component. When a value passed as a prop is updated in the parent, the changes are reflected inside the child component.

Parent components are able to pass data into props through component attributes. In the above example, we can see this in :item="num"; Attribute names have to reflect their corresponding prop names. For longer variable names, it is considered good practice to use the kebab-case naming convention on component attributes while using the camelCase naming convention on props.

Passing Props as an Array

One way props can be added inside the child component is by listing them as an Array of Strings. Afterward, they can be accessed inside the component as any other reactive data. We have seen this already in the example above.

Passing Props as an Object

Listing props as an Array give us less control and the ability to filter what kind of data a prop ought to be. We just assume that the provided data is of the type and conditions that we desire. For simple data expecting empty or non-empty Strings, that can be enough. But for complex cases where we need strict variable types and conditions, Arrays can not be enough.

To have more control over data being passed as props, we can declare them as objects. With Objects, Vue allows us to define the type, validator and default values for passed prop data.

Let’s see an example.

Practice this code!
<html>
  <div id="app">
    <ul>
      <list-item v-for="(craft, key) of aircraft" :key="key" :name="craft.name" :flight-hours="craft.flightHours" retired ></list-item>
    </ul>
  </div>

  <script src="https://unpkg.com/[email protected]"></script>
  <script>
    let app = Vue.createApp({
      data(){
        return {
          aircraft: [{
            // name: "Airbus A380",
            flightHours: 0,
            retired: false
          },{
            name: "Boeing 747",
            flightHours: "74000",
            // retired: true
          }]
        }
      }
    });
    app.component('list-item', {
      template: `<li> {{ name }}: Flown {{ flightHours }} hours{{ isRetired }} </li>`,
      props: {
        name: {
          type: String,
          default: 'Unknown Aircraft',
        },
        flightHours: {
          type: Number,
          default: 1,
          validator: (val) => val >= 1
        },
        retired: {
          type: Boolean
        }
      },
      computed: {
        isRetired(){
          return this.retired ? ", Already Retired." : ", Still in service."
        }
      },
      // mounted(){
      //   setTimeout(() => { this.flightHours += 1; }, 3000)
      // }
    })
    app.mount("#app");
  
</script>
</html>

This example resolves to the following list.

Unknown Aircraft: Flown 0 hours, Already Retired.
Boeing 747: Flown 74000 hours, Already Retired.
Type Checks and Default Values

The name prop checks if the provided variable is of the String type, if not, it defaults to “Unknown Aircraft”; This is true to the first item as seen on the resolved list since the first Object on the parent’s aircraft Array has been commented. Uncomment it to fix this. Refresh the page to proceed.

If you open your browser’s console log, you will see a warning citing Invalid prop: type check failed for prop "flightHours". Expected Number with value 74000, got String with value "74000". Just as the warning narrates, we have provided a String type in the second item of the aircraft Array inside the parent component, leading to this warning. Change that String to a Number to clear this warning. Refresh the page to proceed.

Prop Validation Function

Again, looking at your browser’s console log, you will see a warning - custom validator check failed for prop "flightHours". This is because on the flightHours prop, we’ve set a validator to check that the value passed should be a Number greater than one, but observing the parent component, the first aircraft Array item is passing a flightHours property with the value 0. Fix that by replacing the 0 with a desirable Number to silence the warning.

Boolean Props

With all the warnings cleared, still, both the items in our list display “Already Retired” despite the first item passing retired explicitly as false and the second not passing it at all.

This is because, while looping through the aircraft Array, we have passed retired as a flat attribute in all our components. Props of the type Boolean are treated differently compared to those of other types inside components. If we pass a flat prop name of the type Boolean or bind it with an expression that is truthy, it always resolves to true inside the component, just as is the case with the disabled attribute in HTML elements. To get a Boolean variable to resolve to false inside the child component, we need to explicitly pass a false value or not include the attribute.

So, to fix this issue in our example, replace the flat retired attribute with :retired="craft.retired", proceed by uncommenting the retired property in the second aircraft Array item.

Updating Props

Props are read-only, modifying them will throw a warning. Try uncommenting the code block involving the mounted hook inside the list-item component and refresh the page afterward.

After three seconds, you will see warnings thrown on the browser’s console log. This is because we are attempting to mutate the value of flightHours inside the list-item component.

If you want to modify how a prop is presented inside the component, use a computed property to do that, just as we employed the isRetired property in our example.

The $emit Function

If we need to change data inside the parent from a child component, Vue provides the $emit function that enables us to do just that. The $emit function enables us to emit custom events that can be listened to on the parent side containing the component element by using the v-on (@) directive.

Practice this code!
<html>
  <div id="app">
    <h3>Top Podcasts</h3>
    <ol>
      <list-item v-for="(podcast, key) of podcasts" :key="key" :name="podcast.podcast" :votes="podcast.votes" @up-vote="podcast.votes += 1" ></list-item>
    </ol>
  </div>

  <script src="https://unpkg.com/[email protected]"></script>
  <script>
    let app = Vue.createApp({
      data(){
        return {
          podcasts: [{
            votes: 1,
            podcast: "The Vuenoob Podcast"
          },{
            votes: 2,
            podcast: "Not So Important"
          }]
        }
      },
      watch: {
        podcasts: {
          handler(){
            this.podcasts.sort((a, b) => b.votes - a.votes);
          },
          deep: true,
          immediate: true
        }
      }
    });
    app.component('list-item', {
      template: `<li> {{ name }} ({{ votes }} votes) <button @click="$emit('up-vote')">Vote</button></li>`,
      props: {
        name: {
          type: String,
          default: 'Unknown Podcast',
        },
        votes: {
          type: Number,
          default: 1,
          validator: (val) => val >= 0
        }
      }
    })
    app.mount("#app");
  
</script>
</html>

On top of passing a custom event’s name to be listened to as demonstrated in the above example, the $emit function lets us pass event arguments that relatively get passed on as arguments to the functions resolving the emitted event.

For example, if we wanted a voter to be able to pass a specific Number of votes to their choice, we would need to pass the Number of votes as the second argument of the $emit function. Update our example above by updating the parent and child component as follows.

Modify the parent component of the previous code block to reflect this code!
<!-- Parent Component -->
  <list-item v-for="(podcast, key) of podcasts" :key="key" :name="podcast.podcast" :votes="podcast.votes" @up-vote="(votesCount) => podcast.votes += votesCount" ></list-item>

 

Modify parts of the child component of the previous code block to reflect this code!
// Child Component

// Update the template to
{
  template: `<li> {{ name }} ({{ votes }} votes) <span><input type="number" v-model.number="voteCount"><button @click="$emit('up-vote', voteCount)">Vote</button></span></li>`,
  // Add data property
  data(){
    return {
      voteCount: 1
    }
  }
})

As demonstrated in our example, always use the kebab-case naming convention if your custom event name contains more than one word.

Using the $emit function is another way to update data without straightly mutating it inside the component.

Previous

VueJs Lifecycle Hooks: A Look Into the Lifecycle of a Vue App

Subscribe to the VueNoob Newsletter

[[[ noty.message ]]]

By using this site, you agree that you have read and understand its Privacy Policy.