Especialização em Front-End com Angular • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 05 — Comunicação Tipada com Signal Inputs input(), output() e model()

Este exemplo apresenta o modelo moderno de comunicação entre componentes no Angular 17.1+/18/19, substituindo os antigos decoradores @Input() e @Output() pelas funções funcionais baseadas em Signals: input.required(), output() e model() (Two-Way Binding nativo).

1. Componente Filho: src/app/components/threshold-slider.component.ts

// src/app/components/threshold-slider.component.ts
import { Component, input, output, model, computed } from '@angular/core';

@Component({
  selector: 'app-threshold-slider',
  standalone: true,
  template: `
    <div class="border rounded-xl p-4 bg-slate-50 space-y-3">
      <div class="flex justify-between items-center">
        <label class="text-xs font-bold text-slate-700 uppercase tracking-wider">
          {{ label() }}
        </label>
        <span class="text-xs font-mono font-bold text-red-600 bg-red-50 px-2 py-0.5 rounded">
          {{ value() }} {{ unit() }}
        </span>
      </div>

      <!-- Two-Way Binding nativo do signal model() -->
      <div class="flex items-center gap-3">
        <button
          type="button"
          (click)="decrement()"
          [disabled]="value() <= min()"
          class="w-7 h-7 rounded-lg bg-white border shadow-sm font-bold text-slate-700 hover:bg-slate-100 disabled:opacity-40"
        >
          -
        </button>

        <input
          type="range"
          [min]="min()"
          [max]="max()"
          [step]="step()"
          [value]="value()"
          (input)="onRangeChange($event)"
          class="flex-1 h-2 bg-slate-200 rounded-lg cursor-pointer accent-red-600"
        />

        <button
          type="button"
          (click)="increment()"
          [disabled]="value() >= max()"
          class="w-7 h-7 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-between items-center text-[11px] text-slate-400 font-mono">
        <span>Mín: {{ min() }}</span>
        <span>Máx: {{ max() }}</span>
      </div>
    </div>
  `
})
export class ThresholdSliderComponent {
  // 1. Signal Inputs (Somente Leitura e Tipadas)
  label = input.required<string>(); // input obrigatória exigida pelo compilador
  min = input<number>(0);
  max = input<number>(100);
  step = input<number>(5);
  unit = input<string>('%');

  // 2. Signal Model: Two-Way Data Binding nativo (leitura e escrita síncronas)
  value = model<number>(50);

  // 3. Signal Output: Emissão funcional de eventos
  thresholdExceeded = output<{ metric: string; currentVal: number }>();

  increment(): void {
    const next = this.value() + this.step();
    if (next <= this.max()) {
      this.value.set(next);
      this.checkLimits();
    }
  }

  decrement(): void {
    const prev = this.value() - this.step();
    if (prev >= this.min()) {
      this.value.set(prev);
    }
  }

  onRangeChange(event: Event): void {
    const target = event.target as HTMLInputElement;
    this.value.set(Number(target.value));
    this.checkLimits();
  }

  private checkLimits(): void {
    if (this.value() >= this.max() * 0.9) {
      this.thresholdExceeded.emit({
        metric: this.label(),
        currentVal: this.value()
      });
    }
  }
}

2. Componente Pai Consumidor: src/app/components/cluster-quota-manager.component.ts

// src/app/components/cluster-quota-manager.component.ts
import { Component, signal } from '@angular/core';
import { ThresholdSliderComponent } from './threshold-slider.component';

@Component({
  selector: 'app-cluster-quota-manager',
  standalone: true,
  imports: [ThresholdSliderComponent],
  template: `
    <div class="max-w-xl mx-auto p-6 bg-white border border-slate-200 rounded-2xl shadow-sm space-y-6">
      <div class="border-b pb-4">
        <h2 class="text-xl font-bold text-slate-800">🎛️ Gestão de Cotas de Recursos</h2>
        <p class="text-xs text-slate-500">Comunicação pai-filho via input(), output() e model()</p>
      </div>

      <!-- Alerta Disparado pelo Signal Output -->
      @if (alertBanner()) {
        <div class="p-3 bg-amber-50 border border-amber-200 text-amber-900 rounded-xl text-xs font-semibold">
          {{ alertBanner() }}
        </div>
      }

      <div class="space-y-4">
        <!-- Two-Way Binding limpo [(value)] gerado automaticamente pelo model() -->
        <app-threshold-slider
          label="Limite de CPU do Cluster"
          [min]="10"
          [max]="100"
          [step]="5"
          unit="%"
          [(value)]="cpuQuota"
          (thresholdExceeded)="onQuotaExceeded($event)"
        />

        <app-threshold-slider
          label="Cota de Memória RAM"
          [min]="4"
          [max]="64"
          [step]="4"
          unit="GB"
          [(value)]="ramQuota"
          (thresholdExceeded)="onQuotaExceeded($event)"
        />
      </div>

      <!-- Inspeção do Estado Reativo do Pai -->
      <div class="p-4 bg-slate-900 text-slate-200 rounded-xl font-mono text-xs space-y-1">
        <div class="text-emerald-400 font-bold">// Estado Reativo do Componente Pai:</div>
        <div>cpuQuota Signal: {{ cpuQuota() }}%</div>
        <div>ramQuota Signal: {{ ramQuota() }}GB</div>
      </div>
    </div>
  `
})
export class ClusterQuotaManagerComponent {
  cpuQuota = signal<number>(75);
  ramQuota = signal<number>(32);
  alertBanner = signal<string | null>(null);

  onQuotaExceeded(event: { metric: string; currentVal: number }): void {
    this.alertBanner.set(`⚠️ Alerta Crítico: ${event.metric} ultrapassou 90% da capacidade (${event.currentVal})!`);
  }
}

🖥️ Saída / Comportamento Esperado

Renderização Inicial:
Slider 1 (CPU): "LIMITE DE CPU DO CLUSTER" | Valor: 75 %
Slider 2 (RAM): "COTA DE MEMÓRIA RAM" | Valor: 32 GB
Painel JSON do Pai: cpuQuota: 75%, ramQuota: 32GB

Interação 1 — Clicar no botão (+) da CPU até 95%:
O valor sobe de 75 -> 80 -> 85 -> 90 -> 95.
Ao passar de 90%, o método checkLimits() do filho emite o signal output thresholdExceeded.
Banner âmbar surge no pai:
"⚠️ Alerta Crítico: Limite de CPU do Cluster ultrapassou 90% da capacidade (95)!"
O painel escuro do pai reflete: "cpuQuota Signal: 95%".

Interação 2 — Deslizar range da RAM para 16 GB:
O valor atualiza o signal ramQuota no pai sincronizadamente via [(value)].

🔍 Destaques da Implementação

  1. input.required<T>(): O compilador TypeScript emite erro estático na esteira de build caso o desenvolvedor omita a prop obrigatória no template do pai.
  2. model<T>(): Unifica entrada e saída em um único objeto de leitura e escrita, dispensando a declaração manual de @Input() value e @Output() valueChange.
  3. output<T>() Funcional: Não depende do antigo EventEmitter do RxJS, tornando a emissão de eventos mais leve e performática.