← Other topics

Vue.js Simplified - Single File Components (#13)

Series contents:

Video Notes

In Part 12 of this series, we saw our first example of a Vue Single File Component (SFC) via the file src/App.vue.

The idea with Single File Components is to organize together all the JavaScript, HTML, and CSS of a specific entity or component of your application into one file. Within the JavaScript, you will define all the Vue options (data, computed properties, methods, watchers, etc.) necessary for that component, and each component will be set up as its own Vue instance.

Here’s a diagram for a generic site so you can visualize how things might be broken down into components:

Two key benefits of SFCs include:

  1. Ease of development - rather than hunting around three separate files (JS, HTML, CSS) to manage a specific aspect of your application, you can have everything organized together into one central location.
  2. Reusability - Single File Components can be imported and reused as needed throughout your application. We can see this imagined in the above diagram with the repeating of the NotificationItem and SearchResult components.

Creating a WordCard component for FlashWord

To practice with components, let’s adapt our FlashWord code so that each word card on the page is built via a component:

To do this, create a new file Single File Component /src/components/WordCard.vue with the following code:

<script>
export default {
    name: 'WordCard',
    props: ['word'],
    methods: {
        checkAnswer() {
            // When referencing props, prefix with the `this` keyword
            this.word.correct = this.word.word_b == this.word.answer;

            if (this.word.correct) {
                // Emit the custom event `incrementCorrectCount` to the parent component that is utilizing this component
                this.$emit('incrementCorrectCount');
            }
        }
    }
}
</script>

<template>
    <div class='card' v-bind:class='{ correct: word.correct}'>
        <p class='word'>{{ word.word_a }}</p>
        <input
            v-if='!word.correct' 
            type='text' 
            v-model='word.answer' 
            v-on:keyup.enter='checkAnswer()'>

        <p v-else class='correctAnswer'>{{ word.answer }}</p>
    </div>
</template>

<style scoped>

/* The scoped attribute means that these styles will only apply to elements in this component */

.card {
    background-color: #E8F0FF;
    border-radius: 5px;
    padding: 10px 0;
    font-size: 25px;
}

input[type=text] {
    border: 0;
    font-size: 25px;
    border-radius: 5px;
    margin-top: 5px;
    text-align: center;
    padding: 5px;
}

.word {
    font-weight: bold;
    padding: 0;
    margin: 0;
}

.correctAnswer {
    padding: 0;
    margin: 0;
}

.correct {
    color: #0f5132;
    background-color: #d1e7dd;
}

</style>

Here are the important things to observe about the above code:

  • Each component has its own set of Vue options that can contain data properties, computed properties, watchers, etc.
  • The name option is introduced to name the component. When we go to use this component, we’ll reference this name. By convention, component names should be written in PascalCase and contain at least two words to distinguish them from the default single-word HTML element names (ref).
  • The props option defines any data that we expect to be passed to the component when it’s used, allowing it to be customized. In this case, the customization we need is for each WordCard to work with a different word, so we define a single prop, word.
  • The method checkAnswer was extracted and adapted from our parent App.vue component. It makes sense that this method would now belong within our WordCard component because it’s central to the functionality of that component.
  • Within checkAnswer, we see how we can emit a custom event called incrementCorrectCount to the parent component. In the next section we’ll see how to amend App.vue to handle this event.
  • Any of the card-specific CSS was extracted from our global styles in App.vue into the <style scoped> element within WordCard.vue. The scoped attribute indicates that these styles should only apply to elements within this component.

Key points

  • Props are how we can pass data ”down” from parent to child components
  • Events are how we can communicate information ”up” from child to parent components

Utilizing the WordCard component

With everything in WordCard set up, let’s turn our attention to App to utilize this component.

First, update the JavaScript in src/App.vue to import the new WordCard component:

import WordCard from './components/WordCard.vue'

And update the options to include a new component option that registers the WordCard component:

export default  {
    /* Register any components we will use in this component */
    components: {
        WordCard
    },
    /* The following options were abbreviated for brevity */
    data() {return {} },
    computed: {},
    watch: {},
    methods: {}
}

Additionally, update the App template to utilize the WordCard component within div#cards:

<div id='cards'>
    <WordCard
      v-for='word in shuffledWords'
      v-bind:word='word' 
      v-on:incrementCorrectCount="incrementCorrectCount"></WordCard>
</div>

Observations about the above code:

  • The way we use a component is to write it using tag style just like any other HTML element: <WordCount></WordCount>.
  • Recall that WordCount has a prop called word, which we pass in via the v-bind directive.
  • Recall that WordCount emits a custom event incrementCorrectCount which we listen for here via the v-on directive. In response to this event, we invoke a method called incrementCorrectCount which you should add to your App methods, as shown in the following code. While you’re updating methods, you can also delete the checkAnswer method which is now handled in the WordCount component.
export default  {
    components: {
        WordCard
    },
    data() { return { /* Redacted for brevity */ } },
    computed: {  /* Redacted for brevity */ },
    watch: {  /* Redacted for brevity */ },
    methods: {
        incrementCorrectCount() {
            this.correctCount++;
        }
    }
}

Test it out

Load your application in the browser and test it out. It should work exactly as it did before, as we haven’t changed any of this apps functionality - only how it was built behind the scenes.

Concluding notes

  • Single File Components require build systems
  • Separation of Concerns by feature rather than technology
  • What’s next: Composition API
If this info helped you out, toss a coin in the tip jar?
← Other topics