Fora abstrações, existem 3 classes para trabalhar com formulários no Angular.
FormControl
FormGroup
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.
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:
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 |
|| ||
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]>
}
E veja, funciona mesmo! 🙂
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>
}
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)
}
}
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)
}
}
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)