Angular: Las formas reactivas no están fuertemente tipadas

Creado en 30 dic. 2016  ·  90Comentarios  ·  Fuente: angular/angular

[x] feature request
  • Versión angular: 2

Las formas reactivas están diseñadas para usarse en formas complejas, pero los valueChanges del control son Observable<any> , que están totalmente en contra de las buenas prácticas para el código complejo.

Debería haber una forma de crear controles de formulario fuertemente tipados.

forms feature high

Comentario más útil

Oye, quería compartir una actualización del equipo de Angular: te escuchamos que este es un gran problema. Pronto comenzaremos a trabajar en formularios con más mecanografía, que incluirán la revisión de los RP existentes y la revisión de todos sus comentarios nuevamente. ¡Gracias a todos por tomarse el tiempo para dejar sus pensamientos!

Todos 90 comentarios

relacionado # 11279

Esto no está relacionado con # 11279.

Por favor, explique cómo no está relacionado.
Lo que quieres es que Abstract Control sea genérico, ¿verdad? Esa es la única forma en que valueChanges puede tener un tipo que no sea Observable<any> no habría otra forma de inferir el tipo.
Que es exactamente lo que pregunta # 5404, lo que significa que esto está relacionado con # 11279
Si hay otra forma en que esto podría implementarse sin hacer que AbstractControl sea genérico, explíquelo.

Usar get<Type> como en # 11279 es definitivamente una solución incorrecta. Si TypeScript tuviera algo como Java Unbounded Wildcard, get lo usaría, y no any . ¿Quizás se pueda hacer algo de la misma manera con una interfaz vacía?

También está https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html keyof . Las características de TypeScript 2.1 pueden ser muy interesantes de estudiar para implementar controles de formulario fuertemente tipados.

La forma en que está actualmente, desafortunadamente, no creo que sea utilizable para aplicaciones grandes y necesito diseñar algo sobre ella.

Acabo de notar que en TS 2.2 (https://github.com/Microsoft/TypeScript/wiki/Roadmap#22-february-2017) que han planeado tipos genéricos predeterminados (https://github.com/Microsoft/TypeScript/ issues / 2175) una vez que tengamos esto, creo que podría ser una buena idea revisar este problema, ya que podríamos hacer AbstractControl genérico como AbstractControl<T = any> donde T es el tipo de el valor devuelto por valueChanges que sería un Observable<T> . No sería una buena idea hacerlo actualmente, ya que sería un cambio radical masivo, pero con los genéricos predeterminados, a menos que los malinterprete, no será un cambio radical.

Pequeña actualización sobre esto, parece que los genéricos predeterminados se han movido a TS2.3 . Entonces, con el soporte de TS 2.1 de Angular con la versión 4.0, no debería pasar mucho tiempo antes de que puedan admitir TS 2.3, que es ahora cuando debemos esperar para revisar esto.

TypeScript 2.3 con tipos genéricos predeterminados ya está aquí, ¿tenemos algún plan cuando esté listo el soporte para TS 2.3 en angular?

@desfero esperando # 16707 para que la compilación se actualice a TS2.3

A +1 le encantaría ver esta función. ¿Alguien está trabajando en eso?

Pequeña actualización sobre esto:
Según mi comentario aquí: https://github.com/angular/angular/pull/16828#issuecomment -337034655
La implementación de genéricos en la API de formularios actual no es posible sin cambios importantes.
Entonces, se necesitan cambios importantes
O una reescritura de formas completas

Entonces resulta que mi comentario anterior era incorrecto.
Pude implementar esto en la API de formularios actual, como puede ver aquí # 20040

@Toxicable que todavía tiene el problema de no tener la capacidad de refactorizar de forma segura. get ('persona') por ejemplo, no está usando realmente el símbolo en sí. El ejemplo anterior, de @rpbeukes , tiene una forma

@howiempt

get ('persona') por ejemplo, no está usando realmente el símbolo en sí

No tengo idea de lo que quieres decir con esto, ¿a qué símbolo te refieres aquí?
En mi implementación puedes hacer 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) sin usar la cadena

Esto carece de la capacidad de atravesar múltiples FormGroups.
Si bien mi método no puede inferir los tipos en este escenario, la idea de mi PR es agregar tipos genéricos sin romper cambios o introducir nuevas API (aparte de los genéricos)

@Toxicable Entiendo que tu cambio tiene como objetivo no romper las cosas, no tratar de criticar tu solución. La otra implementación (modernizada) permite utilizar una propiedad real en lugar de una cadena. Al hacer referencia al campo por cadena, si el nombre de la propiedad cambia, las compilaciones se rompen, lo que para mí no es muy seguro. Por ejemplo, cambiar el nombre del campo de 'nombre' a 'nombre', se rompería si no cambiara todas las referencias de g.get ('nombre'). Si pudiera hacer 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>

Todos serían referencias estrechas. La solución de modernización lo hace de una manera un poco intrincada, pero también resuelve ese problema.

@Toxicable gracias por el PR. Espero usarlo :)

Estoy de acuerdo con @howiempt , si podemos conseguir algo como esto sería el primer premio:

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

Una vez más, no sé realmente qué tan factible es esto dentro del alcance más amplio de las cosas.
Confío en tu juicio.

Sigan con el buen trabajo y gracias por la rápida respuesta.

Creo que este método de acceder a otros controles no está relacionado con agregar genéricos.
Sin embargo, siéntase libre de abrir otro problema al respecto.

Realmente no creo que tener el tipo de retorno establecido sea realmente "fuertemente tipado", parece que se requiere la mitad de la implementación, pero es un paso en la dirección correcta.

Hola, he publicado https://github.com/Quramy/ngx-typed-forms para solucionar este problema. Por favor, compruébalo 😄

@Quramy Traté de usar su paquete hace varias semanas y, según recuerdo, realmente no hace mucha aplicación :(

+1. No puedo contar la cantidad de instancias en las que deseaba que se implementara.

Mismo.
Las formas reactivas angulares es una de las características que realmente supera a cualquier otro marco. Hacer formas reactivas fuertemente tipadas lo llevaría al siguiente nivel, ampliando aún más la brecha con la competencia :)

esto se puede hacer con tipos mapeados condicionales y recursividad ..... los tipos mapeados condicionales simplemente se fusionaron en mecanografiado. Si esto se publica, tendremos la oportunidad de hacer posibles formas robustas

No puedo esperar

La solución FormBuilder . También los genéricos FormGroup<T> , FormControll<T> y FormArray<T> no se pueden usar directamente, porque son solo interfaces que no extienden el evento AbtractControl<T> . Esto no fue suficiente para nuestro proyecto actual.

Con ngx-fuertemente-tipadas-formas , ahora mismo he lanzado un proyecto fuerte de formas tipadas
Se rompe un poco con la compatibilidad con versiones anteriores, porque no usa genéricos predeterminados. Por lo tanto, lo obliga a darle explícitamente a sus controles un tipo cualquiera, pero agrega mucha más seguridad de tipos a todas las demás partes y es API compatible con la implementación actual de @ angular / forms.
Quizás esta sea una alternativa válida hasta que esta característica se implemente en Angular.

+1 Esta es una característica poderosa ...

Debe implementarse lo antes posible)

La forma en que está destinado a ser codificado

Hay una implementación interesante en este software de código abierto (AGPL)

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

Usé esto como una solución temporal:
Espero eso ayude

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);
    }
}

Esto es algo en lo que he pensado varias veces en proyectos en los que he trabajado, pero no he usado suficientes proxies de JavaScript para saber el impacto en el rendimiento que esto tendría en cualquier cosa que observe estos valores.

Simplemente creé una solución personalizada en el nivel de 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;
            }
        });
    }
}

Esta solución definitivamente no está muy pulida, y probablemente sería mejor que el FormGroup generado acceda a los valores de una propiedad (como myGroup.fields, donde "fields" sería el tipo proporcionado). Pero esto proporciona una escritura fuerte. Para usarlo:

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

Reuní experiencia en los últimos meses con formularios mecanografiados, utilizando mis formularios mecanografiados en mi proyecto actual. Proporciona un gran valor cuando se trabaja en un proyecto con tres equipos de desarrolladores y todos cambiaron porque era muy barato hacerlo.

Pero quiero discutir si es la decisión correcta solo poner genéricos en la API actual. Mientras creaba los tipos para todos los controles de formulario, encontré muchos casos extremos y cosas que son imposibles o difíciles de escribir, porque creo que la escritura estática no era posible en ese momento y, por lo tanto, no es una de las mayores preocupaciones.
Lamentablemente, esto apunta a la funcionalidad principal con AbstractControl#value , que debe ser algo como DeepPartial<T> , o AbstractControl#get con diferentes implementaciones para cada subclase.
Al ser compatible con versiones anteriores, también se pierde parte de la seguridad de los tipos causada por casos de caída con los tipos any .
¿Quizás considerar una nueva API para formularios reactivos también es una opción para este problema?

Entonces, esto es lo que terminé haciendo mientras ocurre una solución real.

Descargo de responsabilidad ... Acabo de comenzar en Angular, pero estoy bastante familiarizado con Typecript, así que no entiendo completamente las formas reactivas ... esto es lo que terminó funcionando para mí, pero por supuesto no está completamente completo, solo escribí FormGroup, pero Estoy seguro de que será necesario escribir más cosas a medida que aprenda más sobre los formularios ...

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

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

y luego puedo usarlo así

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 estaba buscando este problema exacto! ja ja

@cafesanu Aquí hay una pequeña mejora de su FormGroup escrito para verificar el constructor.

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; }'.
});

Escribí un pequeño envoltorio sobre FromControl que permite generar dinámicamente el tipo de datos basados ​​en llamadas a un constructor: https://github.com/concentricsky/badgr-ui/blob/develop/src/app/common/util/ formularios-mecanografiados.ts

El tipo construido dinámicamente garantiza que la forma del formulario coincida con sus expectativas, en lugar de tener que declarar previamente el tipo de interfaz y luego esperar que realice las llamadas correctas para crear el formulario.

Me encantaría ver algo similar a esto en Angular en algún momento.

El uso se ve así:

// 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;

Hola a todos, en los últimos 3 días estuve experimentando usando un d.ts gist para definir una definición más estricta para las clases de ReactiveForms creando una nueva interfaz con tipo compatible con la clase angular original.
Creo que puede ser una posible solución / solución alternativa para su 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;
}

Pruebas de uso en stackblitz : descargue el código y ejecútelo en VSCode local.No sé por qué en stackblitz los ERRORES para setValue y pathValue no son correctos ...

En este hilo de Twitter hablo con @IgorMinar de algunas "ideas futuras" (después de V8 +)

¡Cualquier comentario, sugerencia y ayuda son bienvenidos !

Mi solución se llama @ng-stack/forms . Una de las características interesantes es la detección automática de tipos de formularios.

Entonces, no es necesario hacer esto en sus componentes:

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

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

Ahora hacer esto:

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

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

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

Para obtener más información, consulte @ ng-stack / forms

Oye, hemos estado trabajando con @zakhenry recientemente en dos cosas:

  • Mejorando los tipos de formularios
  • Mejorar la forma de gestionar los subformularios (dentro de los subcomponentes)

No cubrimos todos los tipos como lo hizo

Si alguien quiere echar un vistazo a lib: ngx-sub-form

Hola @ maxime1992 He mirado el Controls<T> pero no es estricto el AbstractControl to <T[K]>
¿Puedo preguntarte porque? ¿Lo deja "sin escribir" porque siente la necesidad de cambiar el tipo en tiempo de ejecución usando algo como los métodos setControl o registerControl para redefinir y cambiar el FormControl y tal vez el tipo asociado?
Debido a que mi TypedForms.d.ts es un poco más estricto y "fuerza" el AbstractControlTyped to the type T<P> pero no sé si con este tipo de elección hacer cumplir / deshabilitar algo que en la API de ReactiveForms original está permitido y tal vez usado por alguien...

¿Cualquier pensamiento? ¿Algún caso real a considerar?
Cualquier comentario sobre esto puede ayudarme a decidir cómo cambiar las definiciones que he creado y el PR en el que estoy trabajando ...
Gracias

PD: Gran trabajo en ngx-sub-form 👍 la idea de usar ControlValueAccessor para manejar subformularios es algo que también estaba experimentando 😉

Para @ ng-stack / forms, se agregó compatibilidad con la validación escrita.

Clases FormControl , FormGroup , FormArray y todos los métodos de FormBuilder
aceptar "modelo de validación de errores" como segundo parámetro para un 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...

De forma predeterminada, se utiliza un tipo especial llamado 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 contiene una lista de propiedades extraídas de typeof Validators y tipos de devoluciones esperadas:

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 Muchas gracias pero no puedo usar
Validators.compose([Validators.required, Validators.maxLength(20), Validators.minLength(3)])

Hola @youssefsharief, ¿puedes darme más detalles de tu problema con los validadores? ¿Qué tipo de error / problema encontró al usar .d.ts?

Si puede publicar un código de muestra o un stackblitz, lo observaré e intentaré ayudar a resolver el problema si encuentro una solución 😉

Por @ng-stack/forms agregó soporte para input[type="file"] .

Ver también ejemplo en stackblitz

También utilicé el enfoque "clásico" con el uso de FormData para cargar archivos, pero hay varias ganancias:

  • recuperar automáticamente el nombre de entrada del formulario, por ejemplo, <input type="file" name="someName"> -> formControl.value con someName nombre de campo;
  • admite el atributo multiple con la configuración correcta del nombre del campo (nota userpic[] aquí)
<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 por lo que vi hasta ahora, su paquete parece ser una gran solución para los problemas de escritura actuales en Angular Reactive Forms. ¿Alguna vez pensó en contribuir a Angular en lugar de hacer su propia implementación?

Estamos casi en angular 8 y todavía no tenemos formas mecanografiadas listas para usar en un entorno 100% TypeScript, lo que me parece bastante extraño.

@kroeder , acabo de ver que el problema actual se creó hace más de dos años (casi inmediatamente después del lanzamiento de Angular 2), y veo que una solicitud de extracción similar se creó hace 1,5 años sin una fusión ...

Pero si @ng-stack/forms será popular y compatible con Angular 8+, tal vez cree la solicitud de extracción en el futuro.

@KostyaTretyak Suena genial :) He estado usando https://github.com/no0x9d/ngx-strongly-typed-forms que se menciona anteriormente en este hilo y creado por @ no0x9d ¿

@ZNS , no lo sé, pero después de una revisión rápida, creo que no hay un genérico para la validación en ngx-strongly-typed-forms , así como detectar automáticamente los tipos apropiados para los controles de formulario . Puede ser que esté equivocado.

@ZNS @KostyaTretyak Hola. Como se mencionó anteriormente, soy el autor de ngx-strong-typed-forms .
Hice una revisión rápida del conjunto de funciones de @ng-stack/forms y creo que hay algunas pequeñas diferencias.

Existe una diferencia conceptual entre casi todas las soluciones o métodos alternativos y mi proyecto.
En la mayoría de los casos, el Angular FormControl original es extendido o envuelto por algún Proxy. Algunos métodos se anulan con otras firmas de tipo y se delegan a la función original.
Esto introduce una nueva capa con un impacto de rendimiento insignificante, pero más importante es el código que debe mantenerse y puede introducir errores en su proyecto.
En mi opinión, esto es innecesario para las comprobaciones estáticas en tiempo de compilación. La única parte del tiempo de ejecución es el NgModule que proporciona mi FormBuilder escrito, que de hecho es el FormBuilder angular original. Todo lo demás es solo código angular.

En comparación directa, no tengo el ValidationModel y la conversión de objetos a FormGroups y Arrays a FormArrays, pero hice algunos cambios obstinados en AbstractControl#get para que sea más seguro para los tipos y he escrito argumentos de validación.

Con algunas pequeñas adiciones, mi código puede ser compatible con versiones anteriores y podría crear una solicitud de extracción. Pero una solicitud de extracción similar está obsoleta durante mucho tiempo.
Pero si hay esfuerzos para conseguir esto en Angular, estaría feliz de unir fuerzas. Por otro lado, me encantaría ver una nueva API para formularios que esté mejor diseñada para ser mecanografiada estrictamente. Vea mi comentario para más detalles.

A +1 le encantaría ver esta función. ¿Alguien está trabajando en eso?

golpe al equipo angular?

No se preocupan por los desarrolladores. Tienen su propia cartera de pedidos, con blackjack e increíbles funciones de tamaño de paquete de reducción del 5%.

@ Lonli-Lokli

No se preocupan por los desarrolladores.

Criticar es fácil. ¿Te preocupas más por otros desarrolladores?
No he visto un PR tuyo para mejorar las formas ni ningún tipo de comentario constructivo o RFC para hacer que las cosas avancen: man_shrugging:

Tienen su propia cartera de pedidos

De ninguna manera: temeroso :!
¿La gente está dando prioridad a las cosas que la empresa _ [quién les paga] _ necesita?
¡Qué lástima!
image

increíbles-5% -disminución-de-tamaño de paquete de funciones.

Claramente estás hablando de Ivy y la (actualmente) pequeña diferencia en el tamaño del paquete.
Ivy es actualmente experimental y debes participar. ¡Qué sorpresa, las cosas aún no son perfectas! :pensando:
Sí, se ha dicho que Ivy ayudará a reducir el tamaño del paquete y permitirá que las herramientas agiten mejor los árboles en las aplicaciones. ¡Y con suerte, eso vendrá! Por ahora, solo están trabajando en él para asegurarse de que no está rompiendo nada y luego pueden ayudar a tener una mejor información de depuración, compilación incremental en base a componentes en lugar de módulos y agitación de árboles. Pero las herramientas para hacer temblar ese árbol funcionarán más tarde.

Por lo tanto, trate de ser respetuoso y evitar criticar a las personas que le ofrecen un marco de código abierto de forma gratuita. Las cosas no son perfectas, hay un gran trabajo en marcha, sí, parece que algunos problemas se han dejado atrás, pero esa refactorización era necesaria y nunca habrá un buen momento para hacer algo tan grande como eso, simplemente tenía que suceder en algún momento.

Ahora estoy fuera de este debate porque no quiero monopolizar este hilo hablando de cosas improductivas. *vuela lejos*

@ maxime-allex
Hay muchos otros RP (386 a partir de ahora), ¿crees que uno más cambiará algo?
Hablando de este problema, este PR relacionado (https://github.com/angular/angular/pull/20040) aún no se fusionó.

Hablando de refactorización, se mencionó a Ivy hace un año. Sé que alguien puede tratar es una característica importante para los desarrolladores, pero personalmente prefiero ver que las correcciones son importantes para tantos desarrolladores como sea posible.
https://github.com/angular/angular/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

Todavía espero que Angular sea para los desarrolladores, no para el marketing, y espero ver más problemas cerrados en función de la reacción de la comunidad. Esa es mi pregunta, cuáles son las prioridades aquí.

Obviamente, este problema se puede abordar mediante una actualización de la API existente, pero, por separado, he creado una propuesta para mejorar ReactiveFormsModule para abordar una serie de problemas pendientes, incluido este.

Algunos otros problemas abordados incluyen la capacidad de suscribirse a actualizaciones en cualquier propiedad y la capacidad de validar de forma asincrónica un control a través de un servicio (en lugar de asyncValidator ). Puede obtener más información en el n. ° 31963. ¡La retroalimentación es bienvenida!

Como prometí antes , creé Pull Request . Este PR contiene solo una parte de la función @ng-stack/forms (sin: validación, control de formulario de detección automática y entrada de soporte [archivo]).

Oye, quería compartir una actualización del equipo de Angular: te escuchamos que este es un gran problema. Pronto comenzaremos a trabajar en formularios con más mecanografía, que incluirán la revisión de los RP existentes y la revisión de todos sus comentarios nuevamente. ¡Gracias a todos por tomarse el tiempo para dejar sus pensamientos!

¡¡¡¡¡Oh!!!!! ¡Saliendo!

Esas son muy buenas noticias, equipo de Angular, ¡gracias!
Tan pronto como haya un lanzamiento, desaprobaré angular-typesafe-reactive-forms-helper .

¡¡¡SÍ !!!!

¡¡Estoy tan emocionada!! ¡¡Gracias, equipo de Angular !!

¿Podemos detenernos con la reacción al spam?
Use los emojis para las reacciones, ya que están destinados a https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/ - gracias.

Como el equipo de Angular ha confirmado que trabaja en formas reactivas fuertemente tipadas. Me gustaría compartir mis razones de mi implementación que usa mucho infer type para extraer el tipo de valor con el fin de obtener una experiencia de desarrollo de tipo estático sin problemas.

Primer enfoque: comenzó con el tipo de valor

Cuando comencé a diseñar el FormGroup, Utilicé un tipo de valor simple e intuitivo como T .

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

Pero descubrí que cuando tengo que manejar una arquitectura de forma de tabla compleja, es muy difícil hacer un enlace de tipo seguro en HTML angular.

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" />

La implementación anterior requería que los desarrolladores hicieran una conversión de tipos personalizada, lo cual es tedioso y propenso a errores. En mi humilde opinión, esto pierde completamente la razón para usar la forma reactiva fuertemente tipada.

Segundo enfoque: comenzó con un ControlType

Ya que el uso de un tipo de valor simple no funciona correctamente. Se me ocurre otra idea para usar un KeyValueControlcomo T y use infer extraer el tipo de valor de 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" />

Demo en vivo

Edit gaplo917/angular-typed-form-codesandbox

Capturas de pantalla del IDE de demostración en vivo

Esa es una verdadera experiencia de autocompletar en formas complejas.
Screenshot 2020-06-12 at 19 02 10

Formas de tipo angular

100% compatible con los módulos de forma reactiva existentes
https://github.com/gaplo917/angular-typed-forms

Los comentarios y las mejoras son bienvenidos. Espero que las próximas formas reactivas fuertemente tipadas puedan manejar modelos de formas complejas como tablas y subformularios anidados.

@IgorMinar , Angular Core Team y Angular Community Members .

Este largo comentario se centra completamente en dos declaraciones que el autor del ticket resalta "en contra de las prácticas " y " fuertemente mecanografiadas ".

Sugeriría en lugar de un enfoque basado en la interfaz para la forma reactiva fuertemente tipada, deberíamos optar por el enfoque basado en clases, pero no de la manera que se menciona en los ng-stacks / forms . Tampoco recomendaría cambiar la base de código de Angular Ractive Forms, porque podemos lograr una forma fuertemente tipada sin cambiar la base de código de muchas maneras. Permítanme describir en detalle cuáles son los desafíos de alto nivel que veo en el enfoque impulsado por la interfaz y el enfoque impulsado por la clase es más intuitivo que otros y también estamos obteniendo que el objeto FormGroup tiene un tipo fuerte. En ambos casos, nuestro objeto FormGroup es Strongly Typed, no estamos perdiendo el poder del TypeScript Type en el enfoque basado en clases.

Mi sugerencia

Como todos estamos familiarizados con las prácticas de programación orientada a objetos, la clase nos brinda más flexibilidad y capacidad de mantenimiento del código. Algunos de los beneficios que destaco a continuación:

  1. Desacople nuestro código.
  2. Menos código en comparación con el enfoque actual, así como el enfoque basado en interfaz.
  3. Podemos utilizar decoradores personalizados en la propiedad.
  4. El código es legible, mantenible y extensible.
  5. Con este enfoque, no podemos necesitar escribir la lógica empresarial en la plantilla, como si estuviéramos poniendo *ngIf con múltiples condiciones para mostrar los mensajes de error. Creo que las plantillas no están diseñadas para escribir la lógica empresarial.
  6. Mucho más...

Permítanme transformar el código de interfaz mencionado anteriormente en Class y aplicar el decorador de validación en las propiedades Class . Aquí estamos siguiendo las prácticas del principio de responsabilidad única . Eche un vistazo al siguiente código:

image

Consideremos algunos casos y comparémoslo con la interfaz y el enfoque fuertemente tipado impulsado por la clase que nos ayuda a comprender la diferencia en ambos.

1. Cree un FormGroup
Aquí estamos usando la misma instancia de FormBuilder y el mismo método de group . Pero el nombre del módulo de importación será diferente como ReactiveTypedFormsModule lugar de ReactiveFormsModule . Creemos un FormGroup :
image

Según el código anterior, surge la pregunta,

¿El enfoque actual funcionará después de importar el ReactiveTypedFormsModule ?
Sí, funcionará, no se cambiará nada después de importar el ReactiveTypedFormsModule .

Veamos rápidamente los otros casos y concluyamos este artículo.

2. Cambiar el valor de FormControl
En lugar de llamar al método setValue , podemos asignar directamente el valor en la propiedad Class . Establecerá automáticamente el FormControl .

image

3. Realice la lógica empresarial en función de los cambios de valor de FormControl.
En lugar de suscribir el FormControl ValueChanges , use el poder del método setter en TypeScript.

image

4. Convierta el valor de entrada
Nos estamos enfocando en Strongly-Typed, pero ¿qué pasa con los valores que provienen del control de entrada, como para la fecha, obtenemos el valor en el formato String pero esperamos el formato Date en el código TS? para superar este problema, creamos una directiva o método para convertir el valor según sea necesario. No mostraré aquí el código actual ya que es un poco torpe porque tenemos que crear la directiva y hacer las cosas bla, bla ... 😄, así que me gustaría mostrar el código del enfoque basado en clases aquí:

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

4. Cambiar el valor de FormGroup FormControl anidado
Podemos asignar directamente el valor en la propiedad respectiva en lugar de obtener el objeto FormGroup anidado y llamar al método SetValue .

image

5. Agregar FormGroup en FormArray anidado
No más que decir, eche un vistazo al siguiente código 😄.

image

Hacer referencia al objeto FormGroup en la plantilla HTML

Código simple 😄. No se cambiará nada en la plantilla HTML, pero también obtendrá más en la plantilla HTML. Consulte el siguiente código

<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>

Enlace Stackblitz : Ejemplo de trabajo de forma fuertemente reactiva
Ejemplo en Github : forma reactiva fuertemente tipada

@ajayojha ,

  1. La sobrecarga que acompaña a los tipos de TypeScript es mala.
  2. Validación en tiempo de ejecución con la ayuda de decoradores: es bueno.
  3. ¿Por qué necesitas setValue() y valueChanges() si hay setters / getters?

Que pienso:

  1. Escribir tipos de TypeScript es como escribir pruebas estáticas. Alguien puede crear aplicaciones sin pruebas porque cree que es innecesario, pero es una mala práctica.
  2. Validación en tiempo de ejecución con la ayuda de decoradores: puede ser una buena idea, de acuerdo.
  3. Además de setValue() también hay patchValue() , reset() , que también funcionan con el valor de formulario. Reemplazar solo setValue() con un establecedor hará que el código sea inconsistente. Además, cuando tenemos que escribir establecedores para cada propiedad del modelo de formulario, agregará mucha más sobrecarga de código, así como sobrecarga de rendimiento en el caso de getters. En mi opinión, mezclar nombres de propiedades de modelo de formulario con propiedades de control de formulario también es una mala idea.

Gracias, @KostyaTretyak por sus inquietudes sobre mi comentario, estaría feliz de responder lo mismo, por favor vea abajo mis comentarios en consecuencia :).

  1. La sobrecarga que acompaña a los tipos de TypeScript es mala.

Solo para su información, el objeto formgroup está fuertemente tipado. Las interfaces son buenas pero no adecuadas para todas las áreas y no estoy diciendo que los tipos de TypeScript sean malos, no creo que en alguna parte haya mencionado que los tipos sean malos. Mi única preocupación es la Interfaz porque estamos anulando las prácticas de diseño de software con el enfoque de Interfaz o incluso puedo decir que estamos usando el enfoque de Interfaz en el lugar equivocado y mi enfoque sugerido es Clase. En lo que respecta a mi comprensión a través del enfoque de clase, no estamos comprometiendo el beneficio de los tipos de TypeScript que obtenemos en la interfaz o incluso diría que obtenemos más que el enfoque de interfaz en términos de legibilidad, escalabilidad y capacidad de mantenimiento.

¿Estamos utilizando la práctica correcta de Interface para formas reactivas fuertemente tipadas?

Permítanme describir un poco más en términos de Interfaz es la mala práctica (según yo) para la forma reactiva fuertemente tipada.

Los tipos de TypeScript son buenos, pero no se sugiere que en todas partes tengamos que mezclar cualquier cosa que no esté de acuerdo con las prácticas del software. Como he mencionado claramente los problemas con la interfaz. Solo para pensar en mis preocupaciones destacadas sobre Interface. Permítanme compartir mi caso, en una de mis aplicaciones empresariales que contiene más de 6k + componentes. Si opto por el enfoque de interfaz, el equipo de desarrollo me hará buenas preguntas antes de realizar los cambios:

  • Qué interfaz tenemos que usar y dónde, ya que la misma entidad se usa en múltiples componentes con diferentes propiedades. Entonces, ¿tenemos que crear el componente de interfaz independiente? Si es así, lea el segundo caso.
  • Si seguimos el enfoque anterior, ¿cuál será el enfoque en el que tendré que usar ambas propiedades de la interfaz en uno o muchos componentes? Para solucionar este problema, puedo crear una interfaz más y extender la misma. ¿Es bueno crear más y más archivos solo con el movimiento de una forma reactiva de tipo fuerte? ¿Qué pasa con la capacidad de mantenimiento? No tengo la respuesta, excepto para decir que el equipo de Angular está proporcionando esta solución, por lo que es buena :) (si el equipo de Angular opta por el enfoque de interfaz).
  • Si optamos por un enfoque a + b, en algunos de mis componentes se requieren pocas propiedades, no todas, ¿entonces? Tengo tres soluciones para dar a mi equipo de desarrollo.

    • Cree una nueva interfaz y copie / pegue las propiedades requeridas en la interfaz recién creada. Este es el enfoque más sucio del mundo del software. Esto crea muchos problemas cuando se cambia una propiedad individual en el lado del servidor, entonces es difícil rastrear las áreas por igual en cuántas interfaces cambiar el nombre de la propiedad.

    • Establezca la propiedad como anulable. Si estoy definiendo la propiedad anulable, ¿por qué tengo que seguir el enfoque 'B'? Nuevamente, no tengo una respuesta :( para darle a mi equipo de desarrollo.

    • No cree otra interfaz, use el tipo de utilidad 'parcial' y haga que todas las propiedades sean opcionales. Al hacer esto, estamos perdiendo el beneficio real de la interfaz. Esto también va en contra de las prácticas. Si tengo que seguir esto, entonces ¿Por qué tengo que seguir el enfoque 'A', nuevamente No hay respuesta :).

    • Si estoy haciendo que todas / algunas propiedades sean anulables, ¿qué pasa con la legibilidad del código y cómo puedo juzgar cuántas propiedades se requieren para pasar el valor al servidor? Luego tengo que verificar el componente respectivo y echar un vistazo. Importante problema de legibilidad del código.

Ahora, solo para pensar en los casos anteriores en una perspectiva más amplia y compararlos con los tipos de TypeScript con interfaz en formularios reactivos para tipos fuertes. Creo que cada buen enfoque ahorrará tiempo de desarrollo y en este enfoque. Lamento decir que no veo ningún beneficio de acuerdo con los Principios y Prácticas de Diseño de Software.

  1. Validación en tiempo de ejecución con la ayuda de decoradores: es bueno.

Estoy de acuerdo con su comentario sobre " Está bien ", el enfoque del decorador que no podemos lograr en el enfoque de interfaz. Creo que esta es la característica más poderosa de TypeScript, entonces por qué no podemos usar la misma en Reactive Form Approach y darle al equipo de desarrollo el control total de las propiedades de sus objetos.

  1. ¿Por qué necesita setValue () si hay establecedores?

¿Dónde he dicho que necesito 'setValue ()'? No necesito setValue y no he mostrado en el ejemplo donde estoy llamando al método setValue en Class Driven Approach. Por favor, corríjame si estoy equivocado.

  1. Escribir tipos de TypeScript es como escribir pruebas estáticas. Alguien puede crear aplicaciones sin pruebas porque cree que es innecesario, pero es una mala práctica.

No estoy diciendo que los tipos de TypeScript sean como escribir pruebas estáticas. Pero no estoy de acuerdo con los cambios de confirmación en las clases base de forma reactiva, creo que podemos lograr lo mismo sin tocar la definición de clase. Aquí, podemos usar la potencia real de la interfaz que no estamos usando según las confirmaciones hasta ahora. ¿Es una buena práctica que la lógica se esté ejecutando durante tanto tiempo y estamos agregando los tipos genéricos configurando el valor predeterminado de ' ningún'?
Creo que podemos lograr lo mismo sin tocar las clases base de Reactive Form. No sé por qué no estamos aprovechando Interface en esto en lugar de cambiar la definición de clase base y también cambiar la especificación.

  1. Validación en tiempo de ejecución con la ayuda de decoradores: puede ser una buena idea, de acuerdo.

Es bueno saber que ambos somos la misma página en esto :).

  1. Además de setValue () también hay patchValue (), reset (), que también funcionan con form value. Reemplazar solo setValue () con un setter hará que el código sea inconsistente. Además, cuando tenemos que escribir establecedores para cada propiedad del modelo de formulario, agregará mucha más sobrecarga de código, así como sobrecarga de rendimiento. En mi opinión, mezclar nombres de propiedades de modelo de formulario con propiedades de control de formulario también es una mala idea.

Permítanme describir el punto anterior en tres secciones que llaman al método, la sobrecarga de desempeño del definidor y las propiedades del modelo de formulario de mezcla.

Método de llamada: Como era de esperar, mientras escribía esta publicación, pensaba que alguien podría sugerirme usar el método 'patchValue' o 'reset'. Nuevamente, me gustaría decir que en el caso del mundo real, la mayoría del equipo de desarrollo está usando el método 'setValue' en lugar de patchValue u otros métodos (esta es mi experiencia de acuerdo con Angular Application Code Review y Stackoverflow Posts setValue vs patchValue). Mi enfoque es simplemente llamar al método para asignar el valor, sin importar qué método estemos solicitando.

Rendimiento de Setter : Estoy de acuerdo con la afirmación de que los establecedores crean una sobrecarga de rendimiento. Si este es el caso, entonces diría que tenemos que arreglar primero en Angular Project porque para vincular la forma reactiva, Angular Framework está usando el método setter en la clase Control Value Accessor y muchas otras directivas y esto crea una sobrecarga de rendimiento sin usar el Enfoque de clase. Una cosa más, el mismo enfoque que también estamos usando en múltiples componentes con el decorador @Input , tenemos que encontrar que el equipo alternativo o angular debe proporcionar una solución diferente (creo) para superar este tipo de problema de rendimiento. Entonces, no creo que esto sea una preocupación importante. Ahora, llegando a la preocupación por el rendimiento, compárelo con el enfoque existente y el enfoque del método de establecimiento (esto es opcional, el equipo de desarrollo puede optar si desea lo mismo que ChangeDetectionStrategy en Angular. Consulte el ejemplo en el sitio de documentación de rxweb para optar. este caso. Juzgue que cuántas funciones están llamando cuando suscribimos el valor cambia luego de establecer el valor o llamar directamente al método setter. Creo que esto es mucho más intuitivo en términos de menor ejecución de código en comparación con los cambios de valor, tamaño bajo de compilación paquete, legibilidad del código y muchas otras cosas buenas.

Mezcla de las propiedades : Entonces, ¿cuál es su opinión? ¿Está asignando un nombre de propiedad FormControl diferente al nombre de propiedad devuelto por el servidor? En caso afirmativo, diría que este es un problema importante en el código porque cada vez que tengo que cambiar el nombre de la propiedad antes de publicarlo en el servidor, lo siento, pero no lo preferiría en toda la aplicación. Si considero su buena opinión para mi formulario de solicitud, que contiene un promedio de más de 40 campos, entonces tengo que establecer cada valor de propiedad manualmente, solo para pensar en el código en el componente solo por la sacudida de asignar el valor y el tamaño de compilación de la producción. ¿Es esta una mejor opinión que el enfoque de clase?
Ahora pasemos a la solución propuesta, no estamos mezclando dos cosas en una. Las propiedades de FormControl son diferentes y las propiedades de clase son diferentes del tipo de datos respectivo. Si desea cambiar el nombre de la propiedad, ya que el nombre de la propiedad FormControl es diferente a la propiedad Data, entonces puede hacerlo, consulte la documentación del paquete de formulario reactivo de rxweb. Entonces no hay problema ya que su sensación de mal (mezclar el nombre de la propiedad con los nombres de control de formulario) tiene una solución en el enfoque propuesto.

Espero haber respondido a todas sus inquietudes. Siéntase libre de compartir si tiene alguna otra inquietud sobre esto :).

Como dije en mi comentario anterior, no hay necesidad de cambiar las clases base de la forma reactiva porque podemos lograr las mismas cosas utilizando el poder de las prácticas del principio de segregación de interfaces. Aquí está la solución de forma reactiva fuertemente tipada de extremo a extremo con el paquete de

¿Cómo se ve el código después de la implementación?

Stackblitz: Abierto
Github : Ejemplo de forma reactiva fuertemente tipada impulsada por interfaz

Alguien tiene alguna sugerencia, no dude en compartirla.

Entonces, la versión 10 de Angular ahora disponible , esta es una versión importante y las formas aparentemente reactivas no se escribirán fuertemente hasta al menos la versión 11 de Angular. Entonces, tendremos que esperar al menos hasta el otoño para implementar esta función.

Tengo una pregunta (¿o una propuesta?) Con respecto a la forma en que se construye el modelo de formulario en la mayoría de las sugerencias / RP que vi aquí.

Al mirar la mayoría de las bibliotecas y PR que intentan hacer que el tipo de formularios reactivos sea seguro, puede ver que crean un modelo que se ve así:

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

esto luego se "traduce" a algo como esto:

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

Entonces, en palabras simplificadas: "Si es un objeto, entonces construya un FormGroup para él. Si es una matriz, entonces construya un FormArray. Y si es un valor primitivo, entonces cree un FormControl".

Pero hay un problema: ya no puede usar objetos en FormControls.

Las soluciones que vi hasta ahora: algunas bibliotecas simplemente no admiten esto. Y algunas bibliotecas usan algún tipo de "truco" para crear una pista de que realmente desea usar un FormControl en lugar de un FormGroup.

Mi pregunta / propuesta: ¿Qué hablaría en contra de definir explícitamente el modelo de formulario de la siguiente manera?

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'),
  })
})

Esto tiene la gran ventaja de que ahora puede colocar objetos en FormControls. Y no requiere ningún tipo de "truco" para hacerlo :)

He creado un Codesandbox para esto, para que tal vez puedas probarlo tú mismo: https://codesandbox.io/s/falling-grass-k4u50?file=/src/app/app.component.ts

@MBuchalik , sí, esta es la primera decisión obvia que se te viene a la mente cuando comienzas a trabajar en "formas de escritura fuerte". También comencé con esto, pero tiene una desventaja significativa: la necesidad de crear dos modelos: uno para controles de formulario y el otro, para valores de formulario.

Por otro lado, hasta donde tengo entendido, esta solución nos permitirá implementar "formas de tipo fuerte" sin romper el cambio, y no tendremos que esperar al lanzamiento de la próxima versión principal de Angular. Aquí es necesario trabajar en la práctica con dicha solución para evaluar si tiene deficiencias más críticas que la necesidad de crear dos modelos.

@MBuchalik Compartí las mismas opiniones con usted y le planteé la misma pregunta al PR y uno de los contribuyentes angulares ( @KostyaTretyak ) ha respondido.

Puede echar un vistazo a la discusión en el PR:
https://github.com/angular/angular/pull/37389#discussion_r438543624

TLDR;

Aquí hay un par de problemas:
Si seguimos su enfoque, necesitamos crear dos modelos diferentes: para controles de formulario y para valores de formulario.
El modelo de los controles de formulario es más difícil de leer.

Y he implementado esta idea para nuestros proyectos hace medio año que ya se utilizan en la producción. Una experiencia de autocompletado estático COMPLETO para el tipo de controles en HTML angular realmente aumenta la productividad de nuestro desarrollador junior. (con fullTemplateTypeCheck habilitado)

He compartido "por qué voy de esta manera" en el comentario anterior en este hilo:
https://github.com/angular/angular/issues/13721#issuecomment -643214540

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

¡Gracias por tus ideas @KostyaTretyak y @ gaplo917! 👍

Si lo entendí correctamente, podemos resumirlo de la siguiente manera.

Si solo queremos usar un solo modelo, entonces se podría usar una solución como la proporcionada por @KostyaTretyak . Sin embargo, la desventaja es que ahora ya no podemos usar objetos en FormControls. (Sé que hay un "truco" que permitiría esto. Pero, de nuevo, nuestro modelo no está "limpio", por lo que nuevamente necesitaríamos 2 modelos).

Si queremos poder usar objetos en FormControls, entonces probablemente (!) No hay forma de evitar el uso de un enfoque como el que ilustré I (o @ gaplo917). La desventaja es que básicamente necesitas 2 modelos. O al menos utilice algunos tipos de ayuda para "extraer" el modelo de valor de formulario.

Entonces, ahora solo tenemos que pensar si los objetos en FormControls deberían ser posibles o no. Esto simplemente respondería a la pregunta sobre cuál de los dos enfoques es el que se debe seleccionar. ¿O me estoy perdiendo algo?

¡Gracias por tus ideas @KostyaTretyak y @ gaplo917! 👍

Si lo entendí correctamente, podemos resumirlo de la siguiente manera.

Si solo queremos usar un solo modelo, entonces se podría usar una solución como la proporcionada por @KostyaTretyak . Sin embargo, la desventaja es que ahora ya no podemos usar objetos en FormControls. (Sé que hay un "truco" que permitiría esto. Pero, de nuevo, nuestro modelo no está "limpio", por lo que nuevamente necesitaríamos 2 modelos).

Si queremos poder usar objetos en FormControls, entonces probablemente (!) No hay forma de evitar el uso de un enfoque como el que ilustré I (o @ gaplo917). La desventaja es que básicamente necesitas 2 modelos. O al menos utilice algunos tipos de ayuda para "extraer" el modelo de valor de formulario.

Entonces, ahora solo tenemos que pensar si los objetos en FormControls deberían ser posibles o no. Esto simplemente respondería a la pregunta sobre cuál de los dos enfoques es el que se debe seleccionar. ¿O me estoy perdiendo algo?

@MBuchalik En mi opinión, si confía en el compilador de TypeScript y confía mucho en la función de "inferencia de tipo", no necesita tener 2 modelos. Nuestro sistema interno tiene más de 60 formularios, algunos de ellos son muy complejos anidados con 3 niveles de profundidad FormArray-FormGroup-FormArray y tampoco necesitamos un modelo explícito para el tipo de valor.

Solo hay 2 tipos de modelos de datos con los que jugar, que son:

  • Modelo de respuesta / solicitud de datos API
  • Modelo FormControl

99,9% de las veces,

  1. Cree una encapsulación para cada forma compleja
  2. Transformar los datos remotos -> formulario de datos
  3. Transformar los datos del formulario -> carga útil remota

el siguiente fragmento de código es la ilustración:

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)
   }
}

PD: Esta es una arquitectura de flujo de datos obstinada que podemos disfrutar programando con seguridad en TypeScript.

Si solo queremos usar un solo modelo, entonces se podría usar una solución como la proporcionada por @KostyaTretyak . Sin embargo, la desventaja es que ahora ya no podemos usar objetos en FormControls. (Sé que hay un "truco" que permitiría esto. Pero, de nuevo, nuestro modelo no está "limpio", por lo que nuevamente necesitaríamos 2 modelos).

Aquí todavía es necesario estimar la frecuencia con la que necesitamos usar un objeto por FormControl . Creo que se puede estimar en algún lugar entre el 5 y el 30%. Es decir, si usamos soluciones con un modelo, podemos cubrir el 70-95% de los casos de uso de FormControl . Por lo demás, solo proporcione una pista para TypeScript como un tipo adicional (consulte Control<T> , no es correcto llamarlo un "segundo modelo"):

interface FormModel {
  date: Control<Date>;
}

¿Se puede llamar truco al tipo Control<T> ? - Sí, probablemente sea un truco, pero no un truco tosco. No conozco ningún caso en el que este tipo no funcione según lo previsto o tenga efectos secundarios.

Oh, recordé los efectos secundarios con Control<T> cuando necesitamos usar una biblioteca externa para el modelo de valor de formulario. En tales casos, realmente se necesitan dos modelos:

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.

Pero en este código, la sobrecarga solo está aquí:

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

Gracias a @ArielGueta , ahora se conoce un problema crítico con el tipo Control<T> . Es decir, ni siquiera intentaré implementar Control<T> en futuras solicitudes de extracción para Angular, como planeé antes.

Gracias a @ArielGueta , ahora se conoce un problema crítico con el tipo Control<T> . Es decir, ni siquiera intentaré implementar Control<T> en futuras solicitudes de extracción para Angular, como planeé antes.

@KostyaTretyak No es cierto. El problema crítico solo muestra que su implementación de "ControlType" es incorrecta.

Una implementación completa de "Tipo de control" no tiene ningún problema.

Demostración en vivo: https://codesandbox.io/s/lucid-bassi-ceo6t?file=/src/app/demo/forms/type -test.ts

Screenshot 2020-07-01 at 00 35 11

Es decir, ni siquiera intentaré implementar Controlen futuras solicitudes de extracción para Angular, como lo planeé antes.

Bien, eso significa que su PR (y los consecutivos) probablemente nunca admitirán objetos en FormControls.

@MBuchalik , por el momento (Angular v10), si tenemos el siguiente modelo de formulario:

interface FormModel {
  date: Date;
}

y si en nuestro componente queremos acceder al valor de la propiedad date , debemos hacer lo siguiente:

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

Mi solicitud de extracción actual proporciona un valor genérico para el formulario, pero no proporciona un tipo para el control del formulario:

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

@ gaplo917 , @MBuchalik , probé sus soluciones e intenté implementar mi propia solución similar, pero no todas funcionan a la perfección. Esta solución también proporciona un conjunto de tipos para extraer de forma recursiva valores de modelo de formulario. Los cambios generales y de ruptura son muy importantes, consulte el borrador de relaciones públicas .

Dudo mucho que en este momento estas soluciones deban proponerse para ser implementadas en Angular. Es decir, por ahora tendremos que usar genéricos solo para valores de formulario, no para tipos de control de formulario.

pero no todos funcionan a la perfección

Solo he dedicado unas pocas horas a mi ilustración, así que no esperaba que fuera perfecta;) ¿Podrías dar ejemplos de cosas que no funcionan bien? (¿Especialmente en cosas que, desde su punto de vista, no se pueden arreglar fácilmente?)

Por cierto, una sugerencia con respecto a la compatibilidad con versiones anteriores: desde mi punto de vista, es relativamente difícil hacer que la implementación sea completamente compatible con versiones anteriores. Debido a esto, tal vez podríamos hacer lo siguiente: No cambiamos las clases FormControl, FormGroup y FormArray en absoluto. En su lugar, creamos nuevos que heredan de ellos (tal vez llamémoslos StrictFormControl<T> y StrictFormGroup<T> o como quieras). Estos son entonces los que hacemos que el tipo sea seguro. El beneficio: estamos 100% seguros de que no se realiza ningún cambio importante. :)

pocas horas en mi ilustración, así que no esperaba que fuera perfecta;)

Trabajé con esta solución durante un par de días y veo lo difícil que será trabajar con formularios.

  1. En primer lugar, gastos generales importantes y la necesidad de tener dos modelos.
  2. En segundo lugar, esta solución no es mejor en términos de confiabilidad que mi solución con el tipo Control<T> , porque de la misma manera es necesario extraer de forma recursiva el valor del modelo de formulario.
  3. Trabaja con controles de formulario anidados. Si tenemos el siguiente modelo de formulario:
interface FormModel {
  one: FormGroup<{two: FormControl<string>}>;
}

Y si obtenemos formGroup.controls.one.value , TypeScript proporciona una pista con el tipo condicional, no con el tipo {two: string} (como debería ser). Por lo tanto, es un valor difícil de leer del IDE.

Y si obtenemos formGroup.controls.one.value, TypeScript proporciona una pista con el tipo condicional, no con el tipo {two: string} (como debería ser). Por lo tanto, es un valor difícil de leer del IDE.

Bien, solo para asegurarme de que entendí todo correctamente. Si usa mi implementación y escribe lo siguiente:

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

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

(lo hizo un poco más detallado;))

Si ahora busco myForm.controls.one.value entonces se ve así:

grafik

Entonces, ¿dice que, en este ejemplo, "dos" no deberían ser opcionales? Supongo que esta no es la forma correcta de escribir el valor del formulario. El valor del formulario solo incluye campos no deshabilitados. Entonces, desde mi punto de vista, debería ser un Parcial recursivo. No puede saber en tiempo de compilación qué campos estarán deshabilitados y cuáles no.

Entonces, ¿dice que, en este ejemplo, "dos" no deberían ser opcionales?

¿Qué? No.

Mi prueba de tu solución:

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

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

Lo que veo en codesandbox después de value :

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

Aquí PartialFormGroupValue refiere al tipo condicional PartialFormValue .

Ah, está bien, creo que lo tengo. Entonces quieres decir que el tipo es difícil de leer, ¿verdad? Originalmente pensé que estabas hablando de un error o algo así.

Bueno, la mayoría de los IDE solo presentarán las sugerencias para las propiedades disponibles una vez que continúe escribiendo. Así que realmente no veo grandes problemas aquí. (Por supuesto, sería mejor leer si solo dijera {two?: string} . Pero no creo que esto sea muy importante. Esa es al menos mi opción).

Si implementó su Control<T> , ¿cómo lo eliminaría de la escritura del valor del formulario sin hacer algo como lo hice yo? ¿Y cómo convertiría el valor del formulario en un parcial recursivo sin utilizar un tipo auxiliar?

Si implementó su Control, ¿cómo lo eliminaría de la escritura del valor del formulario sin hacer algo como lo hice yo? ¿Y cómo convertiría el valor del formulario en un parcial recursivo sin utilizar un tipo auxiliar?

En este caso, mi solución no es mejor:

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

Di este ejemplo porque lo pediste:

¿Podría dar ejemplos de cosas que no funcionan bien? (¿Especialmente en cosas que, desde su punto de vista, no se pueden arreglar fácilmente?)

Por cierto, solucioné un problema crítico con Control<T> .

Para resolver los problemas de enlace HTML con Angular 10 y [formControl] , esta es la ruta que seguí:

Como se señaló en otro número (https://github.com/angular/angular/issues/36405#issuecomment-655110082), para mis formularios generalmente creo clases que extienden FormGroup para facilitar la reutilización y las pruebas . Con esa estructura, pude resolver el problema por ahora actualizando mi código de algo como esto:

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

A esto:

// 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());
}

En ese punto, form.controls mostrará correctamente su tipo como { id: FormControl, name: FormControl } , vinculando correctamente en el HTML y se agregará correctamente si el formulario fuera más complicado con grupos de formularios o matrices anidados.

Usar la función formDefinition no es bonito, pero fue la solución más limpia que pude encontrar para evitar la duplicación entre la definición del formulario y el constructor.

Creo que podría actualizar FormGroup para tener la definición de tipo genérico anterior sin introducir cambios importantes (bueno, eso puede no ser cierto para los formularios que agregan / eliminan controles dinámicamente, supongo; no se mostrarían en controls tipo)

editar
Parece que es incluso más sencillo si no necesita crear clases que amplíen FormGroup; podría crear una función auxiliar que resuelva el 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
... o podrías hornearlo en la clase FormGroup sí ( FormBuilder ¿tal vez?)

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
Extendí los ejemplos anteriores para incluir escribir en value sy creé un artículo para resumir todo:

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

Esto ahora está marcado en la hoja de ruta para el desarrollo futuro: https://angular.io/guide/roadmap#better -developer-ergonomics-with-strict-typing-for-angularforms

@pauldraper ¿Podría explicar qué ha cambiado en comparación con la hoja de ruta de hace ~ 2 meses? El único cambio que veo es la redacción del título. Pero todavía está en la sección "Futuro". Al igual que hace 2 meses.

@MBuchalik quizás haya estado allí durante 2 meses.

¿Fue útil esta página
0 / 5 - 0 calificaciones