Componentes
Conceitos
Os componentes são instâncias reutilizáveis de VueJS que podem ser criadas e utilizadas em qualquer parte da aplicação. Eles são compostos por um template, que define a estrutura do componente, e por uma instância Vue, que define o comportamento do componente. Os componentes são uma das principais características do VueJS e são muito úteis para organizar e reutilizar o código de uma aplicação. Ainda, com o uso de componentes, é possível dividir a interface de usuário em partes menores e independentes, o que facilita a manutenção e a evolução da aplicação.
No VueJS, em geral, usamos o conceito de SFC (Single File Component) para criar componentes. Um SFC é um arquivo que contém o template, a lógica e o estilo de um componente em um único lugar, como já temos visto em aulas anteriores.
Durante esta aula, vamos estruturar o projeto da livraria com o uso de componentes, criando componentes para as diversas partes da aplicação, como o cabeçalho, o rodapé e a lista de livros.
Alguns conceitos de reutilização de componentes farão mais sentido quando estudarmos o conceitos de rotas, com o Vue Router. Isso porque a reutilização de componentes é muito útil quando se trabalha com rotas dinâmicas e a criação de páginas com elementos comuns.
Criação de um componente simples
Para criar um componente, basta criar um arquivo com a extensão .vue e definir o template, a lógica e o estilo do componente. O VueJS já vem com uma estrutura básica de um SFC, que pode ser utilizada como base para criar novos componentes. De certa forma, o conceito básico de um componente já foi amplamente utilizado no arquivo App.vue, que é o ponto de entrada da aplicação. O arquivo App.vue é um SFC que contém o template, a lógica e o estilo da aplicação. A partir dele, podemos criar novos componentes e utilizá-los na aplicação.
Componente de rodapé
Vamos primeiro criar um component para o rodapé da aplicação. Da forma que está, o rodapé da aplicação é apenas um texto fixo. Vamos criar um componente para o rodapé e utilizá-lo na aplicação. Para isso, vamos criar um arquivo chamado FooterComponent.vue na pasta src/components. O conteúdo do arquivo FooterComponent.vue será o seguinte:
./src/components/FooterComponent.vue<template>
<footer>
<nav>
<section class="upper-footer">
<div>
<p class="footer-title">
<a href="#"> IFbooks </a>
</p>
<ul>
<li><span class="mdi mdi-facebook" /></li>
<li><span class="mdi mdi-instagram" /></li>
<li><span class="mdi mdi-twitter" /></li>
</ul>
</div>
<div>
<p class="contact-text">Contato</p>
<p><span class="mdi mdi-phone" /> +55 47 99999-9999</p>
<p><span class="mdi mdi-clock" /> 8h às 23h - Seg a Sex</p>
<p><span class="mdi mdi-email" /> contato@ifbooks.com.br</p>
<div class="payment-methods">
<span class="mdi mdi-visa" />
</div>
</div>
</section>
<section class="lower-footer">
<p>© Alguns direitos reservados. IFBooks. 2025</p>
</section>
</nav>
</footer>
</template>
<style scoped>
footer {
background-color: #27ae60;
padding: 2vh 8vw;
color: #fff;
.upper-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 100px;
border-bottom: 2px solid #fff;
& div {
display: flex;
flex-direction: column;
gap: 10px;
& .footer-title {
font-size: 1.2rem;
font-weight: 700;
& a {
text-decoration: none;
color: #fff;
}
}
& .contact-text {
font-size: 1.1rem;
font-weight: 700;
}
}
}
.lower-footer {
display: flex;
justify-content: center;
padding-top: 20px;
font-size: 0.8rem;
}
ul {
display: flex;
padding: 0;
gap: 20px;
li {
list-style: none;
font-size: 1.2rem;
color: #fff;
}
}
}
</style>
Note que esses dois blocos de código foram copiados do arquivo App.vue, que é o ponto de entrada da aplicação.
Agora, vamos importar o componente FooterComponent no arquivo App.vue e utilizá-lo no template. Para isso, vamos adicionar o seguinte código ao arquivo App.vue:
./src/App.vue<script setup>
import { ref } from 'vue';
import FooterComponent from './components/FooterComponent.vue';
...
</script>
Também será necessário editar o bloco de template do arquivo App.vue para incluir o componente FooterComponent. Para isso, vamos substituir o código do rodapé pelo seguinte:
| ./src/App.vue |
|---|
| </main>
<footer-component />
</template>
|
A versão completa do arquivo App.vue está no bloco abaixo.

Versão completa
Note que, além das linhas destacadas, foi retirado o código da tag <footer> do arquivo App.vue. Também, foi removida a parte correspondente no bloco de <style>.
| ./src/App.vue |
|---|
| <script setup>
import { ref } from 'vue'
import FooterComponent from './components/FooterComponent.vue';
const showCart = ref(false)
const cart = ref({
items: [],
total: 0,
})
const books = [
{
id: 1,
title: 'Comigo na livraria',
cover: '/covers/comigo-na-livraria.png',
price: 23.24,
author: 'Martha Medeiros',
},
{
id: 2,
title: 'Quincas Borba',
cover: '/covers/quincas-borba.png',
price: 23.24,
author: 'Machado de Assis',
},
{
id: 3,
title: 'A livraria',
cover: '/covers/a-livraria.png',
price: 13.94,
author: 'Penelope Fitzgerald',
},
{
id: 4,
title: 'A hora da estrela',
cover: '/covers/a-hora-da-estrela.png',
price: 16.84,
author: 'Clarice Lispector',
},
{
id: 5,
title: 'O alienista',
cover: '/covers/o-alienista.png',
price: 266.92,
author: 'Machado de Assis',
},
{
id: 6,
title: 'Mar morto',
cover: '/covers/mar-morto.png',
price: 13.95,
author: 'Jorge Amado',
},
{
id: 7,
title: 'Grande sertão',
cover: '/covers/grande-sertao-veredas.png',
price: 26.04,
author: 'Guimarães Rosa',
},
{
id: 8,
title: 'Flor de poemas',
cover: '/covers/flor-de-poema.png',
price: 15.81,
author: 'Cecília Meireles',
},
]
</script>
<template>
<header>
<nav>
<h1>
<a href="#">
IFbooks
<span class="logo-title"> Apreço a livros </span>
</a>
</h1>
<div class="search-wrapper">
<input type="text" class="search" placeholder="Buscar..." />
</div>
<ul>
<li>Termos</li>
<li>Equipe</li>
<li>Envio</li>
<li>Devoluções</li>
</ul>
<ul class="icons">
<li @click="showCart = !showCart"><span class="mdi mdi-cart"></span></li>
<li><span class="mdi mdi-heart"></span></li>
<li><span class="mdi mdi-account"></span></li>
</ul>
</nav>
</header>
<main v-if="showCart">
<section class="cart">
<h2>Carrinho</h2>
<table>
<thead>
<tr>
<th>Título</th>
<th>Quantidade</th>
<th>Subtotal</th>
</tr>
</thead>
<tbody>
<tr v-for="book in cart.items" :key="book.id">
<td class="cart-item">
<img :src="book.cover" :alt="book.title" />
<div class="cart-item-info">
<p class="cart-item-title">{{ book.title }}</p>
<p class="cart-item-author">{{ book.author }}</p>
<p class="cart-item-price">R$ {{ book.price.toFixed(2) }}</p>
</div>
</td>
<td>
<div class="cart-item-quantity">
<button class="plain">
<span class="mdi mdi-minus" />
</button>
{{ book.quantity }}
<button class="plain">
<span class="mdi mdi-plus" />
</button>
</div>
</td>
<td class="cart-item-subtotal">R$ {{ book.price * book.quantity }}</td>
</tr>
</tbody>
</table>
<button @click="showCart = false" class="outlined">Voltar para loja</button>
<div class="cart-summary">
<div class="cupom">
<input type="text" placeholder="Código do cupom" />
<button>Inserir cupom</button>
</div>
<div class="summary">
<h2>Total da Compra</h2>
<div class="summary-items">
<span>Produtos</span> <span>R$ {{ cart.total.toFixed(2) }}</span> <span>Frete</span>
<span> Grátis</span> <span>Total</span> <span>R$ {{ cart.total.toFixed(2) }}</span>
</div>
<button>Ir para pagamento</button>
</div>
</div>
</section>
</main>
<main v-else>
<section class="hero">
<div class="hero-content">
<h3 class="outlined">Livro destaque</h3>
<h2>Noc Ognia</h2>
<p>
Noc ognia é um romance de Erich-Emmanuel Schmitt, que narra a história de um homem que
vive em um mundo onde as pessoas não podem mais sonhar. O livro é uma reflexão sobre a
importância dos sonhos e da imaginação na vida humana. Erich-Emmanuel Schmitt é um autor
francês conhecido por suas obras que exploram temas filosóficos e existenciais. Ele é um
dos autores mais traduzidos da França e suas obras têm sido amplamente elogiadas pela
crítica.
</p>
<button>Acessar página do livro</button>
</div>
<div class="hero-image">
<img src="/hero.png" alt="Hero Image" />
</div>
</section>
<section class="featured">
<div>
<span class="mdi mdi-truck"></span>
<h2>Frete grátis para SC</h2>
</div>
<div>
<span class="mdi mdi-star"></span>
<h2>Livros recomendados</h2>
</div>
<div>
<span class="mdi mdi-book-open-page-variant"></span>
<h2>Mais vendidos</h2>
</div>
</section>
<section class="books">
<article class="book" v-for="book in books" :key="book.id">
<img :src="book.cover" :alt="book.title" />
<h2>{{ book.title }}</h2>
<p class="book-author">{{ book.author }}</p>
<span class="price-and-like">
<p class="book-price">R$ {{ book.price.toFixed(2) }}</p>
<span class="mdi mdi-heart-outline"></span>
</span>
<button><span class="mdi mdi-cart"></span>Comprar</button>
</article>
</section>
</main>
<footer-component />
</template>
<style scoped>
header nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2vh 8vw;
border-bottom: 2px solid #27ae6099;
& h1 {
font-size: 1.3rem;
color: #000;
& a {
text-decoration: none;
color: #000;
display: flex;
align-items: center;
}
& .logo-title {
border-left: 1px solid #27ae6099;
font-size: 0.8rem;
margin-left: 10px;
padding-left: 10px;
color: #27ae6099;
width: 100px;
line-height: 1rem;
}
}
& input {
width: 400px;
height: 40px;
border-radius: 5px;
font-size: 1rem;
border: 0;
background-color: #f1f1f1;
padding: 5px;
}
& ul {
display: flex;
}
& ul li {
list-style: none;
margin: 0 10px;
font-size: 1rem;
}
& .icons li {
color: #27ae60;
font-size: 1.3rem;
}
& .search-wrapper {
position: relative;
}
& .search-wrapper::before {
content: ''; /* Code glyph para mdi-magnify */
font-family: 'Material Design Icons';
font-size: 1.2rem;
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
& .search {
padding-right: 2rem;
}
}
.hero {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5vh 9vw;
border-bottom: 2px solid #27ae6099;
& h2 {
color: #382c2c;
font-size: 3rem;
font-weight: 700;
}
& .hero-content {
width: 50%;
padding-right: 20px;
font-weight: 400;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
& h3.outlined {
background-color: transparent;
color: #27ae60;
border: 2px solid #27ae60;
padding: 15px 20px;
border-radius: 5px;
font-size: 1rem;
}
& button {
margin-top: 20px;
}
p {
width: 70%;
}
}
& .hero-image {
width: 50%;
text-align: right;
padding-right: 4vw;
& img {
max-width: 100%;
height: auto;
}
}
}
.featured {
display: flex;
padding: 3vh 8vw;
border-bottom: 2px solid #27ae6099;
& div {
display: flex;
align-items: center;
flex: 1;
justify-content: center;
gap: 10px;
& span {
font-size: 2rem;
}
& h2 {
font-size: 1.2rem;
font-weight: 700;
}
}
& article:nth-child(2) {
border-left: 1px solid #27ae6099;
border-right: 1px solid #27ae6099;
}
}
.books {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
padding: 5vh 8vw;
& .book {
display: flex;
flex-direction: column;
min-width: 300px;
width: calc(100% / 4 - 42px);
margin: 20px;
& h2 {
font-size: 1.5rem;
font-weight: 700;
}
& .book-author {
font-size: 1rem;
}
& .book-price {
font-size: 1.2rem;
font-weight: 700;
}
& .price-and-like {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
& .mdi-heart-outline {
font-size: 1.3rem;
color: #27ae60;
}
}
}
}
.cart {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 5vh 8vw;
border-bottom: 2px solid #27ae6099;
& h2 {
font-size: 2rem;
font-weight: 700;
color: #27ae60;
}
& table {
width: 100%;
border-collapse: collapse;
margin: 40px 0;
& th,
td {
padding: 10px;
text-align: left;
}
& th {
border-bottom: 2px solid #27ae6099;
font-size: 1.2rem;
font-weight: 700;
}
& td {
border-bottom: 1px solid rgb(128, 128, 128);
font-size: 1rem;
}
& .cart-item-quantity {
display: flex;
align-items: center;
}
& .cart-item-subtotal {
font-size: 1.1rem;
font-weight: 700;
}
& .cart-item {
display: flex;
align-items: center;
gap: 20px;
& img {
width: 80px;
height: auto;
}
& .cart-item-info {
display: flex;
flex-direction: column;
gap: 5px;
& .cart-item-title {
font-size: 1.2rem;
font-weight: 700;
}
& .cart-item-author {
font-size: 1rem;
}
& .cart-item-price {
font-size: 1.1rem;
font-weight: 600;
}
}
}
}
& .cart-summary {
display: flex;
justify-content: space-between;
width: 100%;
& .cupom {
display: flex;
align-items: center;
margin-top: 80px;
gap: 10px;
& input {
width: 350px;
height: 50px;
border-radius: 5px;
font-size: 1rem;
border: 2px solid rgb(128, 128, 128);
padding: 5px;
}
}
& .summary {
border: 1px solid rgb(128, 128, 128);
padding: 1vw;
& h2 {
font-size: 1.2rem;
font-weight: 700;
color: black;
}
& .summary-items {
display: grid;
grid-template-columns: 3fr 1fr;
& span {
padding: 10px 0;
border-bottom: 1px solid rgb(128, 128, 128);
}
}
& button {
margin-top: 20px;
}
}
}
}
button {
background-color: #27ae60;
color: #fff;
border: none;
padding: 15px 20px;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
gap: 20px;
display: flex;
justify-content: center;
&.outlined {
background-color: transparent;
color: #27ae60;
border: 2px solid #27ae60;
}
&.plain {
background-color: transparent;
color: black;
border: none;
cursor: pointer;
}
}
</style>
|
Replicando em outros componentes
Com as mesmas características do componente do rodapé, temos duas outras partes da aplicação que podem ser transformadas em componentes: o hero banner e a seção de destaque. Vamos criar esses dois componentes e replicar a estrutura do componente do rodapé.
Componente do hero banner
Primeiro, vamos criar o componente do hero banner. Para isso, crie o arquivo HeroBanner.vue na pasta src/components e adicione o seguinte código:
| ./src/components/HeroBanner.vue |
|---|
| <template>
<section class="hero">
<div class="hero-content">
<h3 class="outlined">Livro destaque</h3>
<h2>Noc Ognia</h2>
<p>
Noc ognia é um romance de Erich-Emmanuel Schmitt, que narra a história
de um homem que vive em um mundo onde as pessoas não podem mais sonhar.
O livro é uma reflexão sobre a importância dos sonhos e da imaginação na
vida humana. Erich-Emmanuel Schmitt é um autor francês conhecido por
suas obras que exploram temas filosóficos e existenciais. Ele é um dos
autores mais traduzidos da França e suas obras têm sido amplamente
elogiadas pela crítica.
</p>
<button>Acessar página do livro</button>
</div>
<div class="hero-image">
<img src="/hero.png" alt="Hero Image" />
</div>
</section>
</template>
<style scoped>
.hero {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5vh 9vw;
border-bottom: 2px solid #27ae6099;
& h2 {
color: #382c2c;
font-size: 3rem;
font-weight: 700;
}
& .hero-content {
width: 50%;
padding-right: 20px;
font-weight: 400;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
& h3.outlined {
background-color: transparent;
color: #27ae60;
border: 2px solid #27ae60;
padding: 15px 20px;
border-radius: 5px;
font-size: 1rem;
}
& button {
margin-top: 20px;
}
p {
width: 70%;
}
}
& .hero-image {
width: 50%;
text-align: right;
padding-right: 4vw;
& img {
max-width: 100%;
height: auto;
}
}
}
</style>
|
Note que também esse código foi copiado do arquivo App.vue.
Componente de destaque
O mesmo, agora, faremos com a seção de destaque. Para isso, crie o arquivo FeaturedComponent.vue na pasta src/components e adicione o seguinte código:
| ./src/components/FeaturedComponent.vue |
|---|
| <template>
<section class="featured">
<div>
<span class="mdi mdi-truck"></span>
<h2>Frete grátis para SC</h2>
</div>
<div>
<span class="mdi mdi-star"></span>
<h2>Livros recomendados</h2>
</div>
<div>
<span class="mdi mdi-book-open-page-variant"></span>
<h2>Mais vendidos</h2>
</div>
</section>
</template>
<style scoped>
.featured {
display: flex;
padding: 3vh 8vw;
border-bottom: 2px solid #27ae6099;
& div {
display: flex;
align-items: center;
flex: 1;
justify-content: center;
gap: 10px;
& span {
font-size: 2rem;
}
& h2 {
font-size: 1.2rem;
font-weight: 700;
}
}
& article:nth-child(2) {
border-left: 1px solid #27ae6099;
border-right: 1px solid #27ae6099;
}
}
</style>
|
Novamente, o código acima foi copiado do arquivo App.vue.
Aplicação dos componentes
Agora, vamos importar os componentes no arquivo App.vue e usá-los. Para isso, abra o arquivo src/App.vue e adicione o seguinte código:
| ./src/App.vue |
|---|
| <script setup>
import { ref } from 'vue'
import FooterComponent from '@/components/FooterComponent.vue'
import HeroBanner from '@/components/HeroBanner.vue'
import FeaturedComponent from '@/components/FeaturedComponent.vue'
...
</script>
|
Também, será preciso alterar o bloco de <template> para substituir o código do hero banner e da seção de destaque pelos componentes que acabamos de criar. O código do arquivo src/App.vue ficará assim:
| ./src/App.vue |
|---|
| </main>
<main v-else>
<hero-banner />
<featured-component />
<section class="books">
|
A versão completa do arquivo App.vue está no bloco abaixo.

Versão completa
Note que, além das linhas destacadas, foi retirado o código das tags <section> relativas ao hero e featured do arquivo App.vue. Também, foi removida a parte correspondente no bloco de <style>.
| ./src/App.vue |
|---|
| <script setup>
import { ref } from 'vue'
import FooterComponent from '@/components/FooterComponent.vue'
import HeroBanner from '@/components/HeroBanner.vue'
import FeaturedComponent from '@/components/FeaturedComponent.vue'
const showCart = ref(false)
const cart = ref({
items: [],
total: 0,
})
const books = [
{
id: 1,
title: 'Comigo na livraria',
cover: '/covers/comigo-na-livraria.png',
price: 23.24,
author: 'Martha Medeiros',
},
{
id: 2,
title: 'Quincas Borba',
cover: '/covers/quincas-borba.png',
price: 23.24,
author: 'Machado de Assis',
},
{
id: 3,
title: 'A livraria',
cover: '/covers/a-livraria.png',
price: 13.94,
author: 'Penelope Fitzgerald',
},
{
id: 4,
title: 'A hora da estrela',
cover: '/covers/a-hora-da-estrela.png',
price: 16.84,
author: 'Clarice Lispector',
},
{
id: 5,
title: 'O alienista',
cover: '/covers/o-alienista.png',
price: 266.92,
author: 'Machado de Assis',
},
{
id: 6,
title: 'Mar morto',
cover: '/covers/mar-morto.png',
price: 13.95,
author: 'Jorge Amado',
},
{
id: 7,
title: 'Grande sertão',
cover: '/covers/grande-sertao-veredas.png',
price: 26.04,
author: 'Guimarães Rosa',
},
{
id: 8,
title: 'Flor de poemas',
cover: '/covers/flor-de-poema.png',
price: 15.81,
author: 'Cecília Meireles',
},
]
</script>
<template>
<header>
<nav>
<h1>
<a href="#">
IFbooks
<span class="logo-title"> Apreço a livros </span>
</a>
</h1>
<div class="search-wrapper">
<input type="text" class="search" placeholder="Buscar..." />
</div>
<ul>
<li>Termos</li>
<li>Equipe</li>
<li>Envio</li>
<li>Devoluções</li>
</ul>
<ul class="icons">
<li @click="showCart = !showCart"><span class="mdi mdi-cart"></span></li>
<li><span class="mdi mdi-heart"></span></li>
<li><span class="mdi mdi-account"></span></li>
</ul>
</nav>
</header>
<main v-if="showCart">
<section class="cart">
<h2>Carrinho</h2>
<table>
<thead>
<tr>
<th>Título</th>
<th>Quantidade</th>
<th>Subtotal</th>
</tr>
</thead>
<tbody>
<tr v-for="book in cart.items" :key="book.id">
<td class="cart-item">
<img :src="book.cover" :alt="book.title" />
<div class="cart-item-info">
<p class="cart-item-title">{{ book.title }}</p>
<p class="cart-item-author">{{ book.author }}</p>
<p class="cart-item-price">R$ {{ book.price.toFixed(2) }}</p>
</div>
</td>
<td>
<div class="cart-item-quantity">
<button class="plain">
<span class="mdi mdi-minus" />
</button>
{{ book.quantity }}
<button class="plain">
<span class="mdi mdi-plus" />
</button>
</div>
</td>
<td class="cart-item-subtotal">R$ {{ book.price * book.quantity }}</td>
</tr>
</tbody>
</table>
<button @click="showCart = false" class="outlined">Voltar para loja</button>
<div class="cart-summary">
<div class="cupom">
<input type="text" placeholder="Código do cupom" />
<button>Inserir cupom</button>
</div>
<div class="summary">
<h2>Total da Compra</h2>
<div class="summary-items">
<span>Produtos</span> <span>R$ {{ cart.total.toFixed(2) }}</span> <span>Frete</span>
<span> Grátis</span> <span>Total</span> <span>R$ {{ cart.total.toFixed(2) }}</span>
</div>
<button>Ir para pagamento</button>
</div>
</div>
</section>
</main>
<main v-else>
<hero-banner />
<featured-component />
<section class="books">
<article class="book" v-for="book in books" :key="book.id">
<img :src="book.cover" :alt="book.title" />
<h2>{{ book.title }}</h2>
<p class="book-author">{{ book.author }}</p>
<span class="price-and-like">
<p class="book-price">R$ {{ book.price.toFixed(2) }}</p>
<span class="mdi mdi-heart-outline"></span>
</span>
<button><span class="mdi mdi-cart"></span>Comprar</button>
</article>
</section>
</main>
<footer-component />
</template>
<style scoped>
header nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2vh 8vw;
border-bottom: 2px solid #27ae6099;
& h1 {
font-size: 1.3rem;
color: #000;
& a {
text-decoration: none;
color: #000;
display: flex;
align-items: center;
}
& .logo-title {
border-left: 1px solid #27ae6099;
font-size: 0.8rem;
margin-left: 10px;
padding-left: 10px;
color: #27ae6099;
width: 100px;
line-height: 1rem;
}
}
& input {
width: 400px;
height: 40px;
border-radius: 5px;
font-size: 1rem;
border: 0;
background-color: #f1f1f1;
padding: 5px;
}
& ul {
display: flex;
}
& ul li {
list-style: none;
margin: 0 10px;
font-size: 1rem;
}
& .icons li {
color: #27ae60;
font-size: 1.3rem;
}
& .search-wrapper {
position: relative;
}
& .search-wrapper::before {
content: ''; /* Code glyph para mdi-magnify */
font-family: 'Material Design Icons';
font-size: 1.2rem;
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
& .search {
padding-right: 2rem;
}
}
.books {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
padding: 5vh 8vw;
& .book {
display: flex;
flex-direction: column;
min-width: 300px;
width: calc(100% / 4 - 42px);
margin: 20px;
& h2 {
font-size: 1.5rem;
font-weight: 700;
}
& .book-author {
font-size: 1rem;
}
& .book-price {
font-size: 1.2rem;
font-weight: 700;
}
& .price-and-like {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
& .mdi-heart-outline {
font-size: 1.3rem;
color: #27ae60;
}
}
}
}
.cart {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 5vh 8vw;
border-bottom: 2px solid #27ae6099;
& h2 {
font-size: 2rem;
font-weight: 700;
color: #27ae60;
}
& table {
width: 100%;
border-collapse: collapse;
margin: 40px 0;
& th,
td {
padding: 10px;
text-align: left;
}
& th {
border-bottom: 2px solid #27ae6099;
font-size: 1.2rem;
font-weight: 700;
}
& td {
border-bottom: 1px solid rgb(128, 128, 128);
font-size: 1rem;
}
& .cart-item-quantity {
display: flex;
align-items: center;
}
& .cart-item-subtotal {
font-size: 1.1rem;
font-weight: 700;
}
& .cart-item {
display: flex;
align-items: center;
gap: 20px;
& img {
width: 80px;
height: auto;
}
& .cart-item-info {
display: flex;
flex-direction: column;
gap: 5px;
& .cart-item-title {
font-size: 1.2rem;
font-weight: 700;
}
& .cart-item-author {
font-size: 1rem;
}
& .cart-item-price {
font-size: 1.1rem;
font-weight: 600;
}
}
}
}
& .cart-summary {
display: flex;
justify-content: space-between;
width: 100%;
& .cupom {
display: flex;
align-items: center;
margin-top: 80px;
gap: 10px;
& input {
width: 350px;
height: 50px;
border-radius: 5px;
font-size: 1rem;
border: 2px solid rgb(128, 128, 128);
padding: 5px;
}
}
& .summary {
border: 1px solid rgb(128, 128, 128);
padding: 1vw;
& h2 {
font-size: 1.2rem;
font-weight: 700;
color: black;
}
& .summary-items {
display: grid;
grid-template-columns: 3fr 1fr;
& span {
padding: 10px 0;
border-bottom: 1px solid rgb(128, 128, 128);
}
}
& button {
margin-top: 20px;
}
}
}
}
button {
background-color: #27ae60;
color: #fff;
border: none;
padding: 15px 20px;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
gap: 20px;
display: flex;
justify-content: center;
&.outlined {
background-color: transparent;
color: #27ae60;
border: 2px solid #27ae60;
}
&.plain {
background-color: transparent;
color: black;
border: none;
cursor: pointer;
}
}
</style>
|
As configurações de CSS
Note que, com essas alterações, o estilo CSS dos botões não foram colocados nos novos componentes, por isso a visualização pode estar diferente. Para resolver isso, vamos colocar todos os estilos CSS dos botões no arquivo src/assets/main.css (pode remover do arquivo src/App.vue). Para isso, abra o arquivo src/assets/main.css e deixe o código como abaixo:
| ./src/assets/main.css |
|---|
| @import './base.css';
html {
font-size: clamp(1rem, 1.5vw, 1.2rem);
line-height: 1.5;
}
button {
background-color: #27ae60;
color: #fff;
border: none;
padding: 15px 20px;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
gap: 20px;
display: flex;
justify-content: center;
&.outlined {
background-color: transparent;
color: #27ae60;
border: 2px solid #27ae60;
}
&.plain {
background-color: transparent;
color: black;
border: none;
cursor: pointer;
}
}
|
Manipulando eventos
A manipulação de eventos é uma parte importante da programação de interfaces de usuário. No Vue.js, os componentes filhos podem emitir eventos que são capturados pelos componentes pais. Para isso, é necessário utilizar a diretiva v-on no componente pai e o método $emit no componente filho.
Vamos fazer um exemplo na criação do componente de cabeçalho. O cabeçalho da aplicação terá um ícone (mdi-cart) que, quando clicado, irá emitir um evento para o componente pai, que permitirá alternar entre a exibição e ocultação do carrinho de compras.
Componente de cabeçalho
Primeiramente, vamos criar o componente de cabeçalho. Para isso, crie um novo arquivo chamado HeaderComponent.vue na pasta src/components. O conteúdo do arquivo deve ser o seguinte:
| ./src/components/HeaderComponent.vue |
|---|
| <script setup>
defineEmits(['click-cart']);
</script>
<template>
<header>
<nav>
<h1>
<a href="#">
IFbooks
<span class="logo-title"> Apreço a livros </span>
</a>
</h1>
<div class="search-wrapper">
<input type="text" class="search" placeholder="Buscar..." />
</div>
<ul>
<li>Termos</li>
<li>Equipe</li>
<li>Envio</li>
<li>Devoluções</li>
</ul>
<ul class="icons">
<li @click="$emit('click-cart')"><span class="mdi mdi-cart"></span></li>
<li><span class="mdi mdi-heart"></span></li>
<li><span class="mdi mdi-account"></span></li>
</ul>
</nav>
</header>
</template>
<style scoped>
header nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2vh 8vw;
border-bottom: 2px solid #27ae6099;
& h1 {
font-size: 1.3rem;
color: #000;
& a {
text-decoration: none;
color: #000;
display: flex;
align-items: center;
}
& .logo-title {
border-left: 1px solid #27ae6099;
font-size: 0.8rem;
margin-left: 10px;
padding-left: 10px;
color: #27ae6099;
width: 100px;
line-height: 1rem;
}
}
& input {
width: 400px;
height: 40px;
border-radius: 5px;
font-size: 1rem;
border: 0;
background-color: #f1f1f1;
padding: 5px;
}
& ul {
display: flex;
}
& ul li {
list-style: none;
margin: 0 10px;
font-size: 1rem;
}
& .icons li {
color: #27ae60;
font-size: 1.3rem;
}
& .search-wrapper {
position: relative;
}
& .search-wrapper::before {
content: ''; /* Code glyph para mdi-magnify */
font-family: 'Material Design Icons';
font-size: 1.2rem;
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
& .search {
padding-right: 2rem;
}
}
</style>
|
Note que, como nos casos anteriores, os blocos template e style foram praticamente copiados integralmente do arquivo App.vue. O que muda é que definimos um bloco script, onde declaramos o evento click-cart que será emitido quando o ícone do carrinho for clicado. Também, no bloco template, alteramos a linha 24 para adicionar o evento click no ícone do carrinho, que irá emitir o evento click-cart para o componente pai.
Agora, precisamos importar o componente HeaderComponent no arquivo App.vue e adicionar o evento click-cart no cabeçalho. Para isso, abra o arquivo src/App.vue e faça as seguintes alterações:
| ./src/App.vue |
|---|
| <script setup>
import { ref } from 'vue'
import FooterComponent from '@/components/FooterComponent.vue'
import HeroBanner from '@/components/HeroBanner.vue'
import FeaturedComponent from '@/components/FeaturedComponent.vue'
import HeaderComponent from '@/components/HeaderComponent.vue'
...
</script>
|
Também, precisamos editar o bloco template do arquivo App.vue para adicionar o evento click-cart no cabeçalho. Para isso, altere a linha 12 do arquivo App.vue para:
| ./src/App.vue |
|---|
| <template>
<header-component @click-cart="showCart = !showCart" />
<main v-if="showCart">
...
</template>
|
A versão completa do arquivo App.vue está no bloco abaixo.

Versão completa
Note que, além das linhas destacadas, foi retirado o código das tags <section> relativas ao hero e featured do arquivo App.vue. Também, foi removida a parte correspondente no bloco de <style>.
| ./src/App.vue |
|---|
| <script setup>
import { ref } from 'vue'
import FooterComponent from '@/components/FooterComponent.vue'
import HeroBanner from '@/components/HeroBanner.vue'
import FeaturedComponent from '@/components/FeaturedComponent.vue'
import HeaderComponent from '@/components/HeaderComponent.vue'
const showCart = ref(false)
const cart = ref({
items: [],
total: 0,
})
const books = [
{
id: 1,
title: 'Comigo na livraria',
cover: '/covers/comigo-na-livraria.png',
price: 23.24,
author: 'Martha Medeiros',
},
{
id: 2,
title: 'Quincas Borba',
cover: '/covers/quincas-borba.png',
price: 23.24,
author: 'Machado de Assis',
},
{
id: 3,
title: 'A livraria',
cover: '/covers/a-livraria.png',
price: 13.94,
author: 'Penelope Fitzgerald',
},
{
id: 4,
title: 'A hora da estrela',
cover: '/covers/a-hora-da-estrela.png',
price: 16.84,
author: 'Clarice Lispector',
},
{
id: 5,
title: 'O alienista',
cover: '/covers/o-alienista.png',
price: 266.92,
author: 'Machado de Assis',
},
{
id: 6,
title: 'Mar morto',
cover: '/covers/mar-morto.png',
price: 13.95,
author: 'Jorge Amado',
},
{
id: 7,
title: 'Grande sertão',
cover: '/covers/grande-sertao-veredas.png',
price: 26.04,
author: 'Guimarães Rosa',
},
{
id: 8,
title: 'Flor de poemas',
cover: '/covers/flor-de-poema.png',
price: 15.81,
author: 'Cecília Meireles',
},
]
</script>
<template>
<header-component @click-cart="showCart = !showCart" />
<main v-if="showCart">
<section class="cart">
<h2>Carrinho</h2>
<table>
<thead>
<tr>
<th>Título</th>
<th>Quantidade</th>
<th>Subtotal</th>
</tr>
</thead>
<tbody>
<tr v-for="book in cart.items" :key="book.id">
<td class="cart-item">
<img :src="book.cover" :alt="book.title" />
<div class="cart-item-info">
<p class="cart-item-title">{{ book.title }}</p>
<p class="cart-item-author">{{ book.author }}</p>
<p class="cart-item-price">R$ {{ book.price.toFixed(2) }}</p>
</div>
</td>
<td>
<div class="cart-item-quantity">
<button class="plain">
<span class="mdi mdi-minus" />
</button>
{{ book.quantity }}
<button class="plain">
<span class="mdi mdi-plus" />
</button>
</div>
</td>
<td class="cart-item-subtotal">R$ {{ book.price * book.quantity }}</td>
</tr>
</tbody>
</table>
<button @click="showCart = false" class="outlined">Voltar para loja</button>
<div class="cart-summary">
<div class="cupom">
<input type="text" placeholder="Código do cupom" />
<button>Inserir cupom</button>
</div>
<div class="summary">
<h2>Total da Compra</h2>
<div class="summary-items">
<span>Produtos</span> <span>R$ {{ cart.total.toFixed(2) }}</span> <span>Frete</span>
<span> Grátis</span> <span>Total</span> <span>R$ {{ cart.total.toFixed(2) }}</span>
</div>
<button>Ir para pagamento</button>
</div>
</div>
</section>
</main>
<main v-else>
<hero-banner />
<featured-component />
<section class="books">
<article class="book" v-for="book in books" :key="book.id">
<img :src="book.cover" :alt="book.title" />
<h2>{{ book.title }}</h2>
<p class="book-author">{{ book.author }}</p>
<span class="price-and-like">
<p class="book-price">R$ {{ book.price.toFixed(2) }}</p>
<span class="mdi mdi-heart-outline"></span>
</span>
<button><span class="mdi mdi-cart"></span>Comprar</button>
</article>
</section>
</main>
<footer-component />
</template>
<style scoped>
.books {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
padding: 5vh 8vw;
& .book {
display: flex;
flex-direction: column;
min-width: 300px;
width: calc(100% / 4 - 42px);
margin: 20px;
& h2 {
font-size: 1.5rem;
font-weight: 700;
}
& .book-author {
font-size: 1rem;
}
& .book-price {
font-size: 1.2rem;
font-weight: 700;
}
& .price-and-like {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
& .mdi-heart-outline {
font-size: 1.3rem;
color: #27ae60;
}
}
}
}
.cart {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 5vh 8vw;
border-bottom: 2px solid #27ae6099;
& h2 {
font-size: 2rem;
font-weight: 700;
color: #27ae60;
}
& table {
width: 100%;
border-collapse: collapse;
margin: 40px 0;
& th,
td {
padding: 10px;
text-align: left;
}
& th {
border-bottom: 2px solid #27ae6099;
font-size: 1.2rem;
font-weight: 700;
}
& td {
border-bottom: 1px solid rgb(128, 128, 128);
font-size: 1rem;
}
& .cart-item-quantity {
display: flex;
align-items: center;
}
& .cart-item-subtotal {
font-size: 1.1rem;
font-weight: 700;
}
& .cart-item {
display: flex;
align-items: center;
gap: 20px;
& img {
width: 80px;
height: auto;
}
& .cart-item-info {
display: flex;
flex-direction: column;
gap: 5px;
& .cart-item-title {
font-size: 1.2rem;
font-weight: 700;
}
& .cart-item-author {
font-size: 1rem;
}
& .cart-item-price {
font-size: 1.1rem;
font-weight: 600;
}
}
}
}
& .cart-summary {
display: flex;
justify-content: space-between;
width: 100%;
& .cupom {
display: flex;
align-items: center;
margin-top: 80px;
gap: 10px;
& input {
width: 350px;
height: 50px;
border-radius: 5px;
font-size: 1rem;
border: 2px solid rgb(128, 128, 128);
padding: 5px;
}
}
& .summary {
border: 1px solid rgb(128, 128, 128);
padding: 1vw;
& h2 {
font-size: 1.2rem;
font-weight: 700;
color: black;
}
& .summary-items {
display: grid;
grid-template-columns: 3fr 1fr;
& span {
padding: 10px 0;
border-bottom: 1px solid rgb(128, 128, 128);
}
}
& button {
margin-top: 20px;
}
}
}
}
</style>
|
Propriedades de um componente
Ao criar um componente, é possível passar propriedades para ele. As propriedades são valores que podem ser utilizados pelo componente para alterar seu comportamento ou sua aparência. As propriedades são passadas para o componente como atributos HTML e são acessadas pelo componente como variáveis JavaScript.
Para isso, como no caso dos eventos, elas precisam ser declaradas no componente. Para declarar uma propriedade, usamos a função defineProps do VueJS, que deve ser chamada dentro do bloco <script setup>, que é o bloco onde declaramos as variáveis e funções do componente.
Componente do carrinho
Para exemplificar o uso de propriedades, vamos criar um componente que representa o carrinho de compras. Esse componente vai receber a propriedade: cart que é um objeto que contém um array com os itens do carrinho e o total do carrinho. O componente vai exibir a lista de itens do carrinho e o total do carrinho. O componente vai ser usado na página principal da livraria, onde vamos exibir o carrinho de compras.
Para isso, vamos criar o componente CartComponent.vue dentro da pasta src/components. O arquivo deve ficar assim:
| ./src/components/CartComponent.vue |
|---|
| <script setup>
defineProps(['cart']);
</script>
<template>
<section class="cart">
<h2>Carrinho</h2>
<table>
<thead>
<tr>
<th>Título</th>
<th>Quantidade</th>
<th>Subtotal</th>
</tr>
</thead>
<tbody>
<tr v-for="book in cart.items" :key="book.id">
<td class="cart-item">
<img :src="book.cover" :alt="book.title" />
<div class="cart-item-info">
<p class="cart-item-title">{{ book.title }}</p>
<p class="cart-item-author">{{ book.author }}</p>
<p class="cart-item-price">R$ {{ book.price.toFixed(2) }}</p>
</div>
</td>
<td>
<div class="cart-item-quantity">
<button class="plain">
<span class="mdi mdi-minus" />
</button>
{{ book.quantity }}
<button class="plain">
<span class="mdi mdi-plus" />
</button>
</div>
</td>
<td class="cart-item-subtotal">
R$ {{ book.price * book.quantity }}
</td>
</tr>
</tbody>
</table>
<button class="outlined">Voltar para loja</button>
<div class="cart-summary">
<div class="cupom">
<input type="text" placeholder="Código do cupom" />
<button>Inserir cupom</button>
</div>
<div class="summary">
<h2>Total da Compra</h2>
<div class="summary-items">
<span>Produtos</span> <span>R$ {{ cart.total.toFixed(2) }}</span>
<span>Frete</span> <span> Grátis</span> <span>Total</span>
<span>R$ {{ cart.total.toFixed(2) }}</span>
</div>
<button>Ir para pagamento</button>
</div>
</div>
</section>
</template>
<style scoped>
.cart {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 5vh 8vw;
border-bottom: 2px solid #27ae6099;
& h2 {
font-size: 2rem;
font-weight: 700;
color: #27ae60;
}
& table {
width: 100%;
border-collapse: collapse;
margin: 40px 0;
& th,
td {
padding: 10px;
text-align: left;
}
& th {
border-bottom: 2px solid #27ae6099;
font-size: 1.2rem;
font-weight: 700;
}
& td {
border-bottom: 1px solid rgb(128, 128, 128);
font-size: 1rem;
}
& .cart-item-quantity {
display: flex;
align-items: center;
}
& .cart-item-subtotal {
font-size: 1.1rem;
font-weight: 700;
}
& .cart-item {
display: flex;
align-items: center;
gap: 20px;
& img {
width: 80px;
height: auto;
}
& .cart-item-info {
display: flex;
flex-direction: column;
gap: 5px;
& .cart-item-title {
font-size: 1.2rem;
font-weight: 700;
}
& .cart-item-author {
font-size: 1rem;
}
& .cart-item-price {
font-size: 1.1rem;
font-weight: 600;
}
}
}
}
& .cart-summary {
display: flex;
justify-content: space-between;
width: 100%;
& .cupom {
display: flex;
align-items: center;
margin-top: 80px;
gap: 10px;
& input {
width: 350px;
height: 50px;
border-radius: 5px;
font-size: 1rem;
border: 2px solid rgb(128, 128, 128);
padding: 5px;
}
}
& .summary {
border: 1px solid rgb(128, 128, 128);
padding: 1vw;
& h2 {
font-size: 1.2rem;
font-weight: 700;
color: black;
}
& .summary-items {
display: grid;
grid-template-columns: 3fr 1fr;
& span {
padding: 10px 0;
border-bottom: 1px solid rgb(128, 128, 128);
}
}
& button {
margin-top: 20px;
}
}
}
}
</style>
|
Agora, temos que importar e usar o componente CartComponent no arquivo src/App.vue. Para isso, abra o arquivo src/App.vue e adicione o seguinte código:
| ./src/App.vue |
|---|
| <script setup>
import { ref } from 'vue'
import FooterComponent from '@/components/FooterComponent.vue'
import HeroBanner from '@/components/HeroBanner.vue'
import FeaturedComponent from '@/components/FeaturedComponent.vue'
import HeaderComponent from '@/components/HeaderComponent.vue'
import CartComponent from '@/components/CartComponent.vue'
...
</script>
|
Também, adicione o componente CartComponent no template do arquivo src/App.vue, como no código abaixo:
| ./src/App.vue |
|---|
| <main v-if="showCart">
<cart-component :cart="cart"/>
</main>
|
Note que que na linha 79 o CardComponent foi adicionado dentro do bloco <main> que exibe o carrinho de compras. O componente CartComponent recebe a propriedade cart, que é um objeto que contém os itens do carrinho e o total do carrinho. Veja que o valor da propriedade cart foi enviado via diretiva v-bind, que é a forma de passar propriedades para um componente no VueJS. A diretiva v-bind, neste caso, foi abreviada para :. Assim, o valor da propriedade cart é o objeto cart que foi declarado no bloco <script setup>.
A versão completa do arquivo App.vue está no bloco abaixo.

Versão completa
Note que, além das linhas destacadas, foi retirado o código das tags <section> relativas ao hero e featured do arquivo App.vue. Também, foi removida a parte correspondente no bloco de <style>.
| ./src/App.vue |
|---|
| <script setup>
import { ref } from 'vue'
import FooterComponent from '@/components/FooterComponent.vue'
import HeroBanner from '@/components/HeroBanner.vue'
import FeaturedComponent from '@/components/FeaturedComponent.vue'
import HeaderComponent from '@/components/HeaderComponent.vue'
import CartComponent from '@/components/CartComponent.vue'
const showCart = ref(false)
const cart = ref({
items: [],
total: 0,
})
const books = [
{
id: 1,
title: 'Comigo na livraria',
cover: '/covers/comigo-na-livraria.png',
price: 23.24,
author: 'Martha Medeiros',
},
{
id: 2,
title: 'Quincas Borba',
cover: '/covers/quincas-borba.png',
price: 23.24,
author: 'Machado de Assis',
},
{
id: 3,
title: 'A livraria',
cover: '/covers/a-livraria.png',
price: 13.94,
author: 'Penelope Fitzgerald',
},
{
id: 4,
title: 'A hora da estrela',
cover: '/covers/a-hora-da-estrela.png',
price: 16.84,
author: 'Clarice Lispector',
},
{
id: 5,
title: 'O alienista',
cover: '/covers/o-alienista.png',
price: 266.92,
author: 'Machado de Assis',
},
{
id: 6,
title: 'Mar morto',
cover: '/covers/mar-morto.png',
price: 13.95,
author: 'Jorge Amado',
},
{
id: 7,
title: 'Grande sertão',
cover: '/covers/grande-sertao-veredas.png',
price: 26.04,
author: 'Guimarães Rosa',
},
{
id: 8,
title: 'Flor de poemas',
cover: '/covers/flor-de-poema.png',
price: 15.81,
author: 'Cecília Meireles',
},
]
</script>
<template>
<header-component @click-cart="showCart = !showCart" />
<main v-if="showCart">
<cart-component :cart="cart"/>
</main>
<main v-else>
<hero-banner />
<featured-component />
<section class="books">
<article class="book" v-for="book in books" :key="book.id">
<img :src="book.cover" :alt="book.title" />
<h2>{{ book.title }}</h2>
<p class="book-author">{{ book.author }}</p>
<span class="price-and-like">
<p class="book-price">R$ {{ book.price.toFixed(2) }}</p>
<span class="mdi mdi-heart-outline"></span>
</span>
<button><span class="mdi mdi-cart"></span>Comprar</button>
</article>
</section>
</main>
<footer-component />
</template>
<style scoped>
.books {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
padding: 5vh 8vw;
& .book {
display: flex;
flex-direction: column;
min-width: 300px;
width: calc(100% / 4 - 42px);
margin: 20px;
& h2 {
font-size: 1.5rem;
font-weight: 700;
}
& .book-author {
font-size: 1rem;
}
& .book-price {
font-size: 1.2rem;
font-weight: 700;
}
& .price-and-like {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
& .mdi-heart-outline {
font-size: 1.3rem;
color: #27ae60;
}
}
}
}
button {
background-color: #27ae60;
color: #fff;
border: none;
padding: 15px 20px;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
gap: 20px;
display: flex;
justify-content: center;
&.outlined {
background-color: transparent;
color: #27ae60;
border: 2px solid #27ae60;
}
&.plain {
background-color: transparent;
color: black;
border: none;
cursor: pointer;
}
}
</style>
|
Melhorias no componente carrinho
Agora que temos o componente CartComponent funcionando, podemos fazer algumas melhorias nele. Vamos adicionar a funcionalidade de adicionar e remover itens do carrinho, e também habilitar o botão de "Voltar para a loja". Para isso, vamos criar três eventos no componente CartComponent:
increment-book: evento que é emitido quando o usuário clica no botão de adicionar item ao carrinho. Esse evento deve passar o item que foi adicionado ao carrinho.
decrement-book: evento que é emitido quando o usuário clica no botão de remover item do carrinho. Esse evento deve passar o item que foi removido do carrinho.
hide-cart: evento que é emitido quando o usuário clica no botão de "Voltar para a loja". Esse evento não deve passar nenhum dado.
Para isso, vamos editar o componente CartComponent.vue e adicionar os eventos. O arquivo deve ficar assim:
| ./src/components/CartComponent.vue |
|---|
| <script setup>
defineProps(['cart']);
defineEmits(['hide-cart', 'increment-book', 'decrement-book']);
</script>
<template>
<section class="cart">
<h2>Carrinho</h2>
<table>
<thead>
<tr>
<th>Título</th>
<th>Quantidade</th>
<th>Subtotal</th>
</tr>
</thead>
<tbody>
<tr v-for="book in cart.items" :key="book.id">
<td class="cart-item">
<img :src="book.cover" :alt="book.title" />
<div class="cart-item-info">
<p class="cart-item-title">{{ book.title }}</p>
<p class="cart-item-author">{{ book.author }}</p>
<p class="cart-item-price">R$ {{ book.price.toFixed(2) }}</p>
</div>
</td>
<td>
<div class="cart-item-quantity">
<button @click="decrementBookToCart(book)" class="plain">
<span class="mdi mdi-minus" />
</button>
{{ book.quantity }}
<button @click="$emit('increment-book', book)" class="plain">
<span class="mdi mdi-plus" />
</button>
</div>
</td>
<td class="cart-item-subtotal">
R$ {{ book.price * book.quantity }}
</td>
</tr>
</tbody>
</table>
<button @click="$emit('hide-cart')" class="outlined">
Voltar para loja
</button>
<div class="cart-summary">
<div class="cupom">
<input type="text" placeholder="Código do cupom" />
<button>Inserir cupom</button>
</div>
<div class="summary">
<h2>Total da Compra</h2>
<div class="summary-items">
<span>Produtos</span> <span>R$ {{ cart.total.toFixed(2) }}</span>
<span>Frete</span> <span> Grátis</span> <span>Total</span>
<span>R$ {{ cart.total.toFixed(2) }}</span>
</div>
<button>Ir para pagamento</button>
</div>
</div>
</section>
</template>
<style scoped>
.cart {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 5vh 8vw;
border-bottom: 2px solid #27ae6099;
& h2 {
font-size: 2rem;
font-weight: 700;
color: #27ae60;
}
& table {
width: 100%;
border-collapse: collapse;
margin: 40px 0;
& th,
td {
padding: 10px;
text-align: left;
}
& th {
border-bottom: 2px solid #27ae6099;
font-size: 1.2rem;
font-weight: 700;
}
& td {
border-bottom: 1px solid rgb(128, 128, 128);
font-size: 1rem;
}
& .cart-item-quantity {
display: flex;
align-items: center;
}
& .cart-item-subtotal {
font-size: 1.1rem;
font-weight: 700;
}
& .cart-item {
display: flex;
align-items: center;
gap: 20px;
& img {
width: 80px;
height: auto;
}
& .cart-item-info {
display: flex;
flex-direction: column;
gap: 5px;
& .cart-item-title {
font-size: 1.2rem;
font-weight: 700;
}
& .cart-item-author {
font-size: 1rem;
}
& .cart-item-price {
font-size: 1.1rem;
font-weight: 600;
}
}
}
}
& .cart-summary {
display: flex;
justify-content: space-between;
width: 100%;
& .cupom {
display: flex;
align-items: center;
margin-top: 80px;
gap: 10px;
& input {
width: 350px;
height: 50px;
border-radius: 5px;
font-size: 1rem;
border: 2px solid rgb(128, 128, 128);
padding: 5px;
}
}
& .summary {
border: 1px solid rgb(128, 128, 128);
padding: 1vw;
& h2 {
font-size: 1.2rem;
font-weight: 700;
color: black;
}
& .summary-items {
display: grid;
grid-template-columns: 3fr 1fr;
& span {
padding: 10px 0;
border-bottom: 1px solid rgb(128, 128, 128);
}
}
& button {
margin-top: 20px;
}
}
}
}
</style>
|
Com isso, como já vimos anteriormente, o componente CartComponent emite os eventos increment-book, decrement-book e hide-cart. O evento increment-book é emitido quando o usuário clica no botão de adicionar item ao carrinho. O evento decrement-book é emitido quando o usuário clica no botão de remover item do carrinho. O evento hide-cart é emitido quando o usuário clica no botão de "Voltar para a loja". Esses eventos são usados para atualizar o estado do carrinho na aplicação.
Contudo, note que os eventos increment-book e decrement-book recebem também o item que foi adicionado ou removido do carrinho. Tal parâmetro é passado para o evento, e pode ser acessado no componente pai. Assim, podemos usar esses eventos para atualizar o estado do carrinho na aplicação.
Agora, vamos editar o arquivo src/App.vue e adicionar os métodos que vão lidar com os eventos emitidos pelo componente CartComponent. O arquivo deve ficar assim:
| ./src/App.vue |
|---|
| <main v-if="showCart">
<cart-component
:cart="cart"
@hide-cart="showCart = false"
@increment-book="incrementBookToCart"
@decrement-book="decrementBookToCart"
/>
</main>
|
A versão completa do arquivo App.vue está no bloco abaixo.

Versão completa
| ./src/App.vue |
|---|
| <script setup>
import { ref } from 'vue'
import FooterComponent from '@/components/FooterComponent.vue'
import HeroBanner from '@/components/HeroBanner.vue'
import FeaturedComponent from '@/components/FeaturedComponent.vue'
import HeaderComponent from '@/components/HeaderComponent.vue'
import CartComponent from '@/components/CartComponent.vue'
const showCart = ref(false)
const cart = ref({
items: [],
total: 0,
})
const books = [
{
id: 1,
title: 'Comigo na livraria',
cover: '/covers/comigo-na-livraria.png',
price: 23.24,
author: 'Martha Medeiros',
},
{
id: 2,
title: 'Quincas Borba',
cover: '/covers/quincas-borba.png',
price: 23.24,
author: 'Machado de Assis',
},
{
id: 3,
title: 'A livraria',
cover: '/covers/a-livraria.png',
price: 13.94,
author: 'Penelope Fitzgerald',
},
{
id: 4,
title: 'A hora da estrela',
cover: '/covers/a-hora-da-estrela.png',
price: 16.84,
author: 'Clarice Lispector',
},
{
id: 5,
title: 'O alienista',
cover: '/covers/o-alienista.png',
price: 266.92,
author: 'Machado de Assis',
},
{
id: 6,
title: 'Mar morto',
cover: '/covers/mar-morto.png',
price: 13.95,
author: 'Jorge Amado',
},
{
id: 7,
title: 'Grande sertão',
cover: '/covers/grande-sertao-veredas.png',
price: 26.04,
author: 'Guimarães Rosa',
},
{
id: 8,
title: 'Flor de poemas',
cover: '/covers/flor-de-poema.png',
price: 15.81,
author: 'Cecília Meireles',
},
]
</script>
<template>
<header-component @click-cart="showCart = !showCart" />
<main v-if="showCart">
<cart-component
:cart="cart"
@hide-cart="showCart = false"
@increment-book="incrementBookToCart"
@decrement-book="decrementBookToCart"
/>
</main>
<main v-else>
<hero-banner />
<featured-component />
<section class="books">
<article class="book" v-for="book in books" :key="book.id">
<img :src="book.cover" :alt="book.title" />
<h2>{{ book.title }}</h2>
<p class="book-author">{{ book.author }}</p>
<span class="price-and-like">
<p class="book-price">R$ {{ book.price.toFixed(2) }}</p>
<span class="mdi mdi-heart-outline"></span>
</span>
<button><span class="mdi mdi-cart"></span>Comprar</button>
</article>
</section>
</main>
<footer-component />
</template>
<style scoped>
.books {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
padding: 5vh 8vw;
& .book {
display: flex;
flex-direction: column;
min-width: 300px;
width: calc(100% / 4 - 42px);
margin: 20px;
& h2 {
font-size: 1.5rem;
font-weight: 700;
}
& .book-author {
font-size: 1rem;
}
& .book-price {
font-size: 1.2rem;
font-weight: 700;
}
& .price-and-like {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
& .mdi-heart-outline {
font-size: 1.3rem;
color: #27ae60;
}
}
}
}
</style>
|
Fixando propriedades e eventos
Já usamos o conceito de propriedades (props) e eventos (emits) em aulas anteriores, e também já o usamos em conjunto. Vamos agora, fazer a criação de mais um componente que precise de props e emits, para fixar o conceito.
No caso anterior, enviamos via props um objeto com os dados do carrinho de compras. Agora, vamos criar um componente que recebe um array de objetos, com as informações dos livros, e exibe esses livros em uma lista. O componente também deve emitir um evento quando o usuário clicar no botão de adicionar ao carrinho. O evento deve enviar o objeto do livro que foi adicionado ao carrinho.
Componente de Listagem de Livros
Vamos criar um componente chamado BooksListing.vue que será responsável por exibir a lista de livros. Esse componente deve receber um array de objetos com as informações dos livros e exibir esses livros em uma lista. O componente também deve emitir um evento quando o usuário clicar no botão de adicionar ao carrinho.
Inicialmente, o componente deve ser criado na pasta src/components, com o seguinte conteúdo:
| ./src/components/BooksListing.vue |
|---|
| <script setup>
defineEmits(['add-to-cart']);
defineProps(['books']);
</script>
<template>
<section class="books">
<article class="book" v-for="book in books" :key="book.id">
<img :src="book.cover" :alt="book.title" />
<h2>{{ book.title }}</h2>
<p class="book-author">{{ book.author }}</p>
<span class="price-and-like">
<p class="book-price">R$ {{ book.price.toFixed(2) }}</p>
<span class="mdi mdi-heart-outline"></span>
</span>
<button @click="$emit('add-to-cart', book)">
<span class="mdi mdi-cart"></span>Comprar
</button>
</article>
</section>
</template>
<style scoped>
.books {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
padding: 5vh 8vw;
& .book {
display: flex;
flex-direction: column;
min-width: 300px;
width: calc(100% / 4 - 42px);
margin: 20px;
& h2 {
font-size: 1.5rem;
font-weight: 700;
}
& .book-author {
font-size: 1rem;
}
& .book-price {
font-size: 1.2rem;
font-weight: 700;
}
& .price-and-like {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
& .mdi-heart-outline {
font-size: 1.3rem;
color: #27ae60;
}
}
}
}
</style>
|
Note que já declaramos as props e os emits do componente. As props são books, que é um array de objetos com as informações dos livros, e o evento add-to-cart, que é emitido quando o usuário clica no botão de adicionar ao carrinho. O evento deve enviar o objeto do livro que foi adicionado ao carrinho (linha 16).
Vamos agora alterar o arquivo src/App.vue para usar o novo componente. Para isso, abra o arquivo src/App.vue e adicione o seguinte conteúdo no bloco de script:
| ./src/App.vue |
|---|
| <script setup>
import { ref } from 'vue'
import FooterComponent from '@/components/FooterComponent.vue'
import HeroBanner from '@/components/HeroBanner.vue'
import FeaturedComponent from '@/components/FeaturedComponent.vue'
import HeaderComponent from '@/components/HeaderComponent.vue'
import CartComponent from '@/components/CartComponent.vue'
import BooksListing from '@/components/BooksListing.vue'
...
</script>
|
Também, vamos adicionar o componente BooksListing no template do arquivo src/App.vue, e passar as props e os emits para o componente. O arquivo deve ficar assim:
| ./src/App.vue |
|---|
| <main v-else>
<hero-banner />
<featured-component />
<books-listing :books="books" @add-to-cart="addToCart" />
</main>
|
A versão completa do arquivo App.vue está no bloco abaixo.

Versão completa
| ./src/App.vue |
|---|
| <script setup>
import { ref } from 'vue'
import FooterComponent from '@/components/FooterComponent.vue'
import HeroBanner from '@/components/HeroBanner.vue'
import FeaturedComponent from '@/components/FeaturedComponent.vue'
import HeaderComponent from '@/components/HeaderComponent.vue'
import CartComponent from '@/components/CartComponent.vue'
import BooksListing from '@/components/BooksListing.vue'
const showCart = ref(false)
const cart = ref({
items: [],
total: 0,
})
const books = [
{
id: 1,
title: 'Comigo na livraria',
cover: '/covers/comigo-na-livraria.png',
price: 23.24,
author: 'Martha Medeiros',
},
{
id: 2,
title: 'Quincas Borba',
cover: '/covers/quincas-borba.png',
price: 23.24,
author: 'Machado de Assis',
},
{
id: 3,
title: 'A livraria',
cover: '/covers/a-livraria.png',
price: 13.94,
author: 'Penelope Fitzgerald',
},
{
id: 4,
title: 'A hora da estrela',
cover: '/covers/a-hora-da-estrela.png',
price: 16.84,
author: 'Clarice Lispector',
},
{
id: 5,
title: 'O alienista',
cover: '/covers/o-alienista.png',
price: 266.92,
author: 'Machado de Assis',
},
{
id: 6,
title: 'Mar morto',
cover: '/covers/mar-morto.png',
price: 13.95,
author: 'Jorge Amado',
},
{
id: 7,
title: 'Grande sertão',
cover: '/covers/grande-sertao-veredas.png',
price: 26.04,
author: 'Guimarães Rosa',
},
{
id: 8,
title: 'Flor de poemas',
cover: '/covers/flor-de-poema.png',
price: 15.81,
author: 'Cecília Meireles',
},
]
</script>
<template>
<header-component @click-cart="showCart = !showCart" />
<main v-if="showCart">
<cart-component
:cart="cart"
@hide-cart="showCart = false"
@increment-book="incrementBookToCart"
@decrement-book="decrementBookToCart"
/>
</main>
<main v-else>
<hero-banner />
<featured-component />
<books-listing :books="books" @add-to-cart="addToCart" />
</main>
<footer-component />
</template>
<style scoped>
</style>
|