Skip to content

Latest commit

 

History

History
850 lines (703 loc) · 15.2 KB

File metadata and controls

850 lines (703 loc) · 15.2 KB

Aula 05 - Componentes Vue.js

Duração: 3 horas

  • Conteúdo teórico e prático: 1h30
  • Exercícios práticos: 1h30

Objetivos da Aula

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

1. Introdução a Componentes

1.1 O que é um Componente?

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)

1.2 Estrutura de um Componente

<!-- 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>

2. Props - Passando Dados para Componentes

2.1 Props Básicas

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>

2.2 Props com Tipos e Valores Padrão

<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>

2.3 Props com Objetos

<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>

3. Emissão de Eventos (Emit)

3.1 Emitir Eventos Simples

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>

3.2 Emitir com Dados

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>

3.3 Validação de Eventos

<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>

4. Slots - Composição de Componentes

4.1 Slot Simples

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>

4.2 Slot com Fallback

<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>

4.3 Slot com Dados (Scoped Slot)

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>

5. Exemplo Prático: Sistema de Formulário com Componentes

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>

6. Referências


7. Exercícios Propostos

Exercício 1: Componente de Abas

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

Exercício 2: Componente de Alerta

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

Exercício 3: Componente de Galeria

Criar componente que:

  • Exiba imagens em grid
  • Use slot para cada item
  • Permita prev/next
  • Mostre índice atual
  • Emita evento ao clicar em imagem

Exercício 4: Sistema de Formulário Completo

Estender o exemplo prático com:

  • Componente SelectField
  • Componente TextareaField
  • Validação em tempo real
  • Feedback visual de erros

Exercício 5: Componente de Pagination

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

8. Checklist de Aprendizagem

  • 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