DEV Community

Guilherme Siquinelli
Guilherme Siquinelli

Posted on

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

Top comments (0)