💻 Exemplos: Capítulo 05: Comunicação com
Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis
💻 Código de Exemplo 05 — Comunicação Tipada com defineProps, defineEmits e defineModel
Este exemplo apresenta a comunicação entre componentes pai e filho em conformidade com as melhores práticas do Vue 3.4+, utilizando macros de compilador tipadas com TypeScript puro e o novo defineModel().
1. Componente Filho: EditableMetricInput.vue
<!-- src/components/EditableMetricInput.vue -->
<script setup lang="ts">
import { computed } from 'vue';
// 1. defineModel() do Vue 3.4+ para Two-Way Data Binding nativo e tipado
const modelValue = defineModel<number>({ required: true, default: 0 });
// 2. defineProps com tipagem genérica pura e withDefaults
interface Props {
label: string;
min?: number;
max?: number;
step?: number;
unit?: string;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
min: 0,
max: 100,
step: 1,
unit: 'pts',
disabled: false
});
// 3. defineEmits com tipagem explícita de eventos customizados
const emit = defineEmits<{
(e: 'limitReached', payload: { type: 'min' | 'max'; currentVal: number }): void;
(e: 'resetDefault'): void;
}>();
const isMaxReached = computed(() => modelValue.value >= props.max);
const isMinReached = computed(() => modelValue.value <= props.min);
function increment(): void {
if (modelValue.value + props.step <= props.max) {
modelValue.value += props.step;
} else {
emit('limitReached', { type: 'max', currentVal: modelValue.value });
}
}
function decrement(): void {
if (modelValue.value - props.step >= props.min) {
modelValue.value -= props.step;
} else {
emit('limitReached', { type: 'min', currentVal: modelValue.value });
}
}
function handleReset(): void {
modelValue.value = props.min;
emit('resetDefault');
}
</script>
<template>
<div class="border rounded-xl p-4 bg-slate-50 space-y-3">
<div class="flex justify-between items-center">
<label class="text-sm font-semibold text-slate-700">{{ props.label }}</label>
<span class="text-xs bg-slate-200 text-slate-700 px-2 py-0.5 rounded font-mono">
[{{ props.min }} - {{ props.max }} {{ props.unit }}]
</span>
</div>
<div class="flex items-center gap-3">
<button
type="button"
:disabled="props.disabled || isMinReached"
@click="decrement"
class="w-8 h-8 rounded-lg bg-white border shadow-sm font-bold text-slate-700 hover:bg-slate-100 disabled:opacity-40"
>
-
</button>
<div class="flex-1 text-center font-mono font-bold text-lg text-indigo-700">
{{ modelValue }} <span class="text-xs font-normal text-slate-500">{{ props.unit }}</span>
</div>
<button
type="button"
:disabled="props.disabled || isMaxReached"
@click="increment"
class="w-8 h-8 rounded-lg bg-white border shadow-sm font-bold text-slate-700 hover:bg-slate-100 disabled:opacity-40"
>
+
</button>
</div>
<div class="flex justify-end gap-2 pt-1 border-t">
<button
type="button"
@click="handleReset"
class="text-xs text-slate-500 hover:text-slate-800 underline"
>
Restaurar Mínimo
</button>
</div>
</div>
</template>
2. Componente Pai: MetricsDashboardView.vue
<!-- src/views/MetricsDashboardView.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import EditableMetricInput from '../components/EditableMetricInput.vue';
const cpuThreshold = ref<number>(75);
const memoryQuota = ref<number>(16);
const notificationMessage = ref<string>('');
function onLimitReached(payload: { type: 'min' | 'max'; currentVal: number }): void {
notificationMessage.value = `⚠️ Limite de ${payload.type.toUpperCase()} atingido! Valor atual: ${payload.currentVal}`;
}
function onReset(): void {
notificationMessage.value = 'ℹ️ Valor resetado para a configuração base mínima.';
}
</script>
<template>
<div class="max-w-xl mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-6">
<h2 class="text-xl font-bold text-slate-800">🎛️ Gestão de Métricas do Sistema</h2>
<div v-if="notificationMessage" class="p-3 bg-amber-50 border border-amber-200 text-amber-800 rounded-lg text-xs font-medium">
{{ notificationMessage }}
</div>
<div class="space-y-4">
<!-- Two-way binding transparente via v-model suportado por defineModel -->
<EditableMetricInput
v-model="cpuThreshold"
label="Limite de Alerta de CPU"
:min="10"
:max="95"
:step="5"
unit="%"
@limitReached="onLimitReached"
@resetDefault="onReset"
/>
<EditableMetricInput
v-model="memoryQuota"
label="Cota de Memória RAM"
:min="4"
:max="64"
:step="4"
unit="GB"
@limitReached="onLimitReached"
@resetDefault="onReset"
/>
</div>
<div class="p-4 bg-slate-900 text-slate-100 rounded-xl font-mono text-xs space-y-1">
<div class="text-emerald-400 font-bold">// Estado Reativo do Componente Pai:</div>
<div>cpuThreshold: {{ cpuThreshold }}%</div>
<div>memoryQuota: {{ memoryQuota }}GB</div>
</div>
</div>
</template>
🖥️ Saída / Comportamento Esperado
Renderização Inicial:
Card 1 (CPU): [10 - 95 %] com valor central 75 %
Card 2 (RAM): [4 - 64 GB] com valor central 16 GB
Painel JSON do Pai: { cpuThreshold: 75, memoryQuota: 16 }
Interação 1 — Clicar 4 vezes no '+' da CPU:
75 -> 80 -> 85 -> 90 -> 95 %.
Ao atingir 95:
O botão '+' é desabilitado (disabled: opacity-40).
O evento @limitReached é emitido para o componente pai.
Banner exibe: "⚠️ Limite de MAX atingido! Valor atual: 95".
Painel JSON do pai reflete: cpuThreshold: 95%.
Interação 2 — Clicar em "Restaurar Mínimo":
modelValue é atualizado para 10 no filho e sincronizado no pai sem necessidade de emits manuais 'update:modelValue'.
🔍 Destaques da Implementação
defineModel()Simplificado: No Vue 3.4+, substitui a combinação verbosa deprops: ['modelValue']eemit('update:modelValue').withDefaultscom TypeScript: Fornece valores padrão em tempo de compilação sem duplicar tipos de runtime.- Isolamento de Contrato: O componente filho valida limites internos e notifica o pai exclusivamente através de eventos semânticos.