DEV Community

Guilherme Siquinelli
Guilherme Siquinelli

Posted on

1 1 1

Angular Typed Forms, a melhor forma de tipar seus formulários

Fora abstrações, existem 3 classes para trabalhar com formulários no Angular.

  1. FormControl
  2. FormGroup
  3. FormArray

Geralmente nossos formulários representam alguma entidade ou modelo da nossa aplicação, principalmente quando falamos de CRUDs, como você pode ver no vídeo.

Porém, o vídeo mostra um exemplo de formulário onde os tipos são inferidos pelo formulário já estarem preenchidos, mas como sabemos, a vida real não é assim que acontece, quem preenche o formulário são os usuários ou o banco de dados. Não acaba aqui, para adicionar a tipagem no formulário não basta apenas colocar nossa classe ou interface como generics, pois o FormGroup não aceita.

Image description

Fora que ainda sim, os tipos que importam de verdade, os relacionados a nossas interfaces, ficaram como any.

🤷‍♂️

Nós não queremos saber se é um FormGroup ou um FormControl, o que queremos saber é:

Isso é uma string ou um número?

Para que funcione a interface do mundo real, seria algo assim:

Image description

Ou seja, precisamos criar uma segunda interface se adequando.

Praticamente inviável na minha humilde opinião. 🤨

  ___________________________________
 /                                   \
(  mas como isso pode ser resolvido?  )
 \___________________________________/
         \   ^__^
          \  (oo)\_______
             (__)\       )\/\
                 ||----w |
                 ||     ||
Enter fullscreen mode Exit fullscreen mode

Como não encontrei nada parecido na documentação, tive de escrever eu mesmo um tipo que contorne esta dificuldade, que permite setar a interface no FormGroup root e tudo resolvido, e apesar deu ter usado recursividade, ficou mais simples do que imaginei que pudesse ficar.

import {FormArray, FormControl, FormGroup} from '@angular/forms'

export type DetectType<T> = T extends Array<infer U>
  ? FormArray<DetectType<U>>
  : T extends object
  ? FormGroup<TypedForm<T>>
  : FormControl<T>

export type TypedForm<T> = {
  [K in keyof T]: DetectType<T[K]>
}
Enter fullscreen mode Exit fullscreen mode

E veja, funciona mesmo! 🙂

Image description

Bom, já vimos que funciona bem com o exemplo de tipo mostrado na documentação, agora vamos adentrar um pouco mais a vida real...

É uma prática comum que objetos como endereço e opções estejam segmentados em outros tipos separados, pois podem ser reutilizados em outras partes da aplicação, desta forma:

type Address = {
  number: number
  street: string
}

type FoodOption = {
  food: string
  price: number
}

type CoolParty = {
  address: Address
  forma1: boolean
  foodOptions: Array<FoodOption>
}
Enter fullscreen mode Exit fullscreen mode

E bom, se o tipo pode ser reaproveitado em outros lugares da aplicação, o formulário também, certo? Minha recomendação é que estes formulários sejam quebrados em partes e usados em conjunto.

É desta forma que costumo fazer:

export class AddressForm extends FormGroup<TypedForm<Address>> {
  constructor() {
    super({
      number: new FormControl(),
      street: new FormControl(),
    })
  }
}

export class FoodOptionForm extends FormGroup<TypedForm<FoodOption>> {
  constructor() {
    super({
      food: new FormControl(),
      price: new FormControl(),
    })
  }
}

export class PartyForm extends FormGroup<TypedForm<CoolParty>> {
  constructor() {
    super({
      address: new AddressForm(),
      forma1: new FormControl(),
      foodOptions: new FormArray<FoodOptionForm>([]),
    })
  }

  addOption() {
    this.controls.foodOptions.push(new FoodOptionForm())
  }

  removeOption(index: number) {
    this.controls.foodOptions.removeAt(index)
  }
}
Enter fullscreen mode Exit fullscreen mode

Isso mesmo, orientação a objetos funciona muito bem pra isso!

E repare outro benefício, na classe PartyForm, criei os métodos addOption e removeOption, que adiciona uma nova opção ou remove do array foodOptions, isso será útil na implementação.

Dá pra melhorar ainda?

Dá sim, vamos facilitar o momento de alteração dos dados, permitindo preencher os dados já no momento da criação da instância.

export class AddressForm extends FormGroup<TypedForm<Address>> {
  constructor(address?: Partial<Address>) {
    super({
      number: new FormControl(),
      street: new FormControl(),
    })

    if (address) {
      this.patchValue(address)
    }
  }
}

export class FoodOptionForm extends FormGroup<TypedForm<FoodOption>> {
  constructor(option?: Partial<FoodOption>) {
    super({
      food: new FormControl(),
      price: new FormControl(),
    })

    if (option) {
      this.patchValue(option)
    }
  }
}

export class PartyForm extends FormGroup<TypedForm<CoolParty>> {
  constructor(party?: Partial<CoolParty>) {
    super({
      address: new AddressForm(),
      forma1: new FormControl(),
      foodOptions: new FormArray<FoodOptionForm>([]),
    })

    if (party) {
      this.patchValue(party)
    }
  }

  addOption(option?: Partial<FoodOption>) {
    this.controls.foodOptions.push(new FoodOptionForm(option))
  }

  removeOption(index: number) {
    this.controls.foodOptions.removeAt(index)
  }
}
Enter fullscreen mode Exit fullscreen mode

Recomendo esta forma de trabalhar com formulário, fica bem prático!

Espero que essa dica seja útil a você leitor.

Um abraço

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

AWS GenAI LIVE!

GenAI LIVE! is a dynamic live-streamed show exploring how AWS and our partners are helping organizations unlock real value with generative AI.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️