Publicado em

- 5 minutos de leitura

Angular Signal Forms: Traduzindo Form Models para Domain Models

img of Angular Signal Forms: Traduzindo Form Models para Domain Models

O Angular introduziu o Signal Forms para modernizar o gerenciamento de estado em formulários reativos.

Esta nova abordagem baseada em Signals traz tipagem estrita e reatividade de alta performance para o frontend.

No entanto, surge um desafio comum: a incompatibilidade estrutural entre o Form Model e o Domain Model da sua API.

Neste artigo técnico, vamos explorar como implementar essa tradução de dados de forma eficiente utilizando o ecossistema moderno do Angular.

Integração com HTML Nativo e a Necessidade de Tradução

Campos de formulário HTML geralmente lidam bem com strings vazias, mas falham ou apresentam comportamentos indesejados ao trabalhar diretamente com valores como null, undefined ou Infinity.

Essa característica decorre do fato de que o Signal Forms possui uma abordagem focada em ser altamente integrativa com os elementos do HTML. Por tentar manter uma compatibilidade quase idêntica com o suporte nativo do HTML, ele reflete de forma direta como os inputs do navegador lidam com dados, tornando a tradução de modelos essencial para alinhar essas peculiaridades ao nosso modelo de domínio.

As entidades de domínio do backend frequentemente utilizam valores como null para representar a ausência de dados de forma semântica.

Para garantir a integridade da arquitetura, é fundamental estabelecer uma camada de tradução bidirecional entre esses modelos.

Diferenciando o Form Model do Domain Model

A separação de responsabilidades começa na definição clara das interfaces TypeScript.

O Form Model deve representar o estado exato esperado pelos controles do formulário.

   interface FormModel {
	personal: {
		name: string
		email: string
	}
	address: {
		zipcode: string
		street: string
		number: string
	}
}

Como podemos notar, esta estrutura exige strings explícitas para garantir o funcionamento correto dos inputs de texto na interface.

Por outro lado, o Domain Model reflete a estrutura da entidade de negócio que trafega através de requisições web.

   interface DomainModel {
	personal: {
		name: string
		email: string
	}
	address: null | {
		zipcode: string | null
		street: string | null
		number: string | null
	}
}

Este modelo de domínio aceita valores como null de forma semântica, exigindo um tratamento antes de ser acoplado ao formulário.

Funções de Tradução Bidirecional

O segredo para manter o código limpo e objetivo é extrair a lógica de mapeamento estrutural.

Existem diversas abordagens arquiteturais para realizar esse de/para de dados.

Você pode isolar essa responsabilidade criando um Service para injetar nos componentes, ou até mesmo implementando métodos privados dentro da própria classe do componente que mantém o formulário.

Neste artigo, optamos por utilizar funções puras simplesmente por serem um exemplo prático, direto e extremamente fácil de reutilizar.

A função de tradução para o frontend converte os valores nulos vindos da API em strings vazias seguras que são compatíveis com os campos de formulário HTML.

   function domainModelToFormModel(domain: DomainModel): FormModel {
	return {
		personal: {
			name: domain.personal.name,
			email: domain.personal.email
		},
		address: {
			zipcode: domain.address?.zipcode ?? '',
			street: domain.address?.street ?? '',
			number: domain.address?.number ?? ''
		}
	}
}

O processo inverso garante que o servidor não receba strings vazias caso o bloco de endereço inteiro não tenha sido preenchido pelo usuário.

   function formModelToDomainModel(form: FormModel): DomainModel {
	const isAddressEmpty = Object.values(form.address).every((value) => value === '')

	return {
		personal: {
			name: form.personal.name,
			email: form.personal.email
		},
		address: isAddressEmpty
			? null
			: {
					zipcode: form.address.zipcode,
					street: form.address.street,
					number: form.address.number
				}
	}
}

Imagem com o texto: Curso Formulários com Angular e com a logo do Angular e um formulário. Logo abaixo existe um botão com o texto "Eu quero!"

Reatividade Avançada com Signal Forms e rxResource

Com a lógica de tradução resolvida, o próximo passo é conectar essas rotinas ao fluxo de dados reativo do Angular.

Utilizamos a abstração rxResource especificamente porque o HttpClient do Angular retorna Observables por padrão.

Se a sua aplicação utilizasse a API nativa fetch baseada em Promises, ou se você convertesse os Observables para Promises internamente, a função resource padrão seria a escolha adequada.

A função linkedSignal atua como a ponte reativa ideal para conectar a resposta da API diretamente ao estado inicial do Signal Form.

   import { JsonPipe } from '@angular/common'
import { HttpClient } from '@angular/common/http'
import {
	ChangeDetectionStrategy,
	Component,
	inject,
	linkedSignal,
	resource,
	signal
} from '@angular/core'
import { rxResource } from '@angular/core/rxjs-interop'
import { form, FormField } from '@angular/forms/signals'

@Component({
	selector: 'app-translate-model',
	imports: [FormField, JsonPipe],
	templateUrl: './translate-model.component.html',
	styleUrl: './translate-model.component.css',
	changeDetection: ChangeDetectionStrategy.OnPush
})
export class TranslateModel {
	private httpClient = inject(HttpClient)

	// Define o estado inicial vazio para evitar inconsistências antes do carregamento
	private emptyFormModel = signal<FormModel>({
		personal: {
			name: '',
			email: ''
		},
		address: {
			zipcode: '',
			street: '',
			number: ''
		}
	})

	// Gerencia a requisição assíncrona baseada em Observables
	protected profileDataRef = rxResource({
		stream: () => this.getProfile()
	})

	// Cria um signal que reage automaticamente às mudanças do rxResource
	protected formModel = linkedSignal<FormModel>(() => {
		// Garante que o formulário inicie vazio enquanto aguarda a API
		if (!this.profileDataRef.hasValue()) {
			return this.emptyFormModel()
		}

		// Aplica a tradução puramente funcional assim que os dados chegam
		return domainModelToFormModel(this.profileDataRef.value())
	})

	// Instancia o formulário conectando-o ao linkedSignal
	protected form = form(this.formModel)

	protected save() {
		// Traduz o Form Model de volta para o Domain Model antes de enviar
		const payload = formModelToDomainModel(this.formModel())

		this.saveProfile(payload).subscribe((profile) => {
			console.log('ok', profile)
		})
	}

	private saveProfile(payload: DomainModel) {
		return this.httpClient.put<DomainModel>('http://localhost:3000/profile', payload)
	}

	private getProfile() {
		return this.httpClient.get<DomainModel>('http://localhost:3000/profile')
	}
}

Conclusão

A separação clara entre Form Models e Domain Models é uma excelente prática consolidada na arquitetura avançada de aplicações Angular.

Ao utilizar a API de Signal Forms, essa distinção evita comportamentos inesperados na interface de usuário e garante que a API receba os dados processados exatamente no formato esperado.

Com o uso inteligente de recursos como linkedSignal e a aplicação de funções de mapeamento flexíveis, o fluxo de dados torna-se altamente previsível e tipado rigorosamente de ponta a ponta.

Domine este padrão arquitetural fundamental para construir formulários reativos extremamente robustos, com código simplificado e totalmente alinhados com as diretrizes modernas do framework.

Entre na nossa comunidade!

Receba novos posts, novidades do ecossistema Angular e muito mais.

Sobre o autor

Author's photo
Henrique Custódia Arquiteto Frontend, entusiasta Angular, cat lover, criador de conteúdo e fundador da Code Dimension!