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

  1. defineModel() Simplificado: No Vue 3.4+, substitui a combinação verbosa de props: ['modelValue'] e emit('update:modelValue').
  2. withDefaults com TypeScript: Fornece valores padrão em tempo de compilação sem duplicar tipos de runtime.
  3. Isolamento de Contrato: O componente filho valida limites internos e notifica o pai exclusivamente através de eventos semânticos.