Pular para conteúdo

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>&copy; 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
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
<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
1
2
3
4
5
6
7
8
<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
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
<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
1
2
3
4
5
6
7
8
9
<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>