- Conteúdo teórico e prático: 1h30
- Exercícios práticos: 1h30
Ao final desta aula, o aluno será capaz de:
- Criar componentes reutilizáveis
- Entender props e como passar dados para componentes
- Dominar emissão de eventos entre componentes
- Usar slots para composição
- Implementar componentes inteligentes e bem estruturados
Um componente é uma unidade reutilizável de interface que:
- Encapsula HTML, JavaScript e CSS
- Pode ser usado em múltiplos lugares
- Comunica-se via props (entrada) e events (saída)
- Mantém seu próprio estado
- Pode conter outros componentes (composição)
<!-- Arquivo: src/components/MeuComponente.vue -->
<template>
<!-- Template do componente -->
<div class="meu-componente">
<h2>{{ titulo }}</h2>
<p>{{ conteudo }}</p>
</div>
</template>
<script setup>
// Props recebidas do componente pai
defineProps({
titulo: String,
conteudo: String
})
</script>
<style scoped>
.meu-componente {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
</style>Componente filho (Card.vue):
<template>
<div class="card">
<h3>{{ titulo }}</h3>
<p>{{ descricao }}</p>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
// Forma simples
defineProps({
titulo: String,
descricao: String
})
</script>
<style scoped>
.card {
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #f9f9f9;
}
</style>Componente pai (App.vue):
<template>
<div>
<!-- Usar componente passando props -->
<Card titulo="Título 1" descricao="Descrição 1" />
<Card titulo="Título 2" descricao="Descrição 2" />
</div>
</template>
<script setup>
import Card from './components/Card.vue'
</script><template>
<div class="botao-customizado">
<button :class="tipo" :disabled="desabilitado">
{{ texto }}
</button>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
// Props com validação de tipo e valor padrão
defineProps({
texto: {
type: String,
required: true // Obrigatório
},
tipo: {
type: String,
default: 'primary' // Valor padrão
},
desabilitado: {
type: Boolean,
default: false
},
tamanho: {
type: String,
enum: ['small', 'medium', 'large'], // Valores permitidos
default: 'medium'
}
})
</script>
<style scoped>
.botao-customizado button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.botao-customizado .primary {
background-color: #42b883;
color: white;
}
.botao-customizado .danger {
background-color: #ff6b6b;
color: white;
}
.botao-customizado button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style><template>
<div class="usuario-card">
<img :src="usuario.avatar" />
<h3>{{ usuario.nome }}</h3>
<p>{{ usuario.email }}</p>
<span class="badge">{{ usuario.role }}</span>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
usuario: {
type: Object,
required: true,
// Validação customizada
validator(obj) {
return obj.nome && obj.email
}
}
})
</script>
<style scoped>
.usuario-card {
text-align: center;
padding: 20px;
border: 1px solid #ddd;
}
img {
width: 80px;
height: 80px;
border-radius: 50%;
}
.badge {
background-color: #42b883;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85rem;
}
</style>Componente filho (Botao.vue):
<template>
<button @click="clicado">
{{ texto }}
</button>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
defineProps({
texto: String
})
// Definir eventos que este componente emite
const emit = defineEmits(['clique'])
const clicado = () => {
// Emitir evento para o pai
emit('clique')
}
</script>Componente pai:
<template>
<div>
<!-- Ouvir evento emitido pelo filho -->
<Botao texto="Clique aqui" @clique="handleClique" />
<p v-if="clicado">Botão foi clicado!</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Botao from './components/Botao.vue'
const clicado = ref(false)
const handleClique = () => {
clicado.value = true
}
</script>Componente filho (ItemLista.vue):
<template>
<li class="item">
<span>{{ item.nome }}</span>
<button @click="remover" class="btn-remover">✕</button>
</li>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
defineProps({
item: Object
})
const emit = defineEmits(['remover'])
const remover = () => {
// Emitir evento COM dados
emit('remover', item.id)
}
</script>
<style scoped>
.item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.btn-remover {
background-color: #ff6b6b;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>Componente pai:
<template>
<div>
<h2>Minha Lista</h2>
<ul>
<ItemLista
v-for="item in items"
:key="item.id"
:item="item"
@remover="removerItem"
/>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ItemLista from './components/ItemLista.vue'
const items = ref([
{ id: 1, nome: 'Item 1' },
{ id: 2, nome: 'Item 2' }
])
const removerItem = (id) => {
items.value = items.value.filter(item => item.id !== id)
}
</script><script setup>
import { defineEmits } from 'vue'
// Validar eventos emitidos
const emit = defineEmits({
// Sem validação
simple: null,
// Com validação de parâmetros
click: (payload) => {
return typeof payload === 'number'
},
submit: (payload) => {
return payload && payload.email && payload.mensagem
}
})
// Só funciona se payload for número
const enviarNumero = () => {
emit('click', 42) // ✓ Válido
// emit('click', 'texto') // ✗ Inválido
}
</script>Componente (Modal.vue):
<template>
<div class="modal-overlay" @click="fechar">
<div class="modal" @click.stop>
<header class="modal-header">
<h2>{{ titulo }}</h2>
<button @click="fechar" class="close">✕</button>
</header>
<!-- Slot - conteúdo do pai aparecerá aqui -->
<div class="modal-body">
<slot></slot>
</div>
<footer class="modal-footer">
<slot name="footer"></slot>
</footer>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
defineProps({
titulo: String
})
const emit = defineEmits(['fechar'])
const fechar = () => {
emit('fechar')
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background-color: white;
border-radius: 8px;
min-width: 400px;
max-width: 600px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #eee;
}
.close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #eee;
display: flex;
gap: 10px;
justify-content: flex-end;
}
</style>Usando o componente:
<template>
<div>
<button @click="mostrar = true">Abrir Modal</button>
<Modal
v-if="mostrar"
titulo="Confirmar Ação"
@fechar="mostrar = false"
>
<!-- Conteúdo do slot padrão -->
<p>Tem certeza que deseja continuar?</p>
<!-- Conteúdo do slot nomeado 'footer' -->
<template #footer>
<button @click="confirmar" class="btn-primary">
Confirmar
</button>
<button @click="mostrar = false" class="btn-secondary">
Cancelar
</button>
</template>
</Modal>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Modal from './components/Modal.vue'
const mostrar = ref(false)
const confirmar = () => {
alert('Confirmado!')
mostrar.value = false
}
</script><template>
<div class="card">
<!-- Se o pai não passar conteúdo, exibir padrão -->
<h3>
<slot name="titulo">
Título Padrão
</slot>
</h3>
<div class="conteudo">
<slot>
<p>Conteúdo padrão aqui</p>
</slot>
</div>
</div>
</template>Componente (ListaItem.vue):
<template>
<div>
<div v-for="item in items" :key="item.id">
<!-- Passar dados do componente pai -->
<slot :item="item" :index="items.indexOf(item)">
<!-- Fallback padrão -->
<p>{{ item.nome }}</p>
</slot>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
items: Array
})
</script>Usando:
<template>
<div>
<ListaItem :items="pessoas">
<!-- Acessar dados do slot do pai -->
<template #default="{ item, index }">
<div class="pessoa-card">
<span class="numero">{{ index + 1 }}</span>
<h4>{{ item.nome }}</h4>
<p>{{ item.cargo }}</p>
</div>
</template>
</ListaItem>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ListaItem from './components/ListaItem.vue'
const pessoas = ref([
{ id: 1, nome: 'Ana', cargo: 'Gerente' },
{ id: 2, nome: 'Bruno', cargo: 'Developer' }
])
</script>Componente InputField.vue:
<template>
<div class="form-group">
<label v-if="label" :for="id">
{{ label }}
<span v-if="obrigatorio" class="obrigatorio">*</span>
</label>
<input
:id="id"
:type="tipo"
:value="modelValue"
:placeholder="placeholder"
:disabled="desabilitado"
@input="emit('update:modelValue', $event.target.value)"
@blur="emit('blur')"
/>
<span v-if="erro" class="erro">{{ erro }}</span>
<span v-if="dica" class="dica">{{ dica }}</span>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
defineProps({
modelValue: String,
label: String,
tipo: { type: String, default: 'text' },
placeholder: String,
obrigatorio: Boolean,
desabilitado: Boolean,
erro: String,
dica: String,
id: String
})
const emit = defineEmits(['update:modelValue', 'blur'])
</script>
<style scoped>
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.obrigatorio {
color: red;
}
input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
input:focus {
outline: none;
border-color: #42b883;
box-shadow: 0 0 0 3px rgba(66, 184, 131, 0.1);
}
input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.erro {
color: #ff6b6b;
font-size: 0.85rem;
display: block;
margin-top: 5px;
}
.dica {
color: #999;
font-size: 0.85rem;
display: block;
margin-top: 5px;
}
</style>Componente Formulario.vue:
<template>
<form @submit.prevent="enviar" class="formulario">
<h2>Cadastro de Usuário</h2>
<InputField
v-model="formulario.nome"
label="Nome"
placeholder="Digite seu nome"
obrigatorio
:erro="erros.nome"
dica="Mínimo 3 caracteres"
/>
<InputField
v-model="formulario.email"
label="Email"
tipo="email"
placeholder="seu@email.com"
obrigatorio
:erro="erros.email"
/>
<InputField
v-model="formulario.telefone"
label="Telefone"
placeholder="(11) 99999-9999"
:erro="erros.telefone"
/>
<button type="submit">Enviar</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
import InputField from './InputField.vue'
const formulario = ref({
nome: '',
email: '',
telefone: ''
})
const erros = ref({
nome: '',
email: '',
telefone: ''
})
const validar = () => {
erros.value = {}
if (!formulario.value.nome || formulario.value.nome.length < 3) {
erros.value.nome = 'Nome deve ter no mínimo 3 caracteres'
}
if (!formulario.value.email || !formulario.value.email.includes('@')) {
erros.value.email = 'Email inválido'
}
return Object.keys(erros.value).length === 0
}
const enviar = () => {
if (validar()) {
console.log('Formulário válido:', formulario.value)
alert('Enviado com sucesso!')
}
}
</script>
<style scoped>
.formulario {
max-width: 500px;
margin: 0 auto;
padding: 30px;
background-color: #f9f9f9;
border-radius: 8px;
}
button {
width: 100%;
padding: 12px;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
font-weight: bold;
}
button:hover {
background-color: #35a372;
}
</style>Criar um componente de abas que:
- Receba array de abas como prop
- Emita evento quando aba é clicada
- Use slot para conteúdo de cada aba
- Destaque aba ativa
Criar componente que:
- Exiba diferentes tipos (sucesso, erro, aviso, info)
- Tenha botão para fechar
- Use slot para conteúdo customizável
- Emita evento ao fechar
Criar componente que:
- Exiba imagens em grid
- Use slot para cada item
- Permita prev/next
- Mostre índice atual
- Emita evento ao clicar em imagem
Estender o exemplo prático com:
- Componente SelectField
- Componente TextareaField
- Validação em tempo real
- Feedback visual de erros
Criar componente que:
- Receba total de itens
- Emita evento de página selecionada
- Mostre botões de naveg ação
- Use slots para customização
- Consigo criar um componente básico
- Entendo como usar props para passar dados
- Consigo validar props
- Entendo defineEmits e eventos
- Consigo emitir eventos com dados
- Consigo usar slots simples
- Consigo usar slots nomeados
- Consigo usar scoped slots
- Consigo criar componentes reutilizáveis
- Completei todos os 5 exercícios propostos