Angular: As formas reativas não são fortemente tipadas

Criado em 30 dez. 2016  ·  90Comentários  ·  Fonte: angular/angular

[x] feature request
  • Versão angular: 2

Os formulários reativos devem ser usados ​​em formulários complexos, mas os valueChanges do controle são Observable<any> , o que é totalmente contra as boas práticas para códigos complexos.

Deve haver uma maneira de criar controles de formulário fortemente tipados.

forms feature high

Comentários muito úteis

Ei, gostaria de compartilhar uma atualização da equipe Angular: ouvimos que este é um grande ponto de dor. Em breve começaremos a trabalhar em formulários de digitação mais forte, o que incluirá examinar os PRs existentes e revisar todos os seus comentários novamente. Obrigado a todos por dispensarem seu tempo para deixar seus pensamentos!

Todos 90 comentários

relacionado # 11279

Isso não está relacionado a # 11279.

Por favor, explique como não está relacionado?
O que você deseja é que o Abstract Control seja genérico, certo? Essa é a única maneira que valueChanges pode ter um tipo que não seja Observable<any> não haveria outra maneira de inferir o tipo.
O que é exatamente o que # 5404 está perguntando, o que significa que está relacionado com # 11279
Se houver outra maneira de implementar isso sem tornar AbstractControl um genérico, explique-o.

Usar get<Type> como em # 11279 é definitivamente a solução errada. Se o TypeScript tivesse algo como Java Unbounded Wildcard, get o usaria, e não any . Talvez algo possa ser feito da mesma maneira com uma interface vazia?

Também existe https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html keyof . Os recursos do TypeScript 2.1 podem ser muito interessantes para estudar para implementar controles de formulário fortemente tipados.

Do jeito que está atualmente, infelizmente, não acho que seja utilizável para aplicativos grandes e preciso projetar algo em cima dele.

Acabei de notar que no TS 2.2 (https://github.com/Microsoft/TypeScript/wiki/Roadmap#22-february-2017) que eles planejaram tipos genéricos padrão (https://github.com/Microsoft/TypeScript/ questões / 2175) assim que tivermos isso, acho que pode ser uma boa ideia revisitar esse problema, já que poderíamos fazer AbstractControl genérico como AbstractControl<T = any> onde T é o tipo de o valor retornado por valueChanges que seria um Observable<T> . Não seria uma boa ideia fazê-lo atualmente, uma vez que seria uma alteração significativa, mas com os genéricos padrão, a menos que eu os interprete mal, não será uma alteração significativa.

Uma pequena atualização sobre isso, parece que o Default Generics foi movido para o TS2.3 . Portanto, com o suporte do TS 2.1 da Angular com a versão 4.0, não demorará muito para que eles sejam capazes de oferecer suporte ao TS 2.3, que é agora que devemos esperar para revisitar isso.

TypeScript 2.3 com Default Generic Types já está aqui, temos algum plano quando o suporte para TS 2.3 no angular estará pronto?

@desfero aguardando # 16707 para que a compilação seja atualizada para TS2.3

1 adoraria ver esse recurso. Alguém está trabalhando nisso?

Pequena atualização sobre isso:
De acordo com meu comentário aqui: https://github.com/angular/angular/pull/16828#issuecomment -337034655
implementar genéricos na API de formulários atual não é possível sem interromper as alterações.
Portanto, as alterações de quebra são necessárias
Ou uma reescrita de formulários completos

Acontece que meu comentário anterior estava incorreto.
Consegui implementar isso na API de formulários atual, como você pode ver aqui # 20040

@Toxicable que ainda tem o problema de não ser capaz de refatorar com segurança. get ('pessoa') por exemplo, não está realmente usando o próprio símbolo. O exemplo acima, de @rpbeukes , tem uma forma adaptada de usar basicamente o símbolo do objeto, por exemplo. get (obj.person) sem usar a string. Isso seria preferível a apenas ter tipos de retorno.

@howiempt

get ('pessoa') por exemplo, não está realmente usando o próprio símbolo

Não tenho ideia do que você quer dizer com isso, a que símbolo você está se referindo aqui?
Na minha implementação, você pode fazer algo como

let g = new FormGroup({
  'name': new FormControl('Toxicable'),
  'age': new FormControl(22),
})

g.get('name') //AbstractControl<string>
g.get('age') //AbstractControl<number>

get (obj.person) sem usar a string

Isso não tem a capacidade de atravessar vários FormGroups.
Embora meu método seja incapaz de inferir os tipos neste cenário, a ideia do meu PR é adicionar tipos genéricos sem interromper as alterações ou introduzir qualquer nova API (além dos genéricos)

@Toxicable Eu entendo que sua mudança tem por objetivo não quebrar as coisas, não tentar criticar sua solução. A outra implementação (adaptada) permite que uma propriedade real seja usada em vez de uma string. Ao referenciar o campo por string, se o nome da propriedade mudar, o builds break, o que para mim não é muito seguro. Por exemplo, alterar o nome do campo de 'nome' para 'primeiroNome' quebraria se eu não alterasse todas as referências g.get ('nome'). Se eu pudesse fazer algo como

class PersonDetails {
  name: string;
  age: number;
}
let g = new FormGroup<PersonDetails>({
  name: new FormControl('Toxicable'),
  age: new FormControl(22),
})

g.get(name) //AbstractControl<string>
g.get(age) //AbstractControl<number>

Todas seriam referências justas. A solução de retrofit faz isso de uma forma um tanto "hacky", mas também resolve o problema.

@Toxicable thanx para o PR. Estou ansioso para usá-lo :)

Eu concordo com @howiempt , se conseguirmos algo assim, seria o primeiro prêmio:

g.get(x => x.name) //AbstractControl

Novamente, eu realmente não sei o quão viável isso é dentro do escopo maior das coisas.
Eu confio no seu julgamento.

Continue com o bom trabalho e obrigado pela resposta rápida.

Acho que esse método de acessar outros controles não está relacionado à adição de genéricos.
Sinta-se à vontade para abrir outra edição sobre isso

Eu realmente não acho que ter o tipo de retorno definido é realmente "fortemente tipado", parece que metade da implementação necessária, mas é um passo na direção certa.

Olá, eu lancei https://github.com/Quramy/ngx-typed-forms para uma solução alternativa para esse problema. Por favor, dê uma olhada 😄

@Quramy Tentei usar seu pacote há várias semanas e, pelo que me lembro, ele realmente não faz muita força :(

+1. Não consigo contar a quantidade de instâncias em que desejei que fosse implementado.

Mesmo.
As formas reativas angulares são um dos recursos que realmente superam qualquer outra estrutura existente. Tornar formas reativas fortemente tipadas o levaria para o próximo nível, ampliando ainda mais a lacuna para a competição :)

isso pode ser feito com tipos mapeados condicionais e recursão ... os tipos mapeados condicionais acabaram de ser mesclados no texto digitado. Se isso for publicado, teremos a chance de tornar possíveis os formulários de digitação fortes

Mal posso esperar (

A solução FormBuilder . Além disso, FormGroup<T> , FormControll<T> e FormArray<T> genéricos não podem ser usados ​​diretamente, porque são apenas interfaces que não estendem por evento AbtractControl<T> . Isso não foi suficiente para o nosso projeto atual.

Com ngx-strong-typed-forms eu mesmo não
Ele quebra um pouco com a compatibilidade com versões anteriores, porque não usa genéricos padrão. Portanto, ele força você a dar explicitamente a seus controles um tipo qualquer, mas adiciona muito mais segurança de tipos a todas as outras partes e é compatível com a API da implementação atual de @ angular / forms.
Talvez esta seja uma alternativa válida até que esse recurso seja implementado no Angular.

+1 Este é um recurso poderoso ..

Deve ser implementado o mais rápido possível)

A forma como deve ser codificado

Há uma implementação interessante neste software de código aberto (AGPL)

https://github.com/concentricsky/badgr-ui/blob/master/src/app/common/util/typed-forms.ts

Usei isso como uma solução temporária:
Espero que ajude

import { FormControl, AbstractControl } from "@angular/forms";
export class NumberControl extends FormControl{
    _value: Number;
    get value(){ 
        return this._value;
    }
    set value(value){
        this._value = Number(value);
    }
}

Isso é algo que pensei algumas vezes em projetos em que trabalhei, mas não usei proxies JavaScript o suficiente para saber o impacto no desempenho que isso teria em qualquer coisa que observasse esses valores.

Simplesmente criei uma solução alternativa personalizada no nível do FormBuilder:

import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
type ExtraProperties = { [key: string]: any } | null;
interface IModelConstructor { new(...args: any[]): any }

@Injectable({
    providedIn: 'root'
})
export class TypedFormBuilder {
    array = this.formBuilder.array;
    control = this.formBuilder.control;
    group = this.formBuilder.group;

    constructor(private formBuilder: FormBuilder) {}

    typedGroup<TGroupModel extends object>(ModelType: IModelConstructor, extraProps?: ExtraProperties): FormGroup & TGroupModel {
        const formGroup: any = this.group({
            ...new ModelType(),
            ...extraProps
        });

        return new Proxy<FormGroup & TGroupModel>(formGroup, {
            get: (target: FormGroup & TGroupModel, propName: string | number | symbol) => {
                if (propName in target) {
                    return target[propName];
                } else {
                    const property = target.get(propName as string);
                    return property && property.value;
                }
            },
            set: (target: FormGroup & TGroupModel, propName: string | number | symbol, value: any, _: any) => {
                if (propName in target) {
                    target[propName] = value;
                } else {
                    target.setValue({ [propName]: value });
                }

                return true;
            }
        });
    }
}

Essa solução definitivamente não é superpolida e provavelmente seria melhor ter o FormGroup gerado para acessar os valores em uma propriedade (como myGroup.fields, onde "campos" seria o tipo fornecido). Mas isso fornece uma digitação forte. Para usá-lo:

generateFormGroup() {
    this.theGroup = builder.typedGroup<MyModel>(MyModel);
    this.theGroup.someProperty = 5;
}

Ganhei experiência nos últimos meses com formulários digitados, usando minhas digitações de formulários no meu projeto atual. Ele oferece grande valor ao trabalhar em um projeto com três equipes de desenvolvedores e todos trocaram porque era muito barato fazer isso.

Mas eu quero discutir se é a decisão certa apenas colocar genéricos na API atual. Ao construir os tipos para todos os controles de formulário, encontrei muitos casos extremos e coisas que são impossíveis ou difíceis de digitar, porque acho que a digitação estática não era possível naquele momento e, portanto, não era uma das maiores preocupações.
Infelizmente, isso visa a funcionalidade principal com AbstractControl#value , que deve ser algo como DeepPartial<T> ou AbstractControl#get com implementações diferentes para cada subclasse.
Ser compatível com versões anteriores também perde parte da segurança de tipo causada por casos de queda com tipos any .
Talvez considerar uma nova API para formas reativas também seja uma opção para este problema?

Então, foi isso que acabei fazendo enquanto uma solução real acontecia.

Isenção de responsabilidade ... Acabei de começar no Angular, mas estou bastante familiarizado com o Typescript, então não entendo totalmente as formas reativas ... aqui está o que acabou funcionando para mim, mas é claro que não está totalmente completo, eu apenas digitei FormGroup, mas Tenho certeza de que mais coisas precisarão ser digitadas à medida que eu aprender mais sobre os formulários ...

import { FormGroup } from '@angular/forms';

export class FormGroupTyped<T> extends FormGroup {
  public get value(): T {
    return this.value;
  }
}

e então eu posso usar assim

import { FormGroupTyped } from 'path/to/your/form-group-typed.model';

interface IAuthForm {
  email: string;
  password: string;
}

const authForm: FormGroupTyped<IAuthForm> = fb.group({
  email: ['', [Validators.required]],
  password: ['', [Validators.required]],
});

const formValues: IAuthForm = this.authForm.value;
const email: string = formValues.email; 
const invalidKeyVar: string = formValues.badBoy; // [ts] Property 'badBoy' does not exist on type 'IAuthForm'. [2339]
const invalidTypeVar: number = formValues.password; // [ts] Type 'string' is not assignable to type 'number'. [2322]

@Toxicable Lol estava procurando exatamente esse problema! haha

@cafesanu Aqui está uma pequena melhoria em seu FormGroup digitado para verificar o construtor.

import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, FormGroup, ValidatorFn } from '@angular/forms';

export class FormGroupTyped<T> extends FormGroup {
  readonly value: T;
  readonly valueChanges: Observable<T>;

  constructor(controls: { [key in keyof T]: AbstractControl; },
              validatorOrOpts?: ValidatorFn | Array<ValidatorFn> | AbstractControlOptions | null,
              asyncValidator?: AsyncValidatorFn | Array<AsyncValidatorFn> | null) {
    super(controls, validatorOrOpts, asyncValidator);
  }

  patchValue(value: Partial<T> | T, options?: {
    onlySelf?: boolean;
    emitEvent?: boolean;
  }): void {
    super.patchValue(value, options);
  }

  get(path: Array<Extract<keyof T, string>> | Extract<keyof T, string>): AbstractControl | never {
    return super.get(path);
  }
}

uso:

export class SearchModel {
  criteria: string;
}

const searchForm = new FormGroupTyped<SearchModel >({
  criteria: new FormControl('', Validators.required) // OK
  badBoy: new FormControl('', Validators.required)  // TS2345: Object literal may only specify known properties, and 'badBoy' does not exist in type '{ criteria: AbstractControl; }'.
});

Eu escrevi um pequeno wrapper em torno de FromControl que permite gerar dinamicamente o tipo de dados com base em chamadas para um construtor: https://github.com/concentricsky/badgr-ui/blob/develop/src/app/common/util/ typed-forms.ts

O tipo construído dinamicamente garante que a forma do formulário corresponda às suas expectativas, em vez de ter que pré-declarar o tipo de interface e então esperar que você faça as chamadas certas para criar o formulário.

Adoraria ver algo semelhante a isso no Angular em algum momento.

O uso é parecido com este:

// Create a typed form whose type is dynamically constructed by the builder calls
const group = new TypedFormGroup()
    .add("firstName", typedControl("", Validators.required))
    .add("lastName", typedControl("", Validators.required))
    .add(
        "address",
        typedGroup()
            .add("street", typedControl("2557 Kincaid"))
            .add("city", typedControl("Eugene"))
            .add("zip", typedControl("97405"))
    )
    .addArray(
        "items",
        typedGroup()
            .addControl("itemName", "")
            .addControl("itemId", 0)
    )
;

// All these are type checked:
group.value.address.street.trim();
group.controls.firstName.value;
group.untypedControls.firstName.value;
group.value.items[0].itemId;

Olá a todos, nos últimos 3 dias eu estava experimentando o uso de um d.ts gist para definir uma definição mais rígida para as classes ReactiveForms, criando uma nova interface digitada compatível com a classe angular original.
Acho que pode ser uma possível solução / alternativa para o seu problema 😉

//BASIC TYPES DEFINED IN @angular/forms + rxjs/Observable
type FormGroup = import("@angular/forms").FormGroup;
type FormArray = import("@angular/forms").FormArray;
type FormControl = import("@angular/forms").FormControl;
type AbstractControl = import("@angular/forms").AbstractControl;
type Observable<T> = import("rxjs").Observable<T>;

type STATUS = "VALID" | "INVALID" | "PENDING" | "DISABLED"; //<- I don't know why Angular Team doesn't define it https://github.com/angular/angular/blob/7.2.7/packages/forms/src/model.ts#L15-L45)
type STATUSs = STATUS | string; //<- string is added only becouse Angular base class use string insted of union type https://github.com/angular/angular/blob/7.2.7/packages/forms/src/model.ts#L196)

//OVVERRIDE TYPES WITH STRICT TYPED INTERFACES + SOME TYPE TRICKS TO COMPOSE INTERFACE (https://github.com/Microsoft/TypeScript/issues/16936)
interface AbstractControlTyped<T> extends AbstractControl {
  // BASE PROPS AND METHODS COMMON TO ALL FormControl/FormGroup/FormArray
  readonly value: T;
  valueChanges: Observable<T>;
  readonly status: STATUSs;
  statusChanges: Observable<STATUS>;
  get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
  setValue<V>(value: V extends T ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  patchValue<V>(value: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  reset<V>(value?: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}

interface FormControlTyped<T> extends FormControl {
  // COPIED FROM AbstractControlTyped<T> BECOUSE TS NOT SUPPORT MULPILE extends FormControl, AbstractControlTyped<T>
  readonly value: T;
  valueChanges: Observable<T>;
  readonly status: STATUSs;
  statusChanges: Observable<STATUS>;
  get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
  setValue<V>(value: V extends T ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  patchValue<V>(value: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  reset<V>(value?: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}
interface FormGroupTyped<T> extends FormGroup {
  // PROPS AND METHODS SPECIFIC OF FormGroup
  //controls: { [P in keyof T | string]: AbstractControlTyped<P extends keyof T ? T[P] : any> };
  controls: { [P in keyof T]: AbstractControlTyped<T[P]> };
  registerControl<P extends keyof T>(name: P, control: AbstractControlTyped<T[P]>): AbstractControlTyped<T[P]>;
  registerControl<V = any>(name: string, control: AbstractControlTyped<V>): AbstractControlTyped<V>;
  addControl<P extends keyof T>(name: P, control: AbstractControlTyped<T[P]>): void;
  addControl<V = any>(name: string, control: AbstractControlTyped<V>): void;
  removeControl(name: keyof T): void;
  removeControl(name: string): void;
  setControl<P extends keyof T>(name: P, control: AbstractControlTyped<T[P]>): void;
  setControl<V = any>(name: string, control: AbstractControlTyped<V>): void;
  contains(name: keyof T): boolean;
  contains(name: string): boolean;
  get<P extends keyof T>(path: P): AbstractControlTyped<T[P]>;
  getRawValue(): T & { [disabledProp in string | number]: any };
  // COPIED FROM AbstractControlTyped<T> BECOUSE TS NOT SUPPORT MULPILE extends FormGroup, AbstractControlTyped<T>
  readonly value: T;
  valueChanges: Observable<T>;
  readonly status: STATUSs;
  statusChanges: Observable<STATUS>;
  get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
  setValue<V>(value: V extends T ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  patchValue<V>(value: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  reset<V>(value?: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}

interface FormArrayTyped<T> extends FormArray {
  // PROPS AND METHODS SPECIFIC OF FormGroup
  controls: AbstractControlTyped<T>[];
  at(index: number): AbstractControlTyped<T>;
  push<V = T>(ctrl: AbstractControlTyped<V>): void;
  insert<V = T>(index: number, control: AbstractControlTyped<V>): void;
  setControl<V = T>(index: number, control: AbstractControlTyped<V>): void;
  getRawValue(): T[];
  // COPIED FROM AbstractControlTyped<T[]> BECOUSE TS NOT SUPPORT MULPILE extends FormArray, AbastractControlTyped<T[]>
  readonly value: T[];
  valueChanges: Observable<T[]>;
  readonly status: STATUSs;
  statusChanges: Observable<STATUS>;
  get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
  setValue<V>(value: V extends T[] ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  patchValue<V>(value: V extends Partial<T>[] ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  reset<V>(value?: V extends Partial<T>[] ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}

Teste de uso no stackblitz - baixe o código e execute no VSCode local Não sei por que no stackblitz os ERRORS para setValue e pathValue não estão corretos ...

Neste tópico do Twitter , discuto com @IgorMinar algumas "ideias para o futuro" (após V8 +)

Quaisquer comentários, sugestões e ajuda são muito bem vindos !

Minha solução se chama @ng-stack/forms . Um dos recursos interessantes é a detecção automática de tipos de formulário.

Portanto, não há necessidade de fazer isso em seus componentes:

get userName() {
  return this.formGroup.get('userName') as FormControl;
}

get addresses() {
  return this.formGroup.get('addresses') as FormGroup;
}

Agora faça isso:

// Note here form model UserForm
formGroup: FormGroup<UserForm>;

get userName() {
  return this.formGroup.get('userName');
}

get addresses() {
  return this.formGroup.get('addresses');
}

Para obter mais informações, consulte @ ng-stack / forms

Ei, temos trabalhado com @zakhenry recentemente em 2 coisas:

  • Melhorar os tipos de formulário
  • Melhorar a maneira de gerenciar subformulários (dentro de subcomponentes)

Não cobrimos todos os tipos como @dmorosinotto , mas temos alguma segurança que definitivamente ajudaria ao fazer refatores (para ts e html: tada :).

Se alguém quiser dar uma olhada no sub-formulário lib:

Olá @ maxime1992 , olhei para Controls<T> mas não restringe AbstractControl to <T[K]>
Posso perguntar por quê? Você o deixa "sem tipo" porque sente a necessidade de alterar o tipo em tempo de execução usando algo como os métodos setControl ou registerControl para redefinir e alterar o FormControl e talvez o tipo associado?
Porque meu TypedForms.d.ts é um pouco mais rígido e "força" o AbstractControlTyped to the type T<P> mas eu não sei se com este tipo de escolha forçar / desabilitar algo que na API ReactiveForms original é permitido e talvez usado por alguém...

Qualquer pensamento? Algum caso real a ser considerado?
Qualquer comentário sobre isso pode me ajudar a decidir sobre como mudar as definições que criei e o PR no qual estou trabalhando ...
Obrigado

PS: Ótimo trabalho no ngx-sub-form 👍 a ideia de usar ControlValueAccessor para lidar com subformulários é algo que eu também estava experimentando 😉

Para @ ng-stack / forms adicionado suporte à validação digitada.

Classes FormControl , FormGroup , FormArray e todos os métodos de FormBuilder
aceite o "modelo de validação de erro" como segundo parâmetro para um genérico:

class ValidationModel {
  someErrorCode: { returnedValue: 123 };
}
const control = new FormControl<string, ValidationModel>('some value');
control.getError('someErrorCode'); // OK
control.errors.someErrorCode; // OK
control.getError('notExistingErrorCode'); // Error: Argument of type '"notExistingErrorCode"' is not...
control.errors.notExistingErrorCode; // Error: Property 'notExistingErrorCode' does not exist...

Por padrão usado um tipo especial chamado ValidatorsModel .

const control = new FormControl('some value');
control.getError('required'); // OK
control.getError('email'); // OK
control.errors.required // OK
control.errors.email // OK
control.getError('notExistingErrorCode'); // Error: Argument of type '"notExistingErrorCode"' is not...
control.errors.notExistingErrorCode // Error: Property 'notExistingErrorCode' does not exist...

ValidatorsModel contém uma lista de propriedades extraídas de typeof Validators e tipos de retorno esperados:

class ValidatorsModel {
  min: { min: { min: number; actual: number } };
  max: { max: { max: number; actual: number } };
  required: { required: true };
  requiredTrue: { required: true };
  email: { email: true };
  minLength: { minlength: { requiredLength: number; actualLength: number } };
  maxLength: { requiredLength: number; actualLength: number };
  pattern: { requiredPattern: string; actualValue: string };
}

@dmorosinotto Muito obrigado mas não estou conseguindo usar
Validators.compose([Validators.required, Validators.maxLength(20), Validators.minLength(3)])

Olá @youssefsharief, você pode me dar mais detalhes sobre seu problema com os validadores? Que tipo de erro / problema você encontrou ao usar o .d.ts?

Se você puder postar um código de amostra ou um stackblitz, eu observarei e tentarei ajudar a resolver o problema se eu encontrar uma solução 😉

Para @ng-stack/forms adicionou suporte para input[type="file"] .

Veja também o exemplo em stackblitz

Eu também usei a abordagem "clássica" com o uso de FormData para fazer upload de arquivos, mas há vários lucros:

  • recupera automaticamente o nome de entrada do formulário, por exemplo, <input type="file" name="someName"> -> formControl.value com someName nome do campo;
  • suporte o atributo multiple com a configuração adequada do nome do campo (observe userpic[] aqui)
<input type="file" multiple name="userpic" [formControl]="formControl">

`` `ts
formData.append ('userpic []', myFileInput.files [0]);
formData.append ('userpic []', myFileInput.files [1]);

- `Validators` have four static methods for files
```ts
import { Validators } from '@ng-stack/forms';

Validators.fileRequired;
Validators.fileMaxSize(1024);
Validators.filesMaxLength(10);
Validators.filesMinLength(2);

@KostyaTretyak pelo que vi até agora, seu pacote parece ser uma ótima solução para os atuais problemas de digitação em Angular Reactive Forms. Já pensou em contribuir para o próprio Angular em vez de fazer sua própria implementação?

Estamos quase no angular 8 e ainda não temos formulários digitados prontos para uso em um ambiente 100% TypeScript, o que parece muito estranho para mim.

@kroeder , acabei de ver que o problema atual foi criado há mais de dois anos (quase imediatamente após o lançamento do Angular 2) e vejo que Pull Request semelhante foi criado há 1,5 anos sem mesclagem ...

Mas se @ng-stack/forms for popular e compatível com Angular 8+, talvez eu crie o Pull Request no futuro.

@KostyaTretyak Parece ótimo :) Tenho usado https://github.com/no0x9d/ngx-strongly-typed-forms que é mencionado anteriormente neste tópico e criado por @ no0x9d Como sua biblioteca difere disso?

@ZNS , não sei, mas após uma revisão rápida, acho que não há nenhum genérico para validação em ngx-strongly-typed-forms , bem como Detectar automaticamente os tipos apropriados para controles de formulário . Talvez eu esteja errado.

@ZNS @KostyaTretyak Olá. Como mencionado acima, sou o autor de ngx-fortemente-tipado-formulários .
Fiz uma revisão rápida do conjunto de recursos de @ng-stack/forms e acho que há algumas pequenas diferenças.

Existe uma diferença conceitual entre quase todas as soluções ou soluções alternativas e meu projeto.
Na maioria dos casos, o Angular FormControl original é estendido ou encapsulado por algum Proxy. Alguns métodos são substituídos por outras assinaturas de tipo e delegados à função original.
Isso introduz uma nova camada com um impacto de desempenho negligenciável, mas mais importante é o código que deve ser mantido e pode introduzir bugs em seu projeto.
Em minha opinião, isso é desnecessário para verificações estáticas em tempo de compilação. A única parte do tempo de execução é o NgModule que fornece meu FormBuilder digitado, que é na verdade o Angular FormBuilder original. Todo o resto é apenas código angular.

Em comparação direta, não tenho o ValidationModel e a conversão de objetos em FormGroups e Arrays em FormArrays, mas fiz algumas alterações opinativas em AbstractControl#get para torná-lo mais seguro para tipos e ter argumentos de validador digitados.

Com algumas pequenas adições, meu código pode ser compatível com versões anteriores e eu poderia criar uma solicitação de pull. Mas uma solicitação de pull semelhante fica obsoleta por muito tempo.
Mas se houver esforços para conseguir isso no Angular, ficaria feliz em unir forças. Por outro lado, eu adoraria ver uma nova API para formulários que seja melhor projetada para ser digitada de forma estrita. Veja meu comentário para detalhes.

1 adoraria ver esse recurso. Alguém está trabalhando nisso?

colidir com a equipe angular?

Eles não se importam com os desenvolvedores. Eles têm sua própria lista de pendências, com blackjack e recursos incríveis de 5% de redução do tamanho do pacote.

@ Lonli-Lokli

Eles não se importam com os desenvolvedores.

Criticar é fácil. Você se preocupa mais com outros desenvolvedores?
Não vi um PR seu para melhorar os formulários, nem qualquer tipo de comentário construtivo ou RFC para fazer as coisas seguirem em frente: man_shrugging:

Eles têm suas próprias pendências

De jeito nenhum: com medo :!
As pessoas estão priorizando coisas que a empresa _ [quem as paga] _ precisa?
Que pena!
image

incríveis-5% -diminuir os recursos de tamanho do pacote.

Você está claramente falando sobre Ivy e a (atualmente) diferença muito pequena no tamanho do pacote.
No momento, Ivy está em fase experimental e você precisa ativá-la. Que surpresa, as coisas ainda não são perfeitas! :pensando:
Sim, foi informado que o Ivy ajudará a reduzir o tamanho do pacote e permitirá que as ferramentas façam uma melhor agitação da árvore nos aplicativos. E, com sorte, isso acontecerá! Por enquanto, eles estão apenas trabalhando nisso, certificando-se de que não está quebrando nada e podem mais tarde ajudar a ter melhores informações de depuração, compilação incremental em uma base de componente em vez de módulo e agitação de árvore. Mas o ferramental para fazer aquela árvore tremer funcionará mais tarde.

Portanto, tente ser respeitoso e evite criticar as pessoas que estão lhe dando uma estrutura de código aberto de graça. As coisas não estão perfeitas, um trabalho enorme em andamento, sim, parece que alguns problemas foram deixados para trás, mas aquele refatoramento foi necessário e nunca haverá um bom momento para fazer algo tão grande como isso, apenas teve que acontecer em algum momento.

Agora estou fora deste debate porque não quero monopolizar este tópico falando sobre coisas não produtivas. *voa para longe*

@ maxime-allex
Existem muitos outros PRs (386 a partir de agora), você acha que mais um vai mudar alguma coisa?
Falando nesse problema, esse PR relacionado (https://github.com/angular/angular/pull/20040) ainda não foi mesclado.

Por falar em refatoração, Ivy foi mencionada há um ano. Eu sei que alguém pode tratar é um recurso importante para desenvolvedores, mas pessoalmente prefiro ver que as correções são importantes para o maior número possível de desenvolvedores.
https://github.com/angular/angular/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

Ainda espero que o Angular seja para desenvolvedores, não para o marketing, e espero ver mais questões fechadas com base na reação da comunidade. Essa é a minha pergunta, quais são as prioridades aqui.

Obviamente, esse problema pode ser resolvido por meio de uma atualização da API existente, mas, separadamente, criei uma proposta para melhorar o ReactiveFormsModule para resolver uma série de problemas pendentes com ele, incluindo este.

Algumas outras questões abordadas incluem a capacidade de assinar atualizações em qualquer propriedade e a capacidade de validar de forma assíncrona um controle por meio de um serviço (em vez de asyncValidator ). Você pode aprender mais em # 31963. O feedback é bem-vindo!

Como prometi antes , criei Pull Request . Este PR contém apenas parte do recurso @ng-stack/forms (sem: validação, detecção automática de controle de formulário e entrada de suporte [arquivo]).

Ei, gostaria de compartilhar uma atualização da equipe Angular: ouvimos que este é um grande ponto de dor. Em breve começaremos a trabalhar em formulários de digitação mais forte, o que incluirá examinar os PRs existentes e revisar todos os seus comentários novamente. Obrigado a todos por dispensarem seu tempo para deixar seus pensamentos!

Oh!!!!! Saindo!

Essa é uma notícia muito boa, equipe Angular, obrigado!
Assim que houver um lançamento, vou descontinuar o angular-typesafe-reattive-forms-helper .

YESSSS !!!!

Eu estou tão animado!! Obrigado, equipe Angular !!

Podemos parar com o spam de reação?
Use os emojis para reações, pois eles são destinados a https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/ - obrigado.

Como a equipe Angular confirmou que trabalhará no Formulário Reativo fortemente tipado. Eu gostaria de compartilhar minhas razões de minha implementação que usa fortemente infer type para extrair o tipo de valor a fim de obter uma experiência de desenvolvimento de tipo estático suave.

1ª Abordagem: Iniciado com Tipo de Valor

Quando comecei a projetar o FormGroup, Usei um tipo de valor simples e intuitivo como T .

FormGroup<{ id: string, username: string | null }>

Mas descobri que quando preciso lidar com uma arquitetura de formulário de tabela complexa, é muito difícil fazer uma vinculação de tipo seguro em Angular HTML.

type User = { id: string, username: string | null }
const table: FormArray<User> = new FormArray([{ id: '0', username: 'gaplo917'}])

const row: FormGroup<User> = table.at(0) as FormGroup<User> // you must do a type cast

// NO way to get type-safe binding in Angular HTML
<input [formControl]="table.at(i).controls.id" />

A implementação acima exigia que os desenvolvedores fizessem uma conversão de tipo customizada, que é entediante e sujeita a erros. IMHO, isso perde completamente a razão de usar a forma reativa fortemente tipada.

2ª abordagem: iniciado com um ControlType

Como usar um tipo de valor simples não está funcionando bem. Tenho outra ideia para usar um KeyValueControlcomo T e use infer extrair o tipo de valor do KeyValueControlrecursivamente.

export type KeyValueControl<V> = {
  [key in keyof V]: V[key] & AbstractControl
}

export type InferTypedForm<Type> = Type extends TypedFormControl<infer X>
  ? X
  : Type extends Array<TypedFormControl<infer X1>>
  ? X1[]
  : Type extends Array<TypedFormGroup<infer X2>>
  ? Array<InferTypedFormGroup<X2>>
  : Type extends TypedFormGroup<infer X3>
  ? InferTypedFormGroup<X3>
  : Type extends TypedFormArray<infer X4>
  ? Array<InferTypedForm<X4>>
  : never

export type InferTypedFormGroup<T> = { [key in keyof T]: InferTypedForm<T[key]> }

export type InferTypedFormArray<T> = InferTypedForm<T[]>

export class TypedFormGroup<T extends KeyValueControl<T>> extends FormGroup {
  readonly controls: T
  readonly valueChanges: Observable<InferTypedFormGroup<T>>
  readonly statusChanges: Observable<FormStatus>
  readonly value: InferTypedFormGroup<T>
  ...
}

Como resultado,

interface Foo {
  first: TypedFormControl<string | null>
  last: TypedFormControl<string | null>
}

const table: FormArray<FormGroup<Foo>>

const row: FormGroup<Foo> = table.at(0) // typescript compile OK!

// type-safe binding in Angular HTML, all auto-complete (`id`, `username`) finally works!
<input [formControl]="table.at(0).controls.id" />

Demonstração ao vivo

Edit gaplo917/angular-typed-form-codesandbox

Capturas de tela do IDE de demonstração ao vivo

Essa é uma verdadeira experiência de autocompletar em formas complexas.
Screenshot 2020-06-12 at 19 02 10

Formulários digitados angulares

100% compatível com os módulos de forma reativa existentes
https://github.com/gaplo917/angular-typed-forms

Comentários e melhorias são bem-vindos. Espero que as próximas formas reativas fortemente tipadas possam lidar com modelos de formulários complexos, como Tabela e Subformulários aninhados.

@IgorMinar , Angular Core Team e Angular Community Members .

Este longo comentário está totalmente focado em duas declarações que são destacadas pelo autor do ticket " contra as práticas " e " fortemente tipadas ".

Eu sugeriria que, em vez de uma abordagem baseada em interface para o Formulário Reativo Fortemente Tipado, deveríamos optar pela Abordagem Baseada em Classes, mas não da maneira que é mencionada nas pilhas / formulários ng . Também não recomendaria alterar a base de código de Angular Ractive Forms, porque podemos obter uma forma fortemente tipada sem alterar a base de código de várias maneiras. Deixe-me descrever em detalhes quais são os desafios de alto nível que vejo na Abordagem Direcionada por Interface e a Abordagem Direcionada por Classe é mais intuitiva do que outras e também estamos obtendo o objeto FormGroup é fortemente tipado. Em ambos os casos, nosso objeto FormGroup é fortemente tipado - não estamos perdendo o poder do TypeScript Type no Class-Based Approach.

Minha sugestão

Como todos nós estamos familiarizados com as práticas OOP, a classe nos dá mais flexibilidade e capacidade de manutenção do código. alguns dos benefícios que estou destacando abaixo:

  1. Desacople nosso código.
  2. Menos código em comparação com a abordagem atual, bem como abordagem orientada por interface.
  3. Podemos usar decoradores personalizados na propriedade.
  4. O código pode ser lido, mantido e extensível.
  5. Com essa abordagem, não podemos precisar escrever a lógica de negócios no modelo, como estamos colocando o *ngIf com várias condições para mostrar as mensagens de erro. Acredito que os modelos não se destinam a escrever a lógica de negócios.
  6. Muito mais...

Deixe-me transformar o código da interface mencionado acima no Class e aplicar o decorador de validação nas propriedades Class . Aqui, estamos seguindo as Práticas do Princípio de Responsabilidade Única . Dê uma olhada no código abaixo:

image

Vamos considerar alguns casos e compará-los com a Abordagem fortemente tipada com orientação por classe e interface que nos ajuda a entender a diferença em ambos.

1. Crie um FormGroup
Aqui, estamos usando a mesma instância de FormBuilder e o mesmo método de group . Mas o nome do módulo de importação será diferente como ReactiveTypedFormsModule vez de ReactiveFormsModule . Vamos criar um FormGroup :
image

De acordo com o código acima, surge a pergunta,

A abordagem atual funcionará após a importação de ReactiveTypedFormsModule ?
Sim, funcionará, nada será alterado após importar o ReactiveTypedFormsModule .

Vamos ver rapidamente os outros casos e concluir este post.

2. Altere o valor de FormControl
Em vez de chamar o método setValue , podemos atribuir diretamente o valor à propriedade Class . Ele definirá automaticamente o FormControl .

image

3. Execute a lógica de negócios com base nas alterações de valor do FormControl
Em vez de assinar o FormControl ValueChanges , use o poder do método setter no TypeScript.

image

4. Converta o valor de entrada
Estamos nos concentrando em fortemente tipado, mas o que dizer dos valores que vêm do controle de entrada, como para data, estamos obtendo o valor no formato String , mas estamos esperando o formato Date no código TS, para superar esse problema, criamos uma diretiva ou método para converter o valor conforme necessário. Não vou mostrar aqui o código atual, pois é um pouco desajeitado, porque temos que criar a diretiva e fazer as coisas blá, blá ... 😄, Então, eu gostaria de mostrar o código do Class Driven Approach aqui:

@toDate() // this will convert the value before assigning the value in 'dob' property.
dob:Date;

4. Altere o valor de FormGroup FormControl aninhado
Podemos atribuir diretamente o valor na respectiva propriedade em vez de obter o Nested FormGroup Object e chamar o método SetValue .

image

5. Adicionando FormGroup em Nested FormArray
Não há mais o que dizer, dê uma olhada no código abaixo 😄.

image

Faça referência ao objeto FormGroup no modelo HTML

Código simples 😄. Nada será alterado no modelo HTML, mas você também obterá mais informações no modelo HTML. Por favor, consulte o código abaixo

<form [formGroup]="user.formGroup">
   <input type="text" formControlName="firstName"/>
   <small >{{user.formGroup.controls.firstName.errorMessage}}</small>
<!--Nested FormGroup-->
   <div [formGroup]="user.address.formGroup">...</div>
<!--Nested FormArray - Skills-->
   <div *ngFor="let skill of user.skills">
       <div [formGroup]="skill.formGroup">...</div>
   </div
</form>

Link de Stackblitz : Exemplo de trabalho de forma reativa de tipo forte
Exemplo no Github : forma reativa fortemente tipada

@ajayojha , corrija-me se eu estiver errado, mas parece que seu comentário acima pode ser reduzido a isto:

  1. Os custos indiretos que acompanham os tipos TypeScript são ruins.
  2. Validação de tempo de execução com a ajuda de decoradores - é bom.
  3. Por que você precisa de setValue() e valueChanges() se houver setters / getters?

O que eu acho:

  1. Escrever tipos TypeScript é como escrever testes estáticos. Alguém pode criar aplicativos sem testes porque acham que é desnecessário, mas é uma prática ruim.
  2. Validação de tempo de execução com a ajuda de decoradores - pode ser uma boa ideia, concordo.
  3. Além de setValue() , também existem patchValue() , reset() , que também funcionam com o valor do formulário. Substituir apenas setValue() por um setter tornará o código inconsistente. Além disso, quando tivermos que escrever setters para cada propriedade do modelo de formulário, isso adicionará muito mais sobrecarga de código, bem como sobrecarga de desempenho no caso de getters. Misturar nomes de propriedades de modelo de formulário com propriedades de controle de formulário também é uma má ideia, na minha opinião.

Obrigado, @KostyaTretyak por suas dúvidas sobre meu comentário, ficaria feliz em responder o mesmo, por favor, veja abaixo meus comentários em conformidade :).

  1. Os custos indiretos que acompanham os tipos TypeScript são ruins.

Apenas para sua informação, o objeto formgroup é fortemente tipado. As interfaces são boas, mas não adequadas para todas as áreas e não estou dizendo que os tipos do TypeScript são ruins, não acho que em algum lugar mencionei que os tipos sejam ruins. Minha única preocupação é com a Interface porque estamos substituindo as práticas de design de software com a abordagem de Interface ou mesmo posso dizer que estamos usando a abordagem de Interface no lugar errado e minha abordagem sugerida é Classe. Pelo que entendi por meio da abordagem de Classe, não estamos comprometendo o benefício dos Tipos TypeScript que obtemos na Interface ou mesmo eu diria que obtemos mais do que a abordagem de Interface em termos de Legibilidade, Escalabilidade e Manutenção.

Estamos usando a prática correta de Interface para forma reativa fortemente tipada?

Deixe-me descrever um pouco mais em termos de Interface é a prática ruim (de acordo com mim) para a forma reativa fortemente tipada.

Tipos de TypeScript são bons, mas não é sugerido que em todos os lugares tenhamos que misturar qualquer coisa que não esteja de acordo com as práticas de software. Como mencionei claramente os problemas com a interface. Só para pensar sobre minhas preocupações destacadas sobre a Interface. Deixe-me compartilhar meu caso, em um dos meus aplicativos corporativos que contém mais de 6k + componentes. Se eu estou indo com a abordagem de interface, a equipe de desenvolvimento vai me fazer boas perguntas antes de fazer as mudanças:

  • Qual interface devemos usar onde, já que a mesma entidade é usada em vários componentes com propriedades diferentes. Então, precisamos criar o Componente de Interface Separado? Se sim, então leia o segundo caso.
  • Se seguirmos a abordagem Acima, qual será a abordagem em que terei de usar as duas propriedades da interface em um ou mais componentes. Para a solução desse problema, posso criar mais uma interface e estender a mesma. É bom criar mais e mais arquivos apenas para agitar a forma reativa de tipo forte? E quanto à manutenibilidade ?, não tenho a resposta, exceto para dizer que o Angular Team está fornecendo essa solução, então é bom :) (se o Angular Team optar pelo Interface Approach).
  • Se formos com a abordagem a + b, em alguns dos meus componentes exigiu algumas propriedades, não todas, então? Tenho três soluções para oferecer à minha equipe de desenvolvimento.

    • Crie uma nova interface e copie / cole as propriedades necessárias na interface recém-criada. Esta é a abordagem mais suja no mundo do software. Isso cria muitos problemas quando uma única propriedade é alterada no lado do servidor, então é difícil rastrear as áreas em quantas interfaces alterar o nome da propriedade.

    • Defina a propriedade anulável. Se estou definindo a propriedade anulável, por que devo seguir a abordagem 'B'? Mais uma vez, não tenho uma resposta :( para dar à minha equipe de desenvolvimento.

    • Não crie outra interface, use o tipo de utilitário 'Parcial' e torne todas as propriedades opcionais. Ao fazer isso, estamos perdendo o benefício real da interface. Isso também é contra as práticas. Se eu tiver que seguir isso, então Por que eu tenho que seguir a abordagem 'A', novamente Sem resposta :).

    • Se estou tornando algumas / algumas propriedades anuláveis, que tal a legibilidade do código e como posso julgar quantas propriedades são necessárias para passar o valor para o servidor. Então eu tenho que verificar o respectivo componente e dar uma olhada. Principal preocupação com a legibilidade do código.

Agora, apenas para pensar sobre os casos acima em uma perspectiva mais ampla e comparar com Tipos TypeScript com Interface em Formulários Reativos para Tipos Fortes. Acredito que toda boa abordagem economizará tempo de desenvolvimento e, nesta abordagem, lamento dizer que não vejo nenhum benefício de acordo com os Princípios e Práticas de Design de Software.

  1. Validação de tempo de execução com a ajuda de decoradores - é bom.

Concordo com seu comentário sobre " É bom ", a abordagem do decorador que não podemos alcançar na abordagem de interface. Eu acredito que este é o recurso mais poderoso do TypeScript, então porque não podemos usar o mesmo no Reactive Form Approach e dar à equipe de desenvolvimento o controle total de suas propriedades de objeto.

  1. Por que você precisa de setValue () se houver setters?

Onde eu disse que preciso de 'setValue ()'? Não preciso de setValue e não mostrei no exemplo em que estou chamando o método setValue no Class Driven Approach. Por favor corrija-me se eu estiver errado.

  1. Escrever tipos TypeScript é como escrever testes estáticos. Alguém pode criar aplicativos sem testes porque acham que é desnecessário, mas é uma prática ruim.

Não estou dizendo que os tipos TypeScript são como escrever testes estáticos. Mas não concordo com as alterações de commit nas classes base de forma reativa, acredito que podemos conseguir a mesma coisa sem tocar na definição da classe. Aqui, podemos usar o poder real da interface que não estamos usando de acordo com os commits até agora, é uma boa prática que a lógica esteja em execução por tanto tempo e estamos adicionando os tipos genéricos definindo o valor padrão de ' qualquer'?
Acho que podemos alcançar a mesma coisa sem tocar nas classes básicas da Forma reativa. Não sei por que não estamos aproveitando a interface nisso, em vez de alterar a definição da classe base e também alterar a especificação.

  1. Validação de tempo de execução com a ajuda de decoradores - pode ser uma boa ideia, concordo.

É bom saber que nós dois somos a mesma página neste :).

  1. Além de setValue (), há também patchValue (), reset (), que também funcionam com o valor do formulário. Substituir apenas setValue () por um setter tornará o código inconsistente. Além disso, quando tivermos que escrever configuradores para cada propriedade do modelo de formulário, isso adicionará muito mais sobrecarga de código, bem como sobrecarga de desempenho. Misturar nomes de propriedades de modelo de formulário com propriedades de controle de formulário também é uma má ideia, na minha opinião.

Deixe-me descrever o ponto acima em três seções, chamando o método, sobrecarga de desempenho do setter e combinando propriedades do modelo de formulário.

Método de Chamada: Como esperado, enquanto escrevia este post pensei que alguém poderia sugerir que eu usasse o método 'patchValue' ou 'reset'. Novamente, eu gostaria de dizer que, no caso do mundo real, principalmente a equipe de desenvolvimento está usando o método 'setValue' em vez de patchValue ou outros métodos (esta é minha experiência de acordo com a revisão de código de aplicativo angular e posts Stackoverflow setValue vs patchValue). Meu foco é apenas chamar o método para atribuir o valor, não importa qual método estejamos chamando.

Desempenho do setter : Concordo com a declaração dos setters cria uma sobrecarga de desempenho. Se for esse o caso, eu diria que temos que corrigir primeiro no Angular Project porque para vincular a forma reativa, o Angular Framework está usando o método setter na classe Control Value Accessor e tantas outras diretivas e isso cria uma sobrecarga de desempenho sem usar o Abordagem de classe. Mais uma coisa, a mesma abordagem que também estamos usando em vários componentes com o decorador @Input , temos que encontrar o alternativo ou a equipe Angular para fornecer uma solução diferente (acredito) para superar esse tipo de problema de desempenho. Então, eu não acho que isso seja uma grande preocupação. Agora chegando à questão de desempenho, por favor compare com a abordagem existente e abordagem do método setter (isso é opcional, a equipe de desenvolvimento pode optar se quiser como o mesmo que ChangeDetectionStrategy em Angular. Consulte o exemplo no site de documentação rxweb para optar neste caso. Julgue que quantas funções estão chamando quando assinamos as mudanças de valor, depois de definir o valor ou chamar diretamente o método setter. Acredito que isso seja muito mais intuitivo em termos de menos execução de código em comparação com alterações de valor, tamanho reduzido de compilação pacote, legibilidade do código e tantas outras coisas boas.

Misturando as propriedades : então, qual é a sua opinião, você está atribuindo um nome de propriedade FormControl diferente do nome de propriedade retornado pelo servidor. Se sim, então eu diria que este é um grande problema no código porque toda vez que eu tenho que mudar o nome da propriedade antes de postar no servidor, desculpe, mas eu não preferiria em todo o aplicativo. Se eu considerar sua boa opinião para o meu formulário de inscrição, que contém em média mais de 40 campos, então tenho que definir cada valor de propriedade manualmente, apenas para pensar sobre o código no componente apenas para a sacudida de atribuir o valor e o tamanho de compilação do produto. Esta é uma opinião melhor do que a abordagem da classe?
Agora vamos à solução proposta, não estamos misturando duas coisas em uma. As propriedades do FormControl são diferentes e as propriedades da classe são diferentes do respectivo tipo de dados. Se você deseja alterar o nome da propriedade como o nome da propriedade FormControl é diferente da propriedade Data, então você pode, consulte a documentação do pacote de formulário reativo rxweb. Portanto, não há problema, pois sua sensação de mal (misturar o nome da propriedade com nomes de controle de formulário) tem uma solução na abordagem proposta.

Espero ter respondido a todas as suas dúvidas, sinta-se à vontade para compartilhar se você tiver alguma outra dúvida sobre isso :).

Como eu disse em meu comentário anterior, não há necessidade de alterar as classes básicas da Forma reativa porque podemos alcançar as mesmas coisas usando o poder das Práticas do Princípio de Segregação de Interface. Aqui está a solução de forma reativa fortemente tipada de ponta a ponta com o pacote de @ rxweb / types . Isso funciona bem com a abordagem de Interface e também de Classe :).

Qual a aparência do código após a implementação?

Stackblitz: aberto
Github : Exemplo de forma reativa fortemente tipada orientada por interface

Alguém tem alguma sugestão sinta-se à vontade para compartilhar a mesma.

Portanto, a versão 10 do Angular já está disponível , este é um grande lançamento e as formas aparentemente reativas não serão fortemente tipadas até pelo menos a versão 11 do Angular. Portanto, teremos que esperar pelo menos até o outono para implementar esse recurso.

Tenho uma pergunta (ou uma proposta?) Sobre a forma como o modelo de formulário é construído na maioria das sugestões / PRs que vi aqui.

Ao examinar a maioria das bibliotecas e PRs que tentam tornar o tipo de Formulários Reativos seguro, você pode ver que eles criam um modelo semelhante a este:

interface Address {
  name: Name;  
}
interface Name {
  firstName: string;
  lastName: string;
}

isso é então "traduzido" para algo assim:

const myForm = new FormGroup<Address>({
  name: new FormGroup<Name>({
    firstName: new FormControl('John'),
    lastName: new FormControl('Doe'),
  })
})

Então, em palavras simplificadas: "Se for um objeto, então construa um FormGroup para ele. Se for um array, então construa um FormArray. E se for um valor primitivo, então crie um FormControl."

Mas, há um problema: você não pode mais usar objetos em FormControls.

As soluções que vi até agora: Algumas bibliotecas simplesmente não oferecem suporte para isso. E algumas bibliotecas usam algum tipo de "hack" para criar uma dica de que você realmente deseja usar um FormControl em vez de um FormGroup.

Minha pergunta / proposta: O que falaria contra a definição explícita do modelo de formulário da seguinte maneira?

interface Address {
  name: FormGroup<Name>;  
}
interface Name {
  firstName: FormControl<string>;
  lastName: FormControl<string>;
}

const myForm = new FormGroup<Address>({
  name: new FormGroup<Name>({
    firstName: new FormControl('John'),
    lastName: new FormControl('Doe'),
  })
})

Isso tem a grande vantagem de agora poder colocar objetos em FormControls. E não requer nenhum tipo de "hack" para fazer isso :)

Criei um Codesandbox para isso, para que você possa experimentar por si mesmo: https://codesandbox.io/s/falling-grass-k4u50?file=/src/app/app.component.ts

@MBuchalik , sim, esta é a primeira decisão óbvia que vem à mente quando você começa a trabalhar em "formulários de digitação forte". Eu também comecei com isso, mas tem uma desvantagem significativa - a necessidade de criar dois modelos: um para controles de formulário, o outro - para valores de formulário.

Por outro lado, até onde eu entendo, essa solução nos permitirá implementar "formulários com tipos fortes" sem interromper o chage, e não teremos que esperar pelo lançamento da próxima versão principal do Angular. Aqui é necessário trabalhar na prática com tal solução para avaliar se ela possui deficiências mais críticas do que a necessidade de criar dois modelos.

@MBuchalik Eu compartilhei as mesmas opiniões com você e levantei a mesma pergunta para o PR e um dos contribuidores angulares ( @KostyaTretyak ) respondeu.

Você pode dar uma olhada na discussão no PR:
https://github.com/angular/angular/pull/37389#discussion_r438543624

TLDR;

Aqui estão alguns problemas:
Se seguirmos sua abordagem, precisaremos criar dois modelos diferentes - para controles de formulário e para valores de formulário.
O modelo para controles de formulário é mais difícil de ler.

E eu implementei essa ideia para nossos projetos meio ano atrás que já estão sendo usados ​​na produção. Uma experiência FULL estática de autocompletar para o tipo de controle em Angular HTML realmente aumenta nossa produtividade de desenvolvedor júnior. (com fullTemplateTypeCheck habilitado)

Eu compartilhei "por que eu vou assim" no comentário anterior neste tópico:
https://github.com/angular/angular/issues/13721#issuecomment -643214540

Codesanbox Demo: https://codesandbox.io/s/github/gaplo917/angular-typed-form-codesandbox/tree/master/?fontsize=14&hidenavigation=1&theme=dark

Obrigado por seus insights @KostyaTretyak e @ gaplo917! 👍

Se entendi bem, podemos resumir da seguinte forma.

Se quisermos usar apenas um único modelo, uma solução como a fornecida por @KostyaTretyak pode ser usada. A desvantagem, entretanto, é que agora não podemos mais usar objetos em FormControls. (Eu sei que há um "hack" que permitiria isso. Mas nosso modelo novamente não está "limpo"; portanto, precisaríamos mais uma vez de 2 modelos.)

Se quisermos ser capazes de usar objetos em FormControls, então provavelmente (!) Não há como evitar o uso de uma abordagem como a que eu (ou @ gaplo917) ilustrei. A desvantagem é que você precisa basicamente de 2 modelos. Ou pelo menos use alguns tipos de auxiliares para "extrair" o modelo de valor do formulário.

Portanto, agora só precisamos pensar se os objetos em FormControls devem ser possíveis ou não. Isso simplesmente responderia à pergunta sobre qual das duas abordagens deve ser selecionada. Ou eu estou esquecendo de alguma coisa?

Obrigado por seus insights @KostyaTretyak e @ gaplo917! 👍

Se entendi bem, podemos resumir da seguinte forma.

Se quisermos usar apenas um único modelo, uma solução como a fornecida por @KostyaTretyak pode ser usada. A desvantagem, entretanto, é que agora não podemos mais usar objetos em FormControls. (Eu sei que há um "hack" que permitiria isso. Mas nosso modelo novamente não está "limpo"; portanto, precisaríamos mais uma vez de 2 modelos.)

Se quisermos ser capazes de usar objetos em FormControls, então provavelmente (!) Não há como evitar o uso de uma abordagem como a que eu (ou @ gaplo917) ilustrei. A desvantagem é que você precisa basicamente de 2 modelos. Ou pelo menos use alguns tipos de auxiliares para "extrair" o modelo de valor do formulário.

Portanto, agora só precisamos pensar se os objetos em FormControls devem ser possíveis ou não. Isso simplesmente responderia à pergunta sobre qual das duas abordagens deve ser selecionada. Ou eu estou esquecendo de alguma coisa?

@MBuchalik Na minha opinião, se você confia no compilador Typescript e confia fortemente no recurso de "inferência de tipo", você não precisa ter 2 modelos. Nosso sistema interno tem mais de 60 formulários, alguns deles são muito complexos aninhados com nível de profundidade 3 FormArray-FormGroup-FormArray e também não precisamos de um modelo explícito para o tipo de valor.

Existem apenas 2 tipos de modelos de dados para jogar, que são:

  • Modelo de solicitação / resposta de dados da API
  • Modelo FormControl

99,9% do tempo, nós

  1. Crie um encapsulamento para cada formulário complexo
  2. Transforme os dados remotos -> dados do formulário
  3. Transforme os dados do formulário -> carga útil remota

o seguinte snippet de código é a ilustração:

interface FooApiData {
   id: string
   age: number
   dob: string | null
   createdAt: string
}

interface FooFormControlType {
  id: TypedFormControl<string>
  age: TypedFormControl<number>

  // calendar view required JS date form control binding
  dob: TypedFormControl<Date | null>
} 

interface FooApiUpdateRequest {
   id: string
   dob: string | null
   age: number
}

class FooForm extends TypedFormGroup<FooFormControlType> {
    constructor(private fb: TypedFormBuilder, private initialValue: FooApiData) {
      super({
          id: fb.control(initialValue.id, Validators.required),
          dob: fb.control(initialValue.dob === null ? new Date(initialValue.dob) : null),
          age: fb.number(initialValue.age, Validators.required)
       })
   }
   toRequestBody(): FooApiUpdateRequest {
       const typedValue = this.value
       return {
          id: typedValue.id,
          dob: typedValue.dob !== null ? moment(typedValue.dob).format('YYYYMMDD') : null,
          age: typedValue.age
       }
   }
}

const apiData = apiService.getFoo()

const form = new FooForm(new TypedFormBuilder(), apiData)

// assume some UI changes the form value
function submit() {
   if(form.dirty && form.valid){
     const payload = form.toRequestBody()
     apiService.updateFoo(payload)
   }
}

PS Esta é uma arquitetura de fluxo de dados opinativa que podemos desfrutar de programação de tipo com segurança em Typescript

Se quisermos usar apenas um único modelo, uma solução como a fornecida por @KostyaTretyak pode ser usada. A desvantagem, entretanto, é que agora não podemos mais usar objetos em FormControls. (Eu sei que há um "hack" que permitiria isso. Mas nosso modelo novamente não está "limpo"; portanto, precisaríamos mais uma vez de 2 modelos.)

Aqui ainda é necessário estimar com que freqüência precisamos usar um objeto para FormControl . Acho que pode ser estimado em algo em 5-30%. Ou seja, se usarmos soluções com um modelo, podemos cobrir 70-95% dos casos de uso de FormControl . Para o resto - apenas forneça uma dica para TypeScript como um tipo adicional (consulte Control<T> , não é correto chamá-lo de "segundo modelo"):

interface FormModel {
  date: Control<Date>;
}

O tipo Control<T> ser chamado de hack? - Sim, provavelmente é um hack, mas não um hack bruto. Não conheço nenhum caso em que esse tipo não funcione conforme o esperado ou tenha efeitos colaterais.

Oh, eu me lembrei dos efeitos colaterais com Control<T> quando precisamos usar uma biblioteca externa para o modelo de valor do formulário. Nesses casos, dois modelos são realmente necessários:

import { FormBuilder, Control } from '@ng-stack/forms';

// External Form Model
interface ExternalPerson {
  id: number;
  name: string;
  birthDate: Date;
}

const formConfig: ExternalPerson = {
  id: 123,
  name: 'John Smith',
  birthDate: new Date(1977, 6, 30),
};

interface Person extends ExternalPerson {
  birthDate: Control<Date>;
}


const fb = new FormBuilder();
const form = fb.group<Person>(formConfig); // `Control<Date>` type is compatible with `Date` type.

const birthDate: Date = form.value.birthDate; // `Control<Date>` type is compatible with `Date` type.

Mas, neste código, a sobrecarga está apenas aqui:

interface Person extends ExternalPerson {
  birthDate: Control<Date>;
}

Graças a @ArielGueta , um problema crítico com o tipo Control<T> agora se tornou conhecido. Ou seja, nem tentarei implementar Control<T> em solicitações pull futuras para Angular, como planejado antes.

Graças a @ArielGueta , um problema crítico com o tipo Control<T> agora se tornou conhecido. Ou seja, nem tentarei implementar Control<T> em solicitações pull futuras para Angular, como planejado antes.

@KostyaTretyak Não é verdade. O problema crítico mostra apenas que a implementação de "ControlType" está incorreta.

Uma implementação completa de "Tipo de controle" não tem problema.

Demonstração ao vivo: https://codesandbox.io/s/lucid-bassi-ceo6t?file=/src/app/demo/forms/type -test.ts

Screenshot 2020-07-01 at 00 35 11

Ou seja, nem vou tentar implementar o Controlem solicitações pull futuras para Angular, como planejado antes.

OK, isso significa que seu PR (e os consecutivos) provavelmente nunca suportarão objetos em FormControls?

@MBuchalik , no momento (Angular v10), se tivermos o seguinte modelo de formulário:

interface FormModel {
  date: Date;
}

e se em nosso componente quisermos acessar o valor da propriedade date , precisamos fazer o seguinte:

get date() {
  return this.formGroup.get('date') as FormControl;
}
// ...
this.date.value as Date;

Minha solicitação pull atual fornece um genérico para o valor do formulário, mas não fornece um tipo para o controle do formulário:

get date() {
  return this.formGroup.get('date') as FormControl<Date>;
}
// ...
this.date.value; // Here Date type

@ gaplo917 , @MBuchalik , tentei suas soluções e tentei implementar minha própria solução semelhante, mas nem todas funcionam perfeitamente. Esta solução também fornece um conjunto de tipos para extrair recursivamente os valores do modelo de formulário. As alterações gerais e de interrupção são muito significativas, consulte o rascunho de RP .

Duvido muito que, no momento, essas soluções devam ser propostas para serem implementadas em Angular. Ou seja, por enquanto teremos que usar genéricos apenas para valores de formulário, não para tipos de controle de formulário.

mas todos eles não funcionam perfeitamente

Eu gastei apenas algumas horas na minha ilustração, então não esperava que fosse perfeita;) Você poderia dar exemplos de coisas que não funcionam bem? (Especialmente em coisas que, do seu ponto de vista, não podem ser corrigidas facilmente?)

A propósito, uma sugestão com relação à compatibilidade com versões anteriores: Do meu ponto de vista, é relativamente difícil tornar a implementação totalmente compatível com versões anteriores. Por causa disso, talvez pudéssemos fazer o seguinte: Não alteramos as classes FormControl, FormGroup e FormArray. Em vez disso, criamos novos que herdam deles (talvez chamá-los de StrictFormControl<T> e StrictFormGroup<T> ou como você quiser). Esses são os que tornamos seguros para o tipo. O benefício: temos 100% de certeza de que nenhuma alteração significativa foi feita. :)

algumas horas na minha ilustração, então eu não esperava que fosse perfeito;)

Trabalhei com essa solução por alguns dias e vejo como será difícil trabalhar com formulários.

  1. Em primeiro lugar, despesas gerais significativas e a necessidade de dois modelos.
  2. Em segundo lugar, esta solução não é melhor em termos de confiabilidade do que a minha solução com o tipo Control<T> , porque da mesma forma é necessário extrair recursivamente o valor do modelo de formulário.
  3. Trabalhe com controles de formulário aninhados. Se tivermos o seguinte modelo de formulário:
interface FormModel {
  one: FormGroup<{two: FormControl<string>}>;
}

E se obtivermos formGroup.controls.one.value , o TypeScript fornece uma dica com tipo condicional, não com {two: string} type (como deveria ser). Portanto, valorize o difícil de ler no IDE.

E se obtivermos formGroup.controls.one.value, o TypeScript fornecerá uma dica com o tipo condicional, não com o tipo {two: string} (como deveria ser). Portanto, valorize o difícil de ler no IDE.

OK, então apenas para ter certeza de que entendi tudo corretamente. Se você usar minha implementação e escrever o seguinte:

interface FormModel {
  one: FormGroup<Two>;
}
interface Two {
  two: FormControl<string>;
}

const myForm = new FormGroup<FormModel>({
  one: new FormGroup<Two>({
    two: new FormControl('')
  })
});

(tornou um pouco mais prolixo;))

Se eu agora procurar myForm.controls.one.value , ele se parecerá com isto:

grafik

Então você diz que, neste exemplo, "dois" não deveria ser opcional? Acho que essa não é a maneira certa de digitar o valor do formulário. O valor do formulário inclui apenas campos não desabilitados. Então, do meu ponto de vista, deve ser um Partial recursivo. Você não pode saber em tempo de compilação quais campos serão desabilitados e quais não.

Então você diz que, neste exemplo, "dois" não deveria ser opcional?

Que? Não.

Meu teste de sua solução:

interface FormModel {
  one: FormGroup<{two: FormControl<string>}>;
}

let formGroup: FormGroup<FormModel>;
const some = formGroup.controls.one.value;

O que vejo no codesandbox após passar o mouse sobre value :

(property) FormGroup<FormGroupControls<{ two: FormControl<string>; }>>.value: PartialFormGroupValue<FormGroupControls<{
    two: FormControl<string>;
}>>

Aqui PartialFormGroupValue se refere ao tipo condicional PartialFormValue .

Ah, ok, acho que entendi. Então você quer dizer que o tipo é difícil de ler, certo? Originalmente, pensei que você estava falando sobre um bug ou algo parecido.

Bem, a maioria dos IDEs ainda apresentará apenas as sugestões para as propriedades disponíveis, uma vez que você continue digitando. Então, eu realmente não vejo grandes problemas aqui. (Claro, seria melhor ler se dissesse apenas {two?: string} . Mas não acho isso muito importante. Essa é, pelo menos, minha opinião.)

Se você implementou seu Control<T> , como você o removeria da digitação do valor do formulário sem fazer o sth como eu fiz? E como você tornaria o valor do formulário uma parcial recursiva sem usar um tipo auxiliar?

Se você implementou seu controle, como você o removeria da digitação do valor do formulário sem fazer sth como eu fiz? E como você tornaria o valor do formulário uma parcial recursiva sem usar um tipo auxiliar?

Nesse caso, minha solução não é melhor:

(property) FormGroup<FormGroup<{ two: FormControl<string>; }>>.value: ExtractGroupValue<FormGroup<{
    two: FormControl<string>;
}>>

Dei este exemplo porque você pediu:

Você poderia dar exemplos de coisas que não funcionam bem? (Especialmente em coisas que, do seu ponto de vista, não podem ser corrigidas facilmente?)

A propósito, resolvi um problema crítico com Control<T> .

Para resolver os problemas de vinculação de HTML com Angular 10 e [formControl] , este é o caminho que eu segui:

Conforme observado em outro problema (https://github.com/angular/angular/issues/36405#issuecomment-655110082), para meus formulários geralmente crio classes que estendem FormGroup para facilitar a reutilização e os testes . Com essa estrutura, consegui resolver o problema por enquanto atualizando meu código de algo assim:

class UserFormGroup extends FormGroup {
  constructor() {
    super({
      id: new FormControl(null, Validators.required),
      name: new FormControl(null, Validators.required),
    });
}

Para isso:

// everything will extend these two
export class EnhancedFormGroup<T extends { [key: string]: AbstractControl }> extends FormGroup
{
  controls!: T;
}
export class EnhancedFormArray<T extends AbstractControl> extends FormArray 
{
  controls!: T[];
}

// reworked form from above
function formDefinition() {
   return {
      id: new FormControl(null, Validators.required),
      name: new FormControl(null, Validators.required),
    };
}

class UserFormGroup extends EnhancedFormGroup<ReturnType<typeof formDefinition>> {
  constructor() {
    super(formDefinition());
}

Nesse ponto, form.controls mostrará corretamente seu tipo como { id: FormControl, name: FormControl } , vinculando corretamente no HTML, e agregaria corretamente se o formulário fosse mais complicado com grupos de formulários ou matrizes aninhados.

Usar a função formDefinition não é bonito, mas foi a solução mais limpa que eu poderia sugerir para evitar a duplicação entre a definição do formulário e o construtor.

Eu acredito que você poderia atualizar FormGroup para ter a definição de tipo genérico acima sem introduzir mudanças significativas (bem, isso pode não ser verdade para formulários que dinamicamente adicionam / removem controles, eu acho; eles não apareceriam no controls tipo)

editar
Parece ainda mais simples se você não precisa criar classes que estendem FormGroup; você pode criar uma função auxiliar que resolva o problema genérico:

function createEnhancedFormGroup<T extends { [key: string]: AbstractControl }>(controls: T) {
  return new EnhancedFormGroup<T>(controls);
}

const form = createEnhancedFormGroup({
  id: new FormControl(null, Validators.required),
  name: new FormControl(null, Validators.required),
});

editar 2
... ou você poderia assá-lo na própria classe FormGroup ( FormBuilder talvez?)

export class EnhancedFormGroup<T extends { [key: string]: AbstractControl }> extends FormGroup
{
  controls!: T;

  static create<T extends { [key: string]: AbstractControl }>(controls: T) {
    return new EnhancedFormGroup<T>(controls);
  }
}

const form = EnhancedFormGroup.create({
  id: new FormControl(null, Validators.required),
  name: new FormControl(null, Validators.required),
});

editar 3
Estendi os exemplos acima para incluir a digitação em value s e criei um artigo para resumir tudo:

https://medium.com/youngers-consulting/angular-typed-reactive-forms-22842eb8a181

Isso agora está marcado no roteiro para o desenvolvimento futuro: https://angular.io/guide/roadmap#better -developer-ergonomics-with-strict-typing-for-angularforms

@pauldraper Você poderia explicar o que mudou em comparação com o roteiro de cerca de 2 meses atrás? A única mudança que vejo é a redação do título. Mas ainda está na seção "Futuro". Exatamente como há 2 meses.

@MBuchalik talvez esteja lá por 2 meses.

Esta página foi útil?
0 / 5 - 0 avaliações