Real World Vue 3: Single File Components

·

11 min read

Now that we’ve created our project with the Vue CLI, we’re ready to start customizing it to build our own app.

If you’re coding along (which I encourage you to do) you’ll want to checkout the L3-start branch of our project repo to grab the starting code (L3 stands for Lesson 3). In that code, I want to bring your attention to this file I’ve added:

📄prettierrc.js

module.exports = {
  singleQuote: true,
  semi: false
}

Here, I’ve set up some rules so that Prettier will change any double quotes (") to single ones (') and remove any semicolons (;). I’m not advocating for or against semicolons and double quotes. This is a simple example of how you might add some Prettier configuration rules to your project. We could do something similar for ESLint as well. For a more in-depth look at how you can configure ESLint + Prettier as well as get the most out of VS Code, you can check out this article.

What are these .vue files?

In order to start building our app, we need to get some foundational understanding of how things are working within the demo app the CLI created for us, including the views directory, which includes two single-file .vue files: Home.vue and About.vue

These are the components that Vue Router loads up when we navigate to the Home and About routes, respectively.

image.png

In the next lesson, we’ll explore the essentials of Vue Router, but for now you just need to understand that these “view” components are the different views that can be seen (or navigated to) within our app. They can contain child components that are nested within them, and their children will be displayed in that view as well. For example, the Home.vue component has a child: HelloWorld.vue, which has a bunch of template code that is being displayed when we’re on the Home route.

Each of these .vue files are single file components, and that’s what this lesson is exploring: How are single file components composed, and how do you use them to create a Vue app?

Anatomy of a Single File Component

When we’re talking about Vue apps, we’re really talking about a collection of Vue components.

gif link

image.png

So what do these single file components look like under the hood?

image.png

A typical .vue component has three sections: <template>, <script>, and <style>.

To use the analogy of a human body, you can think of the template as the skeleton of your component since it gives it structure, and the script section is the brains, providing the intelligence and behavior. The style section is exactly what it sounds like: the clothing, makeup, hairstyle, etc.

Traditionally, these sections are written in HTML, JavaScript and CSS. However, with the proper setup, you could also use alternatives such as Pug, TypeScript, and SCSS.

Now that we’re starting to understand single file components, we can start building our own. But first, what are we building in this course, exactly?

The app we’re building

By the end of this course, we will have built an app that display events.

image.png

The events will be pulled in from an external API call, and displayed on the Home page. We’ll be able to click on the event to see the event details.

image.png

Our first Single File Component

To get started building our first component, we’ll simply delete out the code that is within the <template>, <script>, and <style> sections of HelloWorld.vue. While we’re at it, let’s rename this file to EventCard.vue, since it’s the card that displays info for each event.

image.png

📁src/components/EventCard.vue

<template>
  <div class="hello">
  </div>
</template>

<script>
export default {
  name: 'EventCard'
  // props: {
  //  msg: String
  // }
}
</script>

<style scoped>
</style>

Now that this file is cleared out, we can add our own code to it. First, let’s add some styles. We’ll change the class name on the div in order to do that.

📁src/components/EventCard.vue

<template>
  <div class="event-card">
  </div>
</template>

<script>
export default {
  name: 'EventCard'
  // props: {
  //  msg: String
  // }
}
</script>

<style scoped>
.event-card {
  padding: 20px;
  width: 250px;
  cursor: pointer;
  border: 1px solid #39495c;
  margin-bottom: 18px;
}

.event-card:hover {
  transform: scale(1.01);
  box-shadow: 0 3px 12px 0 rgba(0, 0, 0, 0.2);
}
</style>

Now the div has the proper styles, including a hover effect. If you’re wondering what that scoped attribute means, that allows us to scope and isolate these styles to just this component. This way, these styles are specific to this component and won’t affect any other part of our application. You’ll see me using scoped styles throughout this course.

Since we want to display information about the event on this EventCard, we need to give it an event to display. So let’s add that in the data option of our <script> section.

📁src/components/EventCard.vue

<script>
export default {
  name: 'EventCard'
  // props: {
  //  msg: String
  // },
  data() {
    return {
      event: {
          id: 5928101,
          category: 'animal welfare',
          title: 'Cat Adoption Day',
          description: 'Find your new feline friend at this event.',
          location: 'Meow Town',
          date: 'January 28, 2022',
          time: '12:00',
          petsAllowed: true,
          organizer: 'Kat Laydee'
       }
     }
   }
}
</script>

Now, in the <template> we can display some of that event data with JavaScript expressions, like so:

📁src/components/EventCard.vue

<template>
  <div class="event-card">
    <span>@{{ event.time }} on {{ event.date }}</span>
    <h4>{{ event.title }}</h4>
  </div>
</template>

That’s it for the component for now.

In order for this EventCard to be displayed, it needs to be put somewhere that can be routed to, such as the Home.vue file in our views directory. Just like with the HelloWorld.vue file, we’ll need to import EventCard.vue, register it as a child component, and then we can use it in the template.

📁src/views/Home.vue

<template>
  <div class="home">
    <EventCard />
  </div>
</template>

<script>
// @ is an alias to /src
import EventCard from '@/components/EventCard.vue'

export default {
  name: 'Home',
  components: {
    EventCard // register it as a child component
  }
}
</script>

Now, we should be seeing our EventCard showing up in the browser when we’re on the Home view.

image.png

Refactoring for a more production-ready use case

We’re making great progress, but remember we want the EventCard to be displaying in the middle of the Home page. And, since we’ll eventually have a collection of events that we pull in from an API call, we need to do a bit of refactoring to make this a more production-ready use case.

Our refactoring steps include:

  • Move events data to parent (Home.vue)
  • Parent creates EventCard component for each event in its data
  • Parent feeds each EventCard its own event to display
  • Parent displays EventCards in a Flexbox container

Let’s get started with this refactor.

Move events data to parent

Our first step is to delete out the event data from EventCard. We’ll then add an event prop instead, so that the parent can feed this component an event object to display. We’re then left with this code:

📁src/components/EventCard.vue

<template>
  <div class="event-card">
    <span>@{{ event.time }} on {{ event.date }}</span>
    <h4>{{ event.title }}</h4>
  </div>
</template>

<script>
export default {
  props: {
    event: {
      type: Object,
      required: true
    }
  }
}
</script>

<style scoped>
.event-card {
  padding: 20px;
  width: 250px;
  cursor: pointer;
  border: 1px solid #39495c;
  margin-bottom: 18px;
}

.event-card:hover {
  transform: scale(1.01);
  box-shadow: 0 3px 12px 0 rgba(0, 0, 0, 0.2);
}
</style>

Now that EventCard is set up to receive an event, we can add the events data to the parent, Home.vue.

📁src/views/Home.vue

<template>
  <div class="home">
    <EventCard />
  </div>
</template>

<script>
// @ is an alias to /src
import EventCard from '@/components/EventCard.vue'

export default {
  name: 'Home',
  components: {
    EventCard
  },
  data() {
    return {
      events: [
        {
          id: 5928101,
          category: 'animal welfare',
          title: 'Cat Adoption Day',
          description: 'Find your new feline friend at this event.',
          location: 'Meow Town',
          date: 'January 28, 2022',
          time: '12:00',
          petsAllowed: true,
          organizer: 'Kat Laydee'
        },
        {
          id: 4582797,
          category: 'food',
          title: 'Community Gardening',
          description: 'Join us as we tend to the community edible plants.',
          location: 'Flora City',
          date: 'March 14, 2022',
          time: '10:00',
          petsAllowed: true,
          organizer: 'Fern Pollin'
        },
        {
          id: 8419988,
          category: 'sustainability',
          title: 'Beach Cleanup',
          description: 'Help pick up trash along the shore.',
          location: 'Playa Del Carmen',
          date: 'July 22, 2022',
          time: '11:00',
          petsAllowed: false,
          organizer: 'Carey Wales'
        }
      ]
    }
  }
}
</script>

Parent creates EventCard components

Now that Home.vue has the events data, we can use that data to create a new EventCard for each of the event objects that are in that data, using the v-for directive.

📁src/views/Home.vue

<template>
  <div class="home">
    <EventCard v-for="event in events" :key="event.id" :event="event" />
  </div>
</template>

<script>
// @ is an alias to /src
import EventCard from '@/components/EventCard.vue'

export default {
  name: 'Home',
  components: {
    EventCard
  },
  data() {
    return {
      events: [
        {
          id: 5928101,
          category: 'animal welfare',
          title: 'Cat Adoption Day',
          description: 'Find your new feline friend at this event.',
          location: 'Meow Town',
          date: 'January 28, 2022',
          time: '12:00',
          petsAllowed: true,
          organizer: 'Kat Laydee'
        },
        {
          id: 4582797,
          category: 'food',
          title: 'Community Gardening',
          description: 'Join us as we tend to the community edible plants.',
          location: 'Flora City',
          date: 'March 14, 2022',
          time: '10:00',
          petsAllowed: true,
          organizer: 'Fern Pollin'
        },
        {
          id: 8419988,
          category: 'sustainability',
          title: 'Beach Cleanup',
          description: 'Help pick up trash along the shore.',
          location: 'Playa Del Carmen',
          date: 'July 22, 2022',
          time: '11:00',
          petsAllowed: false,
          organizer: 'Carey Wales'
        }
      ]
    }
  }
}
</script>

Notice how we’re binding the event’s id to the :key attribute. This gives Vue.js a way to identify and can keep track of each unique EventCard.

Parent feeds each EventCard its own event

Additionally, as we iterate over the events array to create a new EventCard for each event object, we’re passing in that event object into a new :event prop we’ve added to the EventCard. This way, each EventCard has all of the data it needs to display its own event info.

Parent displays EventCards in a Flexbox container

If we check this out in the browser, it’s working. We’ve created an EventCard for each of the events in Home.vue’s data.

image.png

Finally, we just need to put these events in a Flexbox container to get things looking how we want. Let’s head into the Home.vue file and change the class name of the div that our EventCard is nested within, and add some Flexbox styles.

📁src/views/Home.vue

<template>
  <h1>Events For Good</h1>
  <div class="events">
    <EventCard v-for="event in events" :key="event.id" :event="event" />
  </div>
</template>

<script>
...
</script>

<style scoped>
.events {
  display: flex;
  flex-direction: column;
  align-items: center;
}
</style>

Now, our EventCards will be displayed within a center-aligned column.

What about Global Styles?

So far, we’ve discussed scoped styles and how the scoped attribute allows us to add styles that target the specific component we’re concerned about. But what about global styles that we want applied to our entire app? While there are different ways to achieve this, the simplest way to get started with this is by heading into the App.vue file. Remember: this is the root component of our app.

Notice that there are some styles rules that the CLI set up for us in this component’s <style> section.

📁src/App.vue

<template>
...
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
</style>

These are global styles that are applied to the entire app. Here, we could add a new rule. Like so:

📁src/App.vue

<style>
...
h4 {
  font-size: 20px;
}
</style>

Now, any h4 in our app will have a font-size of 20px. Since our EventCard’s template has an h4, that element will receive this new global style.

📁src/components/EventCard.vue

<div class="event-card">
  <span>@{{ event.time }} on {{ event.date }}</span>
  <h4>{{ event.title }}</h4>
</div>

Speaking of global items in our Vue app, what would happen if we added something like an h1 to our App.vue’s template?

📁src/App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <h1>Events For Good</h1> <!-- new element -->
    <router-view />
  </div>
</template>

Let’s head into the browser and take a look.

image.png

We’re seeing a few things. First, our Flexbox container is working (✅) and the event titles are now just a bit bigger (20px) due to that new global h4 style rule we added. And notice what happens when we navigate to the About route.

image.png

We’re still seeing that h1 displaying “Events For Good”. So this tells us that we can place content in our App.vue’s template that we want to be displayed globally across every view of our application. This could be useful for things like a search bar, header, or of course a nav bar like we already have here.

But for our use case, we don’t need that title showing up in every view, so we’ll place it into the Home.vue file.

📁src/views/Home.vue

<template>
  <h1>Events For Good</h1>
  <div class="events">
    <EventCard v-for="event in events" :key="event.id" :event="event" />
  </div>
</template>
...

Now that title will only show up one the Home route.

Let’s ReVue

We’ve covered a lot. We learned what a single file .vue component is, how it’s composed (with scoped versus global styles), and how to start using these .vue components to build up a Vue app. In the next lesson, we’re going to dive deeper into the essentials of Vue Router to better understand how to set up app navigation. See you there!