Angular: 响应式表单不是强类型的

创建于 2016-12-30  ·  90评论  ·  资料来源: angular/angular

[x] feature request
  • 角度版本: 2

反应式表单旨在用于复杂表单,但控件的valueChangesObservable<any> ,这完全违背了复杂代码的良好做法。

应该有一种方法可以创建强类型表单控件。

forms feature high

最有用的评论

嘿,我想分享 Angular 团队的更新:我们听说这是一个很大的痛点。 我们很快就会开始处理更强类型的表单,其中包括查看现有的 PR 并再次审查您的所有评论。 感谢大家抽出时间留下您的想法!

所有90条评论

相关 #11279

这与#11279 无关。

请解释它是如何不相关的?
你想要的是抽象控制是通用的吗? 这是 valueChanges 可以具有不是Observable<any>的类型的唯一方法,没有其他方法可以推断类型。
这正是#5404 所问的,这意味着这与#11279 有关
如果有另一种方法可以在不使 AbstractControl 成为通用的情况下实现,请解释。

在#11279 中使用get<Type>绝对是错误的解决方案。 如果 TypeScript 有类似 Java Unbounded Wildcard 的东西, get会使用它,而不是any 。 也许可以使用空界面以相同的方式完成某些事情?

还有https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html keyof 。 研究 TypeScript 2.1 的特性来实现强类型表单控件可能非常有趣。

不幸的是,目前的方式我不认为它可用于大型应用程序,我需要在它之上设计一些东西。

我刚刚注意到在 TS 2.2 (https://github.com/Microsoft/TypeScript/wiki/Roadmap#22-february-2017) 中,他们计划了默认通用类型 (https://github.com/Microsoft/TypeScript/问题/2175)一旦我们有了这个,我认为重新审视这个问题可能是个好主意,因为我们可以使AbstractControlAbstractControl<T = any>那样泛型,其中TvalueChanges返回的值将是Observable<T> 。 目前这样做不是一个好主意,因为它会发生巨大的破坏性变化,但使用默认泛型,除非我误解了它们,否则它不会是破坏性的变化。

对此的小更新,看起来默认泛型已移至TS2.3 。 因此,随着 Angular 4.0 版对 TS 2.1 的支持,不久之后他们就能支持 TS 2.3,现在我们应该等待重新审视这个。

带有默认泛型类型的 TypeScript 2.3 已经在这里,我们是否有任何计划在 angular 支持 TS 2.3 时准备好?

@desfero正在等待 #16707 将构建升级到 TS2.3

+1 很想看到这个功能。 有人在努力吗?

对此的小更新:
根据我在这里的评论: https :
在不破坏更改的情况下,不可能在当前的 Forms API 中实现泛型。
因此,需要进行重大更改
或者一个完整的表格重写

所以原来我之前的评论是不正确的。
我能够在当前的 Forms API 上实现这一点,如您所见 #20040

@Toxicable仍然存在缺乏安全重构能力的问题。 例如,get('person') 并没有真正使用符号本身。 上面的例子来自@rpbeukes ,有一种基本使用对象符号的改进方式,例如。 get(obj.person) 不使用字符串。 这比仅具有返回类型更可取。

@howiempt

例如,get('person') 并没有真正使用符号本身

我不知道你的意思是什么,你在这里指的是什么符号?
在我的实现中,您可以执行以下操作

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) 不使用字符串

这缺乏遍历多个 FormGroup 的能力。
虽然我的方法无法推断这种情况下的类型,但我的 PR 的想法是在不破坏更改或引入任何新 API 的情况下添加泛型类型(泛型除外)

@Toxicable我知道你的改变是为了不破坏事情,而不是试图批评你的解决方案。 另一个实现(改造)允许使用实际属性而不是字符串。 通过按字符串引用该字段,如果该属性名称更改,则构建中断,这对我来说不是很安全。 例如,将字段名称从 'name' 更改为 'firstName',如果我不更改所有 g.get('name') 引用,则会中断。 如果我可以做类似的事情

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>

它们都是严格的参考。 改造解决方案以一种有点hacky的方式完成它,但也解决了这个问题。

@Toxicable thanx 用于公关。 期待使用它:)

我同意@howiempt ,如果我们能得到这样的东西,那将是一等奖:

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

同样,我真的不知道这在更大范围内的可行性如何。
我相信你的判断。

保持良好的工作状态,感谢快速响应。

我认为这种访问其他控件的方法与添加泛型无关。
但是,请随意打开另一个问题

我真的不认为设置返回类型真的是“强类型”,似乎需要一半的实现,但这是朝着正确方向迈出的一步。

嗨,我已经发布了https://github.com/Quramy/ngx-typed-forms来解决这个问题。 请检查一下😄

@Quramy 几周前我尝试使用你的包,我记得,它并没有真正做很多强制执行:(

+1。 当我希望它被实施时,无法计算实例的数量。

相同的。
Angular Reactive 表单是真正击败任何其他框架的功能之一。 使用强类型的响应式表单将把它带到一个新的水平,进一步扩大与竞争的差距:)

这可以通过条件映射类型和递归来完成..... 条件映射类型刚刚合并到打字稿中。 如果这将被发布,我们有机会使强类型表格成为可能

等不及了(

遗憾的是, @Quramy解决方案没有将所有参数的结构强制执行到FormBuilder 。 同样通用的FormGroup<T>FormControll<T>FormArray<T>不能直接使用,因为它们只是不事件扩展AbtractControl<T> 。 这对于我们当前的项目来说是不够的。

使用ngx-strongly-typed-forms我现在自己发布了一个强类型表单项目
它在向后兼容性方面有点破坏,因为它不使用默认泛型。 因此,它强制您明确地为您的控件提供任何类型,但为所有其他部分增加了更多的类型安全性,并且 API 与 @angular/forms 的当前实现兼容。
也许这是一个有效的替代方案,直到此功能在 Angular 中实现。

+1 这是一个强大的功能..

应尽快实施)

它的编码方式

我用它作为临时解决方法:
希望能帮助到你

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

这是我在我从事的项目中多次考虑过的问题,但我还没有使用足够的 JavaScript 代理来了解这会对观察这些值的任何事情产生的性能影响。

我只是在 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;
            }
        });
    }
}

这个解决方案绝对不是超级完美的,最好让 FormGroup 生成访问属性上的值(例如 myGroup.fields,其中“字段”将是提供的类型)。 但这确实提供了强类型。 要使用它:

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

通过在当前项目中使用我的表单类型,

但我想讨论一下,将泛型放在当前 API 上是否是正确的决定。 在为所有表单控件构建类型时,我发现了很多边缘情况和无法输入或难以输入的东西,因为我认为在那个时候静态输入是不可能的,所以不是最大的问题之一。
可悲的是,这针对AbstractControl#value的主要功能,它必须类似于DeepPartial<T>AbstractControl#get ,每个子类都有不同的实现。
向后兼容也会失去一些由any类型的失败案例引起的类型安全性。
也许考虑为反应式表单使用新的 API 也是解决此问题的一个选项?

所以,这就是我最终在实际解决方案发生时所做的。

免责声明...我刚开始使用 Angular,但对 Typescript 非常熟悉,所以我不完全理解反应式表单...这是最终对我有用的东西,但当然它并不完全完整,我只是输入了 FormGroup,但是我相信随着我对表格的了解更多,需要输入更多的东西......

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

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

然后我可以像这样使用它

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 正在搜索这个确切的问题! 哈哈

@cafesanu这是您键入的 FormGroup 的一些改进以检查构造函数。

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

用法 :

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

我围绕 FromControl 编写了一个小包装器,它允许根据对构建器的调用动态生成数据类型: https :

动态构造的类型确保表单的形状符合您的期望,而不必预先声明接口类型,然后希望您做出正确的调用来创建表单。

我很想在某个时候在 Angular 中看到类似的东西。

用法如下所示:

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

大家好,在过去的 3 天里,我尝试使用d.ts gist为 ReactiveForms 类定义更严格的定义,从而创建与原始 angular 类兼容的新 Typed 接口。
我认为这可能是您问题的可能解决方案/解决方法😉

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

stackblitz 中的使用测试 - 请下载代码并在本地 VSCode 中运行我不知道为什么在 stackblitz 上 setValue 和 pathValue 的错误不正确...

在这个twitter 线程上,我与@IgorMinar讨论了一些“未来的想法”(在 V8+ 之后)

非常欢迎任何意见、建议和帮助!

我的解决方案称为@ng-stack/forms 。 有趣的功能之一是自动检测表单类型。

因此,无需在您的组件中执行此操作:

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

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

现在这样做:

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

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

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

有关更多信息,请参阅@ng-stack/forms

嘿,我们最近一直在与@zakhenry 合作做两件事:

  • 改进表单类型
  • 改进管理子表单的方式(在子组件内)

我们没有像@dmorosinotto那样涵盖所有类型,但我们有一些安全性,这在进行重构时肯定会有所帮助(对于 ts 和 html :tada:)。

如果有人想看看这个库: ngx-sub-form

@maxime1992我看过Controls<T>但它没有严格AbstractControl to <T[K]>
我能问你为什么吗? 您是否认为需要在运行时使用 setControl 或 registerControl 方法来更改类型以重新定义和更改 FormControl 以及相关联的类型,因此您将其保留为“无类型”?
因为我的 TypedForms.d.ts 有点严格并且“强制” AbstractControlTyped to the type T<P>但我不知道是否通过这种选择强制/禁用原始 ReactiveForms API 中允许并且可能被使用的内容某人...

任何想法? 有什么实际情况需要考虑吗?
对此的任何评论都可能帮助我决定如何更改我创建的定义和我正在处理的 PR...
谢谢

PS:在 ngx-sub-form 上做得很好👍 使用 ControlValueAccessor 来处理子表单的想法也是我在尝试的东西 😉

对于@ng-stack/forms添加了支持类型验证。

FormControlFormGroupFormArrayFormBuilder所有方法
接受“错误验证模型”作为泛型的第二个参数:

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

默认情况下使用名为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包含从typeof Validators提取的属性列表,以及预期的返回类型:

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 非常感谢,但我无法使用
Validators.compose([Validators.required, Validators.maxLength(20), Validators.minLength(3)])

@youssefsharief你能告诉我你的验证器问题的更多细节吗? 您在使用 .d.ts 时发现了什么样的错误/问题?

如果您可以发布示例代码或堆栈闪电战,我会关注它,如果我找到解决方案,我会尝试帮助解决问题😉

对于@ng-stack/forms添加了input[type="file"]

另请参阅stackblitz 上的示例

我还使用了“经典”方法,使用FormData上传文件,但有几个好处:

  • 自动检索表单输入名称,例如<input type="file" name="someName"> -> formControl.valuesomeName字段名称;
  • 支持multiple属性并正确设置字段名称(注意userpic[]此处)
<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从我目前看到的情况

我们几乎处于 angular 8 并且在 100% TypeScript 环境中仍然没有开箱即用的类型化表单,这对我来说很奇怪。

@kroeder ,我刚刚看到当前问题是在两年多前创建的(几乎是在 Angular 2 发布后立即创建的),我看到类似的 Pull Request是在 1.5 年前创建的,没有合并......

但是如果@ng-stack/forms会流行并且兼容 Angular 8+,也许我会在未来创建 Pull Request。

@KostyaTretyak听起来很棒:) 我一直在使用https://github.com/no0x9d/ngx-strongly-typed-forms ,它在本线程前面提到过,由@no0x9d创建,您的库与此有何不同?

@ZNS ,我不知道,但经过快速审查,我认为ngx-strongly-typed-forms没有通用的验证,以及自动检测表单控件的适当类型。 也许我错了。

@ZNS @KostyaTretyak你好。 如上所述,我是ngx-strongly-typed-forms 的作者
我快速回顾了@ng-stack/forms的功能集,并认为存在一些小的差异。

几乎所有解决方案或变通方法与我的项目之间都存在概念差异。
在大多数情况下,原始的 Angular FormControl 被一些代理扩展或包装。 某些方法被其他类型签名覆盖并委托给原始函数。
这引入了一个新层,对性能的影响可以忽略不计,但更重要的是必须维护的代码,并且可能会给您的项目带来错误。
在我看来,这对于编译时的静态检查是不必要的。 唯一的运行时部分是 NgModule,它提供了我的类型化 FormBuilder,它实际上是原始的 Angular FormBuilder。 其他一切都只是 Angular 代码。

在直接比较中,我没有 ValidationModel 和从对象到 FormGroups 和数组到 FormArrays 的转换,但对AbstractControl#get做了一些自以为是的更改以使其更安全,并输入了验证器参数。

通过一些小的添加,我的代码可以向后兼容,我可以创建一个拉取请求。 但是一个类似的拉取请求已经过时了很长时间。
但如果有人努力在 Angular 中实现这一点,我很乐意加入。 另一方面,我很想看到一个新的表单 API,它被更好地设计为严格键入。 详情见我的评论

+1 很想看到这个功能。 有人在努力吗?

撞到有角的团队?

他们不关心开发人员。 他们有自己的待办事项,有二十一点和很棒的 5% 减少捆绑包大小的功能。

@Lonli-Lokli

他们不关心开发人员。

批评很容易。 你更关心其他开发者吗?
还没有看到你的 PR 来改进表单,也没有看到任何建设性的评论或 RFC 来使事情向前发展:man_shrugging:

他们有自己的积压

没办法 :fearful:!
人们正在优先考虑公司_[谁付钱给他们]_ 需要的东西?
真可惜!
image

Awesome-5%-decrease-bundle-size 功能。

您显然在谈论 Ivy 和(当前)包大小的非常小的差异。
Ivy 目前处于试验阶段,您必须选择加入。令人惊讶的是,事情还不完美! :思维:
是的,据说 Ivy 将有助于减少捆绑包的大小并允许工具在应用程序上进行更好的摇树。 希望,那会来! 目前,他们只致力于确保它不会破坏任何东西,并且可以在以后帮助获得更好的调试信息、基于组件而不是基于模块的增量编译以及摇树。 但是用于获得摇树的工具将在以后起作用。

因此,请尽量保持尊重并避免破坏那些免费为您提供开源框架的人。 事情并不完美,大量工作正在进行中,是的,感觉有些问题被抛在了后面,但需要重构,而且永远不会有任何好时机做出这么大的事情,它只是必须在某个时候发生。

现在我退出这场辩论,因为我不想垄断这个讨论非生产性事物的线程。 *飞走了*

@maxime-allex
还有很多其他的 PR(截至目前 386 个),你认为再一个会改变什么吗?
说到这个问题,这个相关的PR(https://github.com/angular/angular/pull/20040)还是没有合并。

说到重构,Ivy 早在一年前就被提到过。 我知道有人可以处理是开发人员的主要功能,但我个人更喜欢看到修复对尽可能多的开发人员很重要。
https://github.com/angular/angular/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

我仍然希望 Angular 是为开发者服务的,而不是为营销服务的,并希望看到更多基于社区反应的问题。 这是我的问题,这里的优先事项是什么。

显然,这个问题可以通过对现有 API 的更新来解决,但是,另外,我已经创建了一个改进 ReactiveFormsModule 的提案,以解决它的许多悬而未决的问题,包括这个问题。

解决的其他一些问题包括订阅任何属性更新的能力以及通过服务(而不是asyncValidator )异步验证控件的能力。 您可以在 #31963 中了解更多信息。 欢迎反馈!

正如我之前Pull Request 。 此 PR 仅包含@ng-stack/forms功能的一部分(不包括:验证、自动检测表单控件和支持输入 [文件])。

嘿,我想分享 Angular 团队的更新:我们听说这是一个很大的痛点。 我们很快就会开始处理更强类型的表单,其中包括查看现有的 PR 并再次审查您的所有评论。 感谢大家抽出时间留下您的想法!

哦!!!!! 退出!

这是非常好的消息,Angular 团队,谢谢!
一旦发布,我将弃用angular-typesafe-reactive-forms-helper

是的!!!!

我太激动了!! 谢谢你,Angular 团队!!

我们可以停止反应垃圾邮件吗?
使用表情符号进行反应,因为它们用于https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/ - 谢谢。

正如 Angular 团队已经确认在强类型响应式表单上工作。 我想分享我的实现原因,即大量使用infer类型来提取值类型以获得流畅的静态类型开发体验。

第一种方法:从值类型开始

当我开始设计 FormGroup 时,我使用了一个直观的简单值类型作为T

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

但是我发现当我必须处理复杂的表格表单架构时,在 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" />

上述实现要求开发人员进行自定义类型转换,这既乏味又容易出错。 恕我直言,这完全失去了使用强类型响应式表单的理由。

第二种方法:从 ControlType 开始

因为使用简单的值类型工作不顺利。 我想出了另一个使用 KeyValueControl 的想法作为T并使用infer从 KeyValueControl 中提取值类型递归地。

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

因此,

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

现场演示

Edit gaplo917/angular-typed-form-codesandbox

现场演示 IDE 屏幕截图

这是复杂形式的真正自动完成体验
Screenshot 2020-06-12 at 19 02 10

Angular Typed Forms

100% 兼容现有的反应式模块
https://github.com/gaplo917/angular-typed-forms

欢迎提出意见和改进。 希望即将到来的强类型响应式表单可以处理复杂的表单模型,如表格和嵌套子表单。

@IgorMinarAngular 核心团队Angular 社区成员

这篇长评论完全集中在票证作者强调的“反对实践”和“强类型”的两个声明上。

我建议我们应该选择基于类的方法而不是基于接口的强类型反应式方法,而不是ng-stacks/forms 中提到的方式。 也不建议更改 Angular Ractive Forms 的代码库,因为我们可以通过多种方式在不更改代码库的情况下实现强类型表单。 让我详细描述一下我在 Interface Driven Approach 和 Class Driven Approach is more Intuitive 中看到的高级挑战是什么,而且我们还得到了 FormGroup 对象是强类型的。 在这两种情况下,我们的 FormGroup 对象都是强类型的,我们不会在基于类的方法中失去 TypeScript 类型的力量。

我的建议

由于我们都熟悉 OOP 实践,因此该类为我们提供了更多的代码灵活性和可维护性。 我在下面强调的几个好处:

  1. 解耦我们的代码。
  2. 与当前方法以及接口驱动方法相比,代码更少。
  3. 我们可以在属性上使用自定义装饰器。
  4. 代码具有可读性、可维护性和可扩展性。
  5. 使用这种方法,我们不需要在模板中编写业务逻辑,就像我们将*ngIf放在多个条件中一样显示错误消息。 我相信模板不是用来编写业务逻辑的。
  6. 更多...

让我将上述接口代码转换为Class并在Class属性上应用验证装饰器。 在这里,我们遵循单一职责原则实践。 看看下面的代码:

image

让我们考虑几个案例,并将其与接口和类驱动的强类型方法进行比较,这有助于我们理解两者的区别。

1. 创建一个表单组
这里我们使用FormBuilder的相同实例和group的相同方法。 但是导入模块名称将与ReactiveTypedFormsModule而不是ReactiveFormsModule 。 让我们创建一个FormGroup
image

根据上面的代码,问题来了,

导入ReactiveTypedFormsModule后当前方法是否有效?
是的,它会起作用,导入ReactiveTypedFormsModule后不会有任何改变。

让我们快速查看其他案例并结束这篇文章。

2.改变FormControl的值
我们可以直接在Class属性上赋值,而不是调用setValue方法。 它将自动设置FormControl值。

image

3.根据FormControl的值变化执行业务逻辑
使用 TypeScript 中setter方法的强大功能,而不是订阅 FormControl ValueChanges

image

4. 转换输入值
我们专注于强类型,但是那些来自输入控件的值呢,比如日期,我们以String格式获取值,但我们期待 TS 代码中的Date格式,为了克服这个问题,我们创建了一个指令或方法来根据需要转换值。 我不会在这里展示当前的代码,因为这有点笨拙,因为我们必须创建指令并做一些废话……😄,所以我想在这里展示类驱动方法代码:

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

4.改变嵌套FormGroup FormControl的值
我们可以直接在各自的属性中赋值,而不是获取嵌套的 FormGroup 对象并调用SetValue方法。

image

5. 在嵌套的 FormArray 中添加 FormGroup
话不多说,看下面的代码😄。

image

在 HTML 模板中引用 FormGroup 对象

简单的代码😄。 HTML 模板中不会有任何更改,但您也会在 HTML 模板中获得更多内容。 请参考以下代码

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

Stackblitz 链接强类型反应式工作示例
Github 上的示例强类型响应式表单

@ajayojha ,如果我错了,请纠正我,但您上面的评论似乎可以简化为:

  1. TypeScript 类型的开销很糟糕。
  2. 在装饰器的帮助下进行运行时验证 - 这很好。
  3. 如果有 setter/getter,为什么需要setValue()valueChanges()

我怎么看:

  1. 编写 TypeScript 类型就像编写静态测试。 有些人可能会在没有测试的情况下创建应用程序,因为他们认为这是不必要的,但这是一种不好的做法。
  2. 在装饰器的帮助下进行运行时验证 - 这可能是一个好主意,同意。
  3. 除了setValue() ,还有patchValue()reset() ,它们也适用于表单值。 仅用 setter 替换setValue()会使代码不一致。 此外,当我们必须为每个表单模型属性编写 setter 时,它会增加更多的代码开销以及在使用 getter 的情况下的性能开销。 在我看来,将表单模型属性名称与表单控件属性混合使用也是一个坏主意。

谢谢, @KostyaTretyak对我的评论的关注,我很乐意回答同样的问题,请相应地在下面查看我的评论:)。

  1. TypeScript 类型的开销很糟糕。

仅供参考,formgroup 对象是强类型的。 接口很好,但并不适合所有领域,我并不是说 TypeScript 类型不好,我认为我在某处没有提到类型不好。 我唯一关心的是接口,因为我们用接口方法否决了软件设计实践,甚至我可以说我们在错误的地方使用了接口方法,我建议的方法是类。 就我对 Class 方法的理解而言,我们并没有损害我们在 Interface 中获得的 TypeScript 类型的好处,甚至我会说我们在可读性、可扩展性和可维护性方面获得的不仅仅是 Interface 方法。

我们是否使用了强类型反应式接口的正确实践?

让我在接口方面多描述一点是强类型反应式表单的不良做法(按照我的说法)。

TypeScript 类型很好,但并不建议我们到处都必须将任何不符合软件实践的东西混为一谈,就像我已经明确提到接口的问题一样。 想想我对界面的突出关注。 让我分享我的案例,在我的一个包含超过 6k+ 组件的企业应用程序中。 如果我使用接口方法,那么开发团队会在进行更改之前问我很好的问题:

  • 我们必须在何处使用哪个接口,因为在具有不同属性的多个组件中使用了相同的实体。 那么我们是否需要明智地创建单独的接口组件? 如果是,则阅读第二个案例。
  • 如果我们采用上述方法,那么我必须在一个或多个组件中使用两个接口属性的方法是什么。 为了解决这个问题,我可以再创建一个接口并扩展它。 为了Strongly Type Reactive Form的震动而创建越来越多的文件是不是很好? 可维护性怎么样?,我没有答案,只能说 Angular 团队正在提供这个解决方案,所以它很好:)(如果 Angular 团队将选择接口方法)。
  • 如果我们采用 a+b 方法,那么在我的一些组件中需要的属性不是全部,然后呢? 我有三个解决方案可以给我的开发团队。

    • 创建一个新界面并在新创建的界面中复制/粘贴所需的属性。 这是软件世界中最肮脏的方法。 当在服务器端更改任何单个属性时,这会产生很多问题,然后很难跟踪更改属性名称的接口数量等区域。

    • 将属性设置为可为空。 如果我定义了可空属性,那么为什么我必须遵循“B”方法?。 我再次没有答案:(给我的开发团队。

    • 不要创建另一个接口,使用“部分”实用程序类型,并使每个属性都是可选的。 通过这样做,我们失去了界面的实际好处。 这也是违反惯例的。 如果我必须遵循这个,那么为什么我必须遵循“A”方法,再次没有答案:)。

    • 如果我让每个/少数属性都可以为空,那么代码可读性如何以及如何判断将值传递给服务器需要多少个属性。 然后我必须检查相应的组件并一瞥。 主要的代码可读性问题。

现在只是从更大的角度考虑上述案例,并与 TypeScript 类型与强类型的响应式表单上的接口进行比较。 我相信每一种好的方法都会节省开发时间,而且在这种方法中,很抱歉,根据软件设计原则和实践,我没有看到任何好处。

  1. 在装饰器的帮助下进行运行时验证 - 这很好。

我同意你对“很好”的评论,我们在界面方法中无法实现的装饰器方法。 我相信这是 TypeScript 最强大的功能,那么为什么我们不能在 Reactive Form Approach 中使用相同的功能并让开发团队完全控制他们的对象属性。

  1. 如果有 setter,为什么需要 setValue()?

我在哪里说过我需要“setValue()”? 我不需要 setValue 并且我没有在示例中显示我在 Class Driven Approach 中调用 setValue 方法。 如果我错了,请纠正我。

  1. 编写 TypeScript 类型就像编写静态测试。 有些人可能会在没有测试的情况下创建应用程序,因为他们认为这是不必要的,但这是一种不好的做法。

我并不是说 TypeScript 类型就像编写静态测试。 但是我不同意响应式形式的基类中的提交更改,我相信我们可以在不触及类定义的情况下实现同样的事情。 在这里,我们可以使用到目前为止我们没有根据提交使用的接口的实际功能,这是一个很好的做法,逻辑运行了这么长时间,我们通过设置默认值来添加泛型类型 '任何'?
我认为我们可以在不接触 Reactive Form 的基类的情况下实现同样的事情。 我不知道为什么我们没有在这方面利用 Interface 而不是更改基类定义并更改规范。

  1. 在装饰器的帮助下进行运行时验证 - 这可能是一个好主意,同意。

很高兴知道我们在这方面是同一页面:)。

  1. 除了 setValue() 之外,还有 patchValue()、reset(),它们也适用于表单值。 仅用 setter 替换 setValue() 会使代码不一致。 此外,当我们必须为每个表单模型属性编写 setter 时,它会增加更多的代码开销以及性能开销。 在我看来,将表单模型属性名称与表单控件属性混合使用也是一个坏主意。

让我分三个部分来描述上面的一点,调用方法、setter 性能开销和混合表单模型属性。

调用方法:正如预期的那样,在写这篇文章时,我想有人可能会建议我使用 'patchValue' 或 'reset' 方法。 我想再说一次,在现实世界的案例中,大多数开发团队正在使用“setValue”方法而不是 patchValue 或其他方法(这是我根据 Angular Application Code Review 和 Stackoverflow Posts setValue vs patchValue 的经验)。 我的重点只是调用分配值的方法,无论我们调用哪种方法。

Setter Performance :我同意 setter 的说法会产生性能开销。 如果是这种情况,那么我会说我们必须首先在 Angular 项目中修复,因为为了绑定反应形式,Angular Framework 使用了 Control Value Accessor 类中的 setter 方法和许多其他指令,这会在不使用类方法。 还有一件事,我们也在多个组件中使用@Input装饰器使用相同的方法,我们必须找到替代方案或 Angular 团队必须提供不同的解决方案(我相信)来克服这种性能问题。 所以,我不认为这是一个主要问题。 现在谈到性能问题,请与现有方法和setter方法方法进行比较(这是可选的,开发团队可以根据需要选择与Angular中的ChangeDetectionStrategy相同。请参阅rxweb文档站点上的示例以进行选择在这种情况下。判断当我们订阅值更改时调用了多少函数,然后在设置值或直接调用 setter 方法之后。我相信这与 valuechanges 相比,在更少的代码执行方面更直观,构建规模小包、代码可读性和许多其他好处。

混合属性:那么您的意见是,您分配的 FormControl 属性名称是否与服务器返回的属性名称不同。 如果是,那么我会说这是代码中的一个主要问题,因为每次我必须在将其发布到服务器之前更改属性名称时,对不起,但我不喜欢在整个应用程序中。 如果我认为您对我的平均包含 40 多个字段的申请表有好意见,那么我必须手动设置每个属性值,只是考虑组件中的代码只是为了分配值和 prod 构建大小的动摇。 这是比课堂方法更好的意见吗?
现在来到建议的解决方案,我们不是将两件事合二为一。 FormControl 属性不同,类属性与各自的数据类型不同。 如果您希望更改属性名称,例如 FormControl 属性名称与 Data 属性不同,那么您可以,请参阅 rxweb 响应式表单包文档。 所以没有问题,因为你的感觉不好(将属性​​名称与表单控件名称混合)在建议的方法中有一个解决方案。

我希望我已经回答了您的所有疑虑,如果您对此有任何其他疑虑,请随时分享:)。

正如我在之前的评论中所说,无需更改 Reactive Form 的基类,因为我们可以通过使用接口隔离原则实践的强大功能来实现相同的事情。 这是带有@rxweb/types包的端到端强类型反应式解决方案。 这适用于接口和类方法:)。

代码实现后的样子?

Stackblitz:开放
Github接口驱动的强类型反应式表单示例

有人有任何建议,请随时分享。

因此, Angular 10 版现已推出,这是一个主要版本,显然反应式表单至少在 Angular 11 版之前不会被强类型化。 所以,我们至少要等到秋天才能实现这个功能。

我有一个问题(或提案?)关于表单模型在我在这里看到的大多数建议/PR 中的构建方式。

在查看大多数试图使 Reactive Forms 类型安全的库和 PR 时,您可以看到它们创建的模型如下所示:

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

然后将其“翻译”为以下内容:

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

因此,简单来说:“如果它是一个对象,则为其构建一个 FormGroup。如果它是一个数组,则构建一个 FormArray。如果它是一个原始值,则创建一个 FormControl。”

但是,有一个问题:您不能再使用 FormControls 中的对象。

到目前为止我看到的解决方案:有些库根本不支持这一点。 并且一些库使用某种“hack”来创建您实际上想要使用 FormControl 而不是 FormGroup 的提示。

我的问题/建议:什么会反对明确定义表单模型如下?

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

这有一个巨大的优势,您现在可以将对象放入 FormControls 中。 它不需要任何类型的“黑客”来做到这一点:)

我为此创建了一个 Codesandbox,以便您可以自己尝试一下: https ://codesandbox.io/s/falling-grass-k4u50?file=/src/app/app.component.ts

@MBuchalik ,是的,这是您开始处理“强类型表单”时想到的第一个明显决定。 我也是从这个开始的,但它有一个明显的缺点——需要创建两个模型:一个用于表单控件,另一个用于表单值。

另一方面,据我所知,这个解决方案将允许我们在不破坏 chage 的情况下实现“强类型形式”,并且我们不必等待下一个主要版本的 Angular 发布。 在这里有必要在实践中使用这种解决方案来评估它是否有比创建两个模型的需要更多的关键缺点。

@MBuchalik我和你分享了同样的观点,并向PR提出了同样的问题,一位有角度的贡献者 ( @KostyaTretyak ) 已经回答了。

你可以看看 PR 中的讨论:
https://github.com/angular/angular/pull/37389#discussion_r438543624

TLDR;

这里有几个问题:
如果我们遵循您的方法,我们需要创建两个不同的模型 - 用于表单控件和表单值。
表单控件的模型更难阅读。

我已经在半年前项目实施了这个想法。 Angular HTML 中控件类型的完整静态自动完成体验确实提高了我们初级开发人员的生产力。 (启用fullTemplateTypeCheck

我在此线程的先前评论中分享了“我为什么要走这条路”:
https://github.com/angular/angular/issues/13721#issuecomment -643214540

Codesanbox 演示: https ://codesandbox.io/s/github/gaplo917/angular-typed-form-codesandbox/tree/master/ =14& hidenavigation = dark

感谢@KostyaTretyak和@gaplo917 的见解! 👍

如果我理解正确,我们可以总结如下。

如果我们只想使用一个模型,那么可以使用@KostyaTretyak提供的解决方案。 然而,缺点是我们现在不能再使用 FormControls 中的对象了。 (我知道有一个“hack”可以允许这样做。但是我们的模型又不是“干净的”;所以我们再次需要 2 个模型。)

如果我们希望能够在 FormControls 中使用对象,那么可能(!)无法使用像我(或@gaplo917)所示的方法。 缺点是你基本上需要2个模型。 或者至少使用一些辅助类型来“提取”表单值模型。

所以,我们现在只需要考虑 FormControls 中的对象是否应该是可能的。 这将简单地回答关于选择两种方法中的哪一种的问题。 或者我错过了什么?

感谢@KostyaTretyak和@gaplo917 的见解! 👍

如果我理解正确,我们可以总结如下。

如果我们只想使用一个模型,那么可以使用@KostyaTretyak提供的解决方案。然而,缺点是我们现在不能再使用 FormControls 中的对象了。 (我知道有一个“hack”可以做到这一点。但是我们的模型又不是“干净的”;所以我们再次需要 2 个模型。)

如果我们希望能够在 FormControls 中使用对象,那么可能(!)无法使用像我(或@gaplo917)所示的方法。缺点是你基本上需要2个模型。或者至少使用一些辅助类型来“提取”表单值模型。

所以,我们现在只需要考虑 FormControls 中的对象是否应该是可能的。这将简单地回答关于选择两种方法中的哪一种的问题。或者我错过了什么?

@MBuchalik在我看来,如果您信任 Typescript 编译器并严重依赖“类型推断”功能,则不需要 2 个模型。我们的内部系统有 60 多种形式,其中一些非常复杂,嵌套有 3 个深度级别FormArray-FormGroup-FormArray而且我们也不需要值类型的显式模型。

只有两种类型的数据模型可以使用:

  • API 数据请求/响应模型
  • 表单控件模型

99.9%的时间,我们

  1. 为每个复杂的表单创建一个封装
  2. 转换远程数据 -> 表单数据
  3. 转换表单数据 -> 远程有效负载

下面的代码片段是说明:

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 这是一个固执的数据流架构,我们可以在 Typescript 中享受类型安全的编程

如果我们只想使用一个模型,那么可以使用@KostyaTretyak提供的解决方案。 然而,缺点是我们现在不能再使用 FormControls 中的对象了。 (我知道有一个“hack”可以允许这样做。但是我们的模型又不是“干净的”;所以我们再次需要 2 个模型。)

这里仍然需要估计我们需要为FormControl使用一个对象的频率。 我认为可以估计在 5-30% 之间。 也就是说,如果我们使用一个模型的解决方案,我们可以覆盖 70-95% 的使用FormControl 。 其余的 - 只需为 TypeScript 提供一个提示作为附加类型(请参阅Control<T> ,将其称为“第二模型”是不正确的):

interface FormModel {
  date: Control<Date>;
}

Control<T>类型可以称为hack吗? - 是的,这可能是一个 hack,但不是一个粗略的 hack。 我不知道这种类型不能按预期工作或有副作用的任何情况。

哦,我记得当我们需要使用外部库进行表单值模型时Control<T>副作用。 在这种情况下,真正需要两种模型:

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.

但在这段代码中,开销仅在这里:

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

感谢@ArielGuetaControl<T>类型的一个关键问题现已为人所知。 也就是说,我什至不会像我之前计划的那样,在未来的 Angular 拉取请求中尝试实现Control<T>

感谢@ArielGuetaControl<T>类型的一个关键问题现已为人所知。 也就是说,我什至不会像我之前计划的那样,在未来的 Angular 拉取请求中尝试实现Control<T>

@KostyaTretyak这不是真的。 关键问题仅表明您的“ControlType”实现不正确。

完整的“控制类型”实现没有问题。

现场演示: https: //codesandbox.io/s/lucid-bassi-ceo6t =/ src/app/demo

Screenshot 2020-07-01 at 00 35 11

也就是说,我什至不会尝试实现 Control在未来的 Angular 拉取请求中,正如我之前计划的那样。

好的,这意味着您的 PR(和连续的 PR)很可能永远不会支持 FormControls 中的对象?

@MBuchalik ,目前(Angular v10),如果我们有以下表单模型:

interface FormModel {
  date: Date;
}

如果在我们的组件中我们想要访问date属性的值,我们需要执行以下操作:

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

我当前的拉取请求提供了表单值的泛型,但没有提供表单控件的类型:

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

@ gaplo917,@MBuchalik,我已经试过您的解决方案,并试图实现自己的类似解决方案,但他们都没有很好地工作。 该解决方案还提供了一组用于递归提取表单模型值的类型。 开销和破坏性更改非常重要,请参阅PR 草案

我强烈怀疑目前这些解决方案是否应该被提议在 Angular 中实现。 也就是说,现在我们只能对表单值使用泛型,而不是表单控件类型。

但它们都不能完美地工作

我只花了几个小时在我的插图上,所以我没想到它是完美的 ;) 你能举出一些效果不佳的例子吗? (特别是在你看来,不容易修复的东西?)

顺便说一句,关于向后兼容性的一个建议:在我看来,使实现完全向后兼容是相对困难的。 因此,我们可能会执行以下操作: 我们根本不更改 FormControl、FormGroup 和 FormArray 类。 取而代之的是,我们创建了从它们继承而来的新对象(可以将它们称为StrictFormControl<T>StrictFormGroup<T>或任何您喜欢的名称)。 这些就是我们使类型安全的那些。 好处:我们 100% 确定没有进行重大更改。 :)

我的插图花了几个小时,所以我没想到它是完美的;)

我使用这个解决方案工作了几天,我看到使用表单有多么困难。

  1. 首先,显着的开销和需要有两个模型。
  2. 其次,这个解决方案在可靠性方面并不比我的Control<T>类型的解决方案好,因为同样需要递归提取表单模型的值。
  3. 使用嵌套的表单控件。 如果我们有以下表单模型:
interface FormModel {
  one: FormGroup<{two: FormControl<string>}>;
}

如果我们得到formGroup.controls.one.value ,TypeScript 会提供带有条件类型的提示,而不是{two: string}类型(应该是)。 因此很难从 IDE 读取值。

如果我们得到 formGroup.controls.one.value,TypeScript 会提供带有条件类型的提示,而不是 {two: string} 类型(应该是这样)。 因此很难从 IDE 读取值。

好的,只是为了确保我正确理解一切。 如果您使用我的实现并编写以下内容:

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

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

(让它变得更冗长;))

如果我现在寻找myForm.controls.one.value那么它看起来像这样:

grafik

所以你说,在这个例子中,“两个”不应该是可选的? 我想这不是键入表单值的正确方法。 表单值仅包括未禁用的字段。 所以,在我看来,它应该是一个递归的 Partial。 您无法在编译时知道哪些字段将被禁用,哪些不会。

所以你说,在这个例子中,“两个”不应该是可选的?

什么? 不。

我对您的解决方案的测试:

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

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

将鼠标悬停在value上后,我在代码和框上看到的内容:

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

这里PartialFormGroupValue指的是条件类型PartialFormValue

啊,好吧,我想我明白了。 所以你的意思是这种类型很难阅读,对吧? 我最初以为你在谈论一个错误或类似的东西。

好吧,一旦您继续输入,大多数 IDE 仍然会显示可用属性的建议。 所以我真的没有看到这里有什么大问题。 (当然,如果它只是说{two?: string}会更好阅读。但我认为这不是非常重要。这至少是我的观点。)

如果您实现了Control<T> ,那么您将如何将其从表单值的输入中删除,而不像我那样做某事? 以及如何在不使用辅助类型的情况下使表单值成为递归部分?

如果你实施了你的控制,然后你将如何从表单值的输入中删除它而不像我那样做某事? 以及如何在不使用辅助类型的情况下使表单值成为递归部分?

在这种情况下,我的解决方案也不是更好:

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

我给出这个例子是因为你要求它:

你能举出一些效果不佳的例子吗? (特别是在你看来,不容易修复的东西?)

顺便说一下,我用Control<T>解决了关键问题

为了解决 Angular 10 和[formControl]的 HTML 绑定问题,这是我走的路线:

如另一个问题 (https://github.com/angular/angular/issues/36405#issuecomment-655110082) 所述,对于我的表单,我通常创建扩展FormGroup以简化重用性和测试. 有了这种结构,我现在可以通过更新我的代码来解决这个问题:

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

对此:

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

此时form.controls将正确地将其类型显示为{ id: FormControl, name: FormControl } ,在 HTML 中正确绑定,并且如果表单因嵌套表单组或数组而更加复杂,则将正确聚合。

使用formDefinition函数并不漂亮,但它是我能想出的最干净的解决方案,以防止表单定义和构造函数之间的重复。

我相信您可以更新FormGroup以获得上述泛型类型定义而不引入破坏性更改(好吧,对于动态添加/删除控件的表单我猜可能不是这样;它们不会显示在controls类型)

编辑
如果不需要创建扩展 FormGroup 的类,看起来就更简单了; 您可以创建一个帮助函数来解决一般问题:

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

编辑 2
...或者你可以把它烘焙到FormGroup类本身( FormBuilder也许?)

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

编辑 3
我已经扩展了上面的例子,包括在value上打字,并创建了一篇文章来总结所有内容:

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

这现在标记在未来发展的路线图上: https ://angular.io/guide/roadmap#better-developer-ergonomics-with-strict-typing-for-angularforms

@pauldraper你能解释一下与大约 2 个月前的路线图相比发生了什么变化吗? 我看到的唯一变化是标题的措辞。 但它仍然在“未来”部分。 就像2个月前一样。

@MBuchalik也许它已经存在 2 个月了。

此页面是否有帮助?
0 / 5 - 0 等级