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:
- 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.
- 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 parentApp.vue
component. It makes sense that this method would now belong within ourWordCard
component because it’s central to the functionality of that component. - Within
checkAnswer
, we see how we can emit a custom event calledincrementCorrectCount
to the parent component. In the next section we’ll see how to amendApp.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 withinWordCard.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 thev-bind
directive. - Recall that WordCount emits a custom event
incrementCorrectCount
which we listen for here via thev-on
directive. In response to this event, we invoke a method calledincrementCorrectCount
which you should add to your App methods, as shown in the following code. While you’re updating methods, you can also delete thecheckAnswer
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