Angular: Реактивные формы не являются строго типизированными

Созданный на 30 дек. 2016  ·  90Комментарии  ·  Источник: angular/angular

[x] feature request
  • Угловая версия: 2

Реактивные формы предназначены для использования в сложных формах, но элементы управления valueChanges равны Observable<any> , что полностью противоречит передовой практике для сложного кода.

Должен быть способ создания строго типизированных элементов управления формой.

forms feature high

Самый полезный комментарий

Привет, я хотел поделиться обновлением от команды Angular: мы слышим, что это большая проблема. Вскоре мы начнем работу над более строго типизированными формами, что будет включать просмотр существующих PR и повторный просмотр всех ваших комментариев. Спасибо всем, что нашли время оставить свои мысли!

Все 90 Комментарий

связанные # 11279

Это не имеет отношения к # 11279.

Объясните, пожалуйста, как это не связано?
Что вы хотите, чтобы абстрактный элемент управления был универсальным, верно? Это единственный способ, которым valueChanges может иметь тип, отличный от Observable<any> Другого способа определить тип не было бы.
Это именно то, что спрашивает # 5404, что означает, что это связано с # 11279
Если есть другой способ реализовать это, не делая AbstractControl универсальным, пожалуйста, объясните это.

Использование get<Type> как в # 11279 - однозначно неправильное решение. Если бы в 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-feb February-2017) запланированы общие типы по умолчанию (https://github.com/Microsoft/TypeScript/ issues / 2175), как только мы получим это, я думаю, было бы неплохо вернуться к этому вопросу, поскольку мы могли бы сделать AbstractControl общим, например AbstractControl<T = any> где T - это тип значение, возвращаемое valueChanges которое будет Observable<T> . Было бы не очень хорошей идеей делать это в настоящее время, так как это будет серьезное критическое изменение, но с универсальными шаблонами по умолчанию, если я не пойму их неправильно, это не будет критическим изменением.

Небольшое обновление по этому поводу , похоже, стандартные универсальные TS2.3 . Таким образом, с поддержкой TS 2.1 со стороны Angular с версией 4.0 вскоре они смогут поддерживать TS 2.3, а сейчас нам следует подождать, чтобы вернуться к этому.

TypeScript 2.3 с универсальными типами по умолчанию уже здесь, есть ли у нас какие-либо планы, когда будет готова поддержка TS 2.3 в angular?

@desfero ждет # 16707 для обновления сборки до TS2.3

+1 хотел бы увидеть эту функцию. Кто-нибудь над этим работает?

16828

В настоящее время заблокирован на https://github.com/Microsoft/TypeScript/issues/16229

Это может быть полезно -

Небольшое обновление по этому поводу:
Согласно моему комментарию здесь: https://github.com/angular/angular/pull/16828#issuecomment -337034655
внедрение дженериков в текущий API форм невозможно без критических изменений.
Так что либо необходимы критические изменения
Или перепишите полные формы

Итак, мой предыдущий комментарий оказался неверным.
Мне удалось реализовать это в текущем 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) без использования строки

При этом отсутствует возможность обхода нескольких групп форм.
Хотя мой метод не может вывести типы в этом сценарии, идея моего 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>

Все они были бы точными ссылками. Решение по модернизации делает это несколько хитроумно, но решает и эту проблему.

@Toxicable спасибо за пиар. С нетерпением жду использования :)

Я согласен с @howiempt , если мы сможем получить что-то вроде этого, это будет первый приз:

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

Опять же, я действительно не знаю, насколько это возможно в более широком масштабе.
Я доверяю твоему мнению.

Продолжайте в том же духе и спасибо за быстрый ответ.

Я думаю, что этот метод доступа к другим элементам управления не имеет отношения к добавлению дженериков.
Не стесняйтесь открыть другой вопрос об этом, однако

Я действительно не думаю, что набор возвращаемых типов действительно «строго типизирован», кажется, половина необходимой реализации, но это шаг в правильном направлении.

Привет, я выпустил https://github.com/Quramy/ngx-typed-forms для решения этой проблемы. Пожалуйста, проверьте это 😄

@Quramy Я пытался использовать ваш пакет несколько недель назад, и, насколько я помню, он не очень-то принудительный :(

+1. Не могу подсчитать количество случаев, когда я хотел, чтобы это было реализовано.

Такой же.
Реактивные формы Angular - это одна из особенностей, которая действительно превосходит любые другие фреймворки. Создание строго типизированных реактивных форм выведет их на новый уровень, еще больше увеличивая разрыв между конкурентами :)

это можно сделать с помощью типов с условным отображением и рекурсии ... типы с условным отображением были просто объединены в машинописный текст. Если это будет опубликовано, у нас есть шанс сделать сильные типизированные формы возможными.

Не могу дождаться (

К сожалению, решение FormBuilder . Также общие FormGroup<T> , FormControll<T> и FormArray<T> нельзя использовать напрямую, потому что это только интерфейсы, которые не расширяют AbtractControl<T> . Для нашего текущего проекта этого было недостаточно.

С ngx-строго-типизированными формами я сам выпустил проект
Он немного нарушает обратную совместимость, потому что не использует универсальные шаблоны по умолчанию. Таким образом, это вынуждает вас явно указать тип элемента управления любой, но добавляет гораздо больше безопасности типов для всех других частей и является API-совместимым с текущей реализацией @ angular / forms.
Возможно, это действительная альтернатива, пока эта функция не будет реализована в Angular.

+1 Это мощная функция ..

Следует реализовать как можно скорее)

Как это должно быть закодировано

В этом программном обеспечении с открытым исходным кодом (AGPL) есть интересная реализация.

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

Я использовал это как временное решение:
Надеюсь, поможет

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, где «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://github.com/concentricsky/badgr-ui/blob/develop/src/app/common/util/ typed-forms.ts

Динамически создаваемый тип гарантирует, что форма формы соответствует вашим ожиданиям, вместо того, чтобы заранее объявлять тип интерфейса, а затем надеяться, что вы сделаете правильные вызовы для создания формы.

Мне бы хотелось когда-нибудь увидеть что-то подобное в 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.
Я думаю, что это может быть возможным решением / обходным решением вашей проблемы 😉

//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 неверны ...

В этой ветке твиттера я обсуждаю с @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 над двумя вещами:

  • Улучшение типов форм
  • Улучшение способа управления подформами (внутри подкомпонентов)

Мы не охватываем все типы, как это сделал

Если кто-то хочет взглянуть на lib: ngx-sub-form

Привет @ maxime1992. Я посмотрел на Controls<T> но он не ограничивает AbstractControl to <T[K]>
Могу я спросить почему? Вы оставляете его «нетипизированным», потому что чувствуете необходимость изменить тип во время выполнения, используя что-то вроде методов setControl или registerControl для переопределения и изменения FormControl и, возможно, связанного типа?
Поскольку мой TypedForms.d.ts немного строже и «заставляет» AbstractControlTyped to the type T<P> но я не знаю, принудительно ли при таком выборе принудительно / деактивируется то, что в исходном API ReactiveForms разрешено и, возможно, используется кто то...

Есть мысли? Есть ли какой-нибудь реальный случай для рассмотрения?
Любые комментарии по этому поводу могут помочь мне решить, как изменить определения, которые я создал, и PR, над которым я работаю ...
Спасибо

PS: Отличная работа над ngx-sub-формой 👍 идея использовать ControlValueAccessor для обработки подчиненных форм - это то, над чем я тоже экспериментировал 😉

Для @ ng-stack / forms добавлена ​​поддержка типизированной проверки.

Классы FormControl , FormGroup , FormArray и все методы FormBuilder
принять «модель проверки ошибок» в качестве второго параметра для универсального:

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?

Если вы можете опубликовать образец кода или stackblitz, я посмотрю на него и постараюсь помочь решить проблему, если найду решение 😉

Для @ng-stack/forms добавлена поддержка input[type="file"] .

См. Также пример на stackblitz

Я также использовал "классический" подход с использованием FormData для загрузки файлов, но есть несколько преимуществ:

  • автоматически получить имя ввода формы, например, <input type="file" name="someName"> -> formControl.value с именем поля someName ;
  • поддержка атрибута multiple с правильной настройкой имени поля (обратите внимание на userpic[] здесь)
<input type="file" multiple name="userpic" [formControl]="formControl">

`` тс
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 Reactive Forms. Вы когда-нибудь думали о том, чтобы внести свой вклад в сам Angular, вместо того, чтобы делать свою собственную реализацию?

Мы почти находимся в 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-строго-типизированных форм .
Я сделал быстрый обзор набора функций @ng-stack/forms и думаю, что есть некоторые небольшие отличия.

Существует концептуальная разница между почти всеми решениями или обходными путями и моим проектом.
В большинстве случаев исходный Angular FormControl расширяется или обертывается каким-либо прокси. Некоторые методы переопределяются сигнатурами других типов и делегируются исходной функции.
Это вводит новый уровень с незначительным влиянием на производительность, но более важным является код, который необходимо поддерживать и который может вносить ошибки в ваш проект.
На мой взгляд, это не нужно для статических проверок во время компиляции. Единственная часть времени выполнения - это NgModule, который предоставляет мой типизированный FormBuilder, который фактически является исходным Angular FormBuilder. Все остальное - это просто код на Angular.

При прямом сравнении у меня нет ValidationModel и преобразования из объектов в FormGroups и Arrays в FormArrays, но я внес некоторые упрямые изменения в AbstractControl#get чтобы сделать его более безопасным по типу, и набрал аргументы валидатора.

С некоторыми небольшими дополнениями мой код может быть обратно совместим, и я могу создать запрос на перенос. Но подобный запрос на вытягивание уже давно устарел.
Но если будут попытки реализовать это в Angular, я буду рад объединить усилия. С другой стороны, мне бы хотелось увидеть новый API для форм, который лучше спроектирован для строгой типизации. См. Подробности в моем комментарии .

+1 хотел бы увидеть эту функцию. Кто-нибудь над этим работает?

натыкайся на угловую команду?

Им нет дела до разработчиков. У них есть собственный бэклог с блэкджеком и потрясающими функциями уменьшения размера пакета на 5%.

@ Lonli-Lokli

Им нет дела до разработчиков.

Критиковать легко. Тебя больше волнуют другие разработчики?
Не видел от вас ни PR для улучшения форм, ни каких-либо конструктивных комментариев или RFC для продвижения вперед: man_shrugging:

У них есть свой бэклог

Ни в коем случае: испуганный :!
Люди уделяют первоочередное внимание вещам, которые нужны компании _ [кто им платит] _?
Какая жалость!
image

awesome-5% -decrease-bundle-size функций.

Вы явно говорите об Айви и (в настоящее время) очень небольшой разнице в размере пакета.
В настоящее время 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 для извлечения типа значения, чтобы получить плавный опыт разработки со статическим типом.

1-й подход: начать с типа значения

Когда я начал разрабатывать 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" />

Вышеупомянутая реализация требовала от разработчиков выполнять кастомное приведение типов, что утомительно и чревато ошибками. IMHO, это полностью лишает смысла использовать строго типизированную Reactive Form.

2-й подход: начат с 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

Скриншоты Live Demo IDE

Это настоящий опыт автозаполнения сложных форм.
Screenshot 2020-06-12 at 19 02 10

Угловые типизированные формы

100% совместимость с существующими модулями реактивной формы
https://github.com/gaplo917/angular-typed-forms

Комментарии и улучшения приветствуются. Надеемся, что грядущие сильно типизированные реактивные формы смогут обрабатывать сложные модели форм, такие как таблицы и вложенные подформы.

@IgorMinar , команда разработчиков Angular и

Этот длинный комментарий полностью сфокусирован на двух утверждениях, которые выделены автором заявки « против практики » и « строго типизированы ».

Я бы посоветовал вместо подхода на основе интерфейса для сильно типизированной реактивной формы выбрать подход на основе классов, но не так, как это упоминается в ng-stacks / forms . Также не рекомендуется изменять кодовую базу Angular Ractive Forms, потому что мы можем получить строго типизированную форму без изменения кодовой базы многими способами. Позвольте мне подробно описать, какие высокоуровневые проблемы я вижу в подходе, основанном на интерфейсе, и подход, основанный на классах, более интуитивно понятен, чем другие, а также мы получаем объект FormGroup со строгой типизацией. В обоих случаях наш объект FormGroup является строго типизированным, мы не теряем возможности TypeScript Type в подходе на основе классов.

Мое предложение

Поскольку все мы знакомы с практиками ООП, этот класс дает нам большую гибкость и удобство сопровождения кода. Некоторые из преимуществ, которые я выделяю ниже:

  1. Разделите наш код.
  2. Меньше кода по сравнению с текущим подходом, а также подход, основанный на интерфейсе.
  3. Мы можем использовать пользовательские декораторы для собственности.
  4. Код удобочитаемый, поддерживаемый и расширяемый.
  5. При таком подходе нам может не понадобиться писать бизнес-логику в шаблоне, как мы помещаем *ngIf с несколькими условиями для отображения сообщений об ошибках. Я считаю, что шаблоны не предназначены для написания бизнес-логики.
  6. Намного больше...

Позвольте мне преобразовать вышеупомянутый код интерфейса в Class и применить декоратор проверки к свойствам Class . Здесь мы следуем принципу единой ответственности . Взгляните на приведенный ниже код:

image

Давайте рассмотрим несколько случаев и сравним их с строго типизированным подходом, основанным на интерфейсе и классах, который помогает нам понять разницу в обоих из них.

1. Создайте FormGroup
Здесь мы используем тот же экземпляр FormBuilder и тот же метод group . Но имя модуля импорта будет другим, например ReactiveTypedFormsModule вместо ReactiveFormsModule . Создадим FormGroup :
image

В соответствии с приведенным выше кодом возникает вопрос:

Будет ли текущий подход работать после импорта ReactiveTypedFormsModule ?
Да, заработает, после импорта файла ReactiveTypedFormsModule ничего не изменится.

Давайте быстро рассмотрим другие случаи и завершим этот пост.

2. Измените значение FormControl.
Вместо вызова метода setValue мы можем напрямую присвоить значение свойству Class . Он автоматически установит значение FormControl .

image

3. Выполните бизнес-логику на основе изменений значений FormControl.
Вместо подписки на FormControl ValueChanges используйте возможности метода setter в TypeScript.

image

4. Преобразуйте входное значение.
Мы фокусируемся на сильной типизации, но как насчет тех значений, которые поступают из элемента управления вводом, например, для даты, мы получаем значение в формате String , но мы ожидаем Date Format в коде TS, чтобы решить эту проблему, мы создаем директиву или метод для преобразования значения по мере необходимости. Я не буду показывать здесь текущий код, так как это немного неуклюже, потому что мы должны создать директиву и делать такие вещи, как бла-бла ... 😄, Итак, я хотел бы показать здесь код подхода, основанного на классах:

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

4. Измените значение вложенной FormGroup FormControl.
Мы можем напрямую присвоить значение в соответствующем свойстве, а не получать вложенный объект FormGroup и вызывать метод SetValue .

image

5. Добавление FormGroup во вложенный FormArray
Больше нечего сказать, взгляните на приведенный ниже код 😄.

image

Ссылка на объект FormGroup в шаблоне HTML

Простой код 😄. В шаблоне 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 Link : рабочий пример строго типизированной реактивной формы
Пример на Github : строго типизированная реактивная форма

@ajayojha , поправьте меня, если я ошибаюсь, но похоже, что ваш комментарий выше можно свести к следующему:

  1. Накладные расходы, связанные с типами TypeScript, плохи.
  2. Проверка времени выполнения с помощью декораторов - это хорошо.
  3. Зачем нужны setValue() и valueChanges() если есть сеттеры / геттеры?

Что я думаю:

  1. Написание типов TypeScript похоже на написание статических тестов. Кто-то может создавать приложения без тестов, потому что считает это ненужным, но это плохая практика.
  2. Валидация во время выполнения с помощью декораторов - неплохая идея, согласитесь.
  3. Помимо setValue() есть также patchValue() , reset() , которые также работают со значением формы. Замена только setValue() сеттером сделает код несовместимым. Вдобавок, когда нам нужно написать сеттеры для каждого свойства модели формы, это добавит гораздо больше накладных расходов на код, а также накладных расходов на производительность в случае с геттерами. На мой взгляд, сочетание имен свойств модели формы со свойствами элемента управления формой также является плохой идеей.

Спасибо, @KostyaTretyak, за то, что вы обеспокоены моим комментарием, я был бы рад ответить так же, пожалуйста, смотрите ниже мои комментарии соответственно :).

  1. Накладные расходы, связанные с типами TypeScript, плохи.

Для вашей информации объект formgroup строго типизирован. Интерфейсы хороши, но подходят не для каждой области, и я не говорю, что типы TypeScript плохие, я не думаю, что где-то я упоминал, что типы плохие. Меня беспокоит только интерфейс, потому что мы отвергаем методы проектирования программного обеспечения с помощью подхода интерфейса, или даже я могу сказать, что мы используем подход интерфейса не в том месте, и мой предлагаемый подход - это класс. Насколько я понимаю через подход классов, мы не ставим под угрозу преимущества типов TypeScript, которые мы получаем в интерфейсе, или даже я бы сказал, что мы получаем больше, чем интерфейсный подход с точки зрения читаемости, масштабируемости и ремонтопригодности.

Правильно ли мы используем интерфейс для строго типизированной реактивной формы?

Позвольте мне описать немного больше с точки зрения интерфейса - плохая практика (по моему мнению) для строго типизированной реактивной формы.

Типы TypeScript - это хорошо, но не предполагается, что везде нам приходится перемешивать что-либо, что не соответствует практике программного обеспечения. Как я ясно упомянул о проблемах с интерфейсом. Просто подумайте о моих проблемах, связанных с интерфейсом. Позвольте мне поделиться своим случаем в одном из моих корпоративных приложений, которое содержит более 6k + компонентов. Если я буду использовать интерфейсный подход, то перед внесением изменений команда разработчиков задаст мне хорошие вопросы:

  • Какой интерфейс мы должны использовать где, поскольку одна и та же сущность используется в нескольких компонентах с разными свойствами. Тогда нужно ли нам создавать отдельный компонент интерфейса? Если да, то прочтите второй случай.
  • Если мы пойдем с вышеприведенным подходом, то каков будет подход, при котором мне придется использовать оба свойства интерфейса в одном или нескольких компонентах. Для решения этой проблемы я могу создать еще один интерфейс и расширить его. Хорошо ли создавать все больше и больше файлов только для встряхивания Strongly Type Reactive Form? Что насчет ремонтопригодности? У меня нет ответа, кроме как сказать, что команда Angular предоставляет это решение, так что оно хорошее :) (если команда Angular выберет интерфейсный подход).
  • Если мы пойдем с подходом a + b, тогда в некоторых из моих компонентов требуется несколько свойств, а не все, тогда? У меня есть три решения, которые я могу дать моей команде разработчиков.

    • Создайте новый интерфейс и скопируйте / вставьте необходимые свойства во вновь созданный интерфейс. Это самый грязный подход в мире программного обеспечения. Это создает множество проблем, когда любое отдельное свойство будет изменено на стороне сервера, тогда трудно отслеживать области одинаково в том, сколько интерфейсов нужно изменить имя свойства.

    • Установите свойство, допускающее значение NULL. Если я определяю свойство, допускающее значение NULL, то почему я должен следовать подходу «B» ?. Опять же, у меня нет ответа :( чтобы передать моей команде разработчиков.

    • Не создавайте другой интерфейс, используйте тип служебной программы «Частичный» и делайте каждое свойство необязательным. Делая это, мы теряем реальную пользу интерфейса. Это тоже против практики. Если я должен следовать этому, то почему я должен следовать подходу «А», снова Нет ответа :).

    • Если я делаю все / несколько свойств обнуляемыми, то как насчет читабельности кода и как я могу судить, сколько свойств требуется для передачи значения на сервер. Затем я должен проверить соответствующий компонент и получить представление о нем. Основная проблема читабельности кода.

Теперь просто подумайте над вышеупомянутыми случаями в более широкой перспективе и сравните с типами TypeScript с интерфейсом на реактивных формах для строго типизированных. Я считаю, что каждый хороший подход сэкономит время разработки, и в этом подходе, к сожалению, я не вижу никаких преимуществ в соответствии с Принципами и практиками проектирования программного обеспечения.

  1. Проверка времени выполнения с помощью декораторов - это хорошо.

Я согласен с вашим комментарием о " Это хорошо ", подход декоратора, которого мы не можем достичь в интерфейсном подходе. Я считаю, что это самая мощная функция TypeScript, почему мы не можем использовать то же самое в подходе реактивных форм и предоставить команде разработчиков полный контроль над свойствами их объектов.

  1. Зачем вам setValue (), если есть сеттеры?

Где я сказал, что мне нужен setValue ()? Мне не нужен setValue, и я не показал в примере, где вызываю метод setValue в подходе, основанном на классах. Пожалуйста, поправьте меня, если я ошибаюсь.

  1. Написание типов TypeScript похоже на написание статических тестов. Кто-то может создавать приложения без тестов, потому что считает это ненужным, но это плохая практика.

Я не говорю, что типы TypeScript похожи на написание статических тестов. Но я не согласен с изменениями фиксации в базовых классах реактивной формы, я считаю, что мы можем добиться того же, не касаясь определения класса. Здесь мы можем использовать фактическую мощность интерфейса, которую мы пока не используем в соответствии с коммитами. Это хорошая практика, когда логика работает так долго, и мы добавляем общие типы, устанавливая значение по умолчанию ' любой'?
Я думаю, что мы можем добиться того же, не касаясь базовых классов Reactive Form. Я не знаю, почему мы не используем интерфейс в этом вместо того, чтобы изменить определение базового класса, а также изменить спецификацию.

  1. Валидация во время выполнения с помощью декораторов - неплохая идея, согласитесь.

Приятно знать, что мы оба - одна и та же страница :).

  1. Помимо setValue () есть также patchValue (), reset (), которые также работают со значением формы. Замена только setValue () установщиком сделает код несогласованным. Кроме того, когда нам нужно написать сеттеры для каждого свойства модели формы, это добавит гораздо больше накладных расходов на код, а также накладных расходов на производительность. На мой взгляд, сочетание имен свойств модели формы со свойствами элемента управления формой также является плохой идеей.

Позвольте мне описать вышеупомянутый момент в трех разделах: метод вызова, накладные расходы на сеттер и свойства модели смешанной формы.

Метод вызова: Как и ожидалось, когда я писал этот пост, я подумал, что кто-то может предложить мне использовать метод patchValue или метод сброса. Опять же, я хотел бы сказать, что в реальном мире в основном команда разработчиков использует метод setValue вместо patchValue или других методов (это мой опыт в соответствии с обзором кода приложения Angular и сообщениями Stackoverflow setValue vs patchValue). Моя цель - просто вызвать метод для присвоения значения, независимо от того, какой метод мы вызываем.

Производительность сеттера : я согласен с утверждением, что сеттеры создают накладные расходы на производительность. Если это так, то я бы сказал, что мы должны сначала исправить в Angular Project, потому что для привязки реактивной формы Angular Framework использует метод установки в классе Control Value Accessor и многие другие директивы, и это создает накладные расходы на производительность без использования Классовый подход. Еще одна вещь - тот же подход, который мы также используем в нескольких компонентах с декоратором @Input , мы должны найти альтернативную команду или команду Angular, которая должна предоставить другое решение (я считаю) для преодоления такого рода проблем с производительностью. Так что я не думаю, что это серьезная проблема. Теперь перейдем к проблеме производительности: сравните с существующим подходом и подходом метода установки (это необязательно, группа разработчиков может выбрать, если пожелает, то же самое, что и ChangeDetectionStrategy в Angular. Пожалуйста, обратитесь к примеру на сайте документации rxweb, чтобы выбрать В этом случае. Судите, сколько функций вызывают, когда мы подписываем изменения значения, а затем после установки значения или прямого вызова метода установки. Я считаю, что это намного более интуитивно понятно с точки зрения меньшего выполнения кода по сравнению с изменениями значений, низкий размер сборки пакет, читабельность кода и многое другое.

Смешивание свойств : Итак, каково ваше мнение, назначаете ли вы имя свойства FormControl, отличное от имени свойства, возвращенного сервером. Если да, то я бы сказал, что это серьезная проблема в коде, потому что каждый раз, когда мне приходится менять имя свойства перед его отправкой на сервер, извините, но я бы не предпочел использовать его во всем приложении. Если я считаю ваше хорошее мнение о моей форме приложения, которая содержит в среднем 40+ полей, тогда мне нужно установить каждое значение свойства вручную, просто подумайте о коде в компоненте только для встряски назначения значения и размера сборки продукта. Неужели это мнение лучше, чем классовый подход?
Теперь перейдем к предлагаемому решению, мы не смешиваем две вещи в одном. Свойства FormControl различны, а свойства класса отличаются от соответствующего типа данных. Если вы хотите изменить имя свойства, например, имя свойства FormControl отличается от свойства Data, вы можете, пожалуйста, обратитесь к документации пакета реактивной формы rxweb. Так что нет проблем, поскольку ваше плохое самочувствие (смешивание имени свойства с именами элементов управления формой) имеет решение в предлагаемом подходе.

Надеюсь, я ответил на все ваши вопросы. Не стесняйтесь поделиться, если у вас есть какие-либо вопросы по этому поводу :).

Как я уже сказал в своем предыдущем комментарии, нет необходимости изменять базовые классы реактивной формы, потому что мы можем добиться того же, используя мощь Практик Принципов Разделения Интерфейсов. Вот решение для сквозной строго типизированной реактивной формы с пакетом @ rxweb / types . Это хорошо работает как с интерфейсом, так и с классовым подходом :).

Как выглядит код после реализации?

Stackblitz: Открытый
Github : пример сильно типизированной реактивной формы, управляемой интерфейсом

У кого-то есть предложения, не стесняйтесь поделиться тем же.

Итак, теперь доступна версия 10 Angular , это основной выпуск, и очевидно, что реактивные формы не будут строго типизированы, по крайней мере, до версии 11 Angular. Итак, нам придется подождать как минимум до осени, чтобы реализовать эту функцию.

У меня есть вопрос (или предложение?) Относительно того, как построена модель формы в большинстве предложений / PR, которые я видел здесь.

При просмотре большинства библиотек и PR, которые пытаются сделать тип Reactive Forms безопасным, можно увидеть, что они создают модель, которая выглядит следующим образом:

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.

Решения, которые я видел до сих пор: некоторые библиотеки просто не поддерживают это. А некоторые библиотеки используют своего рода «взлом», чтобы создать подсказку, что вы действительно хотите использовать 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 , да, это первое очевидное решение, которое приходит в голову, когда вы начинаете работать над «строгими типизированными формами». Я тоже начал с этого, но у него есть существенный недостаток - необходимость создавать две модели: одну для элементов управления формой, другую - для значений формы.

С другой стороны, насколько я понимаю, это решение позволит нам реализовать «строго типизированные формы», не нарушая при этом изменений, и нам не придется ждать выпуска следующей основной версии Angular. Здесь необходимо на практике поработать с таким решением, чтобы оценить, есть ли у него более серьезные недостатки, чем необходимость создания двух моделей.

@MBuchalik Я разделял с вами то же мнение и задал тот же вопрос PR, и один из участников angular ( @KostyaTretyak ) ответил.

Вы можете посмотреть обсуждение в PR:
https://github.com/angular/angular/pull/37389#discussion_r438543624

TL; DR;

Вот пара проблем:
Если мы будем следовать вашему подходу, нам нужно создать две разные модели - для элементов управления формой и для значений формы.
Модель для элементов управления формы труднее читать.

И я реализовал эту идею для наших проектов полгода назад, которые уже используются в производстве. ПОЛНОЕ статическое автозаполнение для типа элементов управления в 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/?fontsize=14&hidenavigation=1&theme=dark

Спасибо за понимание @KostyaTretyak и @ gaplo917! 👍

Если я правильно понял, можно резюмировать это следующим образом.

Если мы хотим использовать только одну единственную модель, можно использовать решение, подобное тому, которое предоставлено @KostyaTretyak . Однако оборотной стороной является то, что теперь мы больше не можем использовать объекты в FormControls. (Я знаю, что есть «хитрость», которая позволит это сделать. Но тогда наша модель снова не «чистая»; так что нам снова понадобятся 2 модели.)

Если мы хотим иметь возможность использовать объекты в FormControls, то, вероятно (!), Нет никакого способа обойтись без подхода, подобного тому, который я проиллюстрировал (или @ gaplo917). Обратной стороной является то, что вам в основном нужно 2 модели. Или, по крайней мере, используйте несколько вспомогательных типов для «извлечения» модели значения формы.

Итак, нам теперь просто нужно подумать о том, должны ли быть объекты в FormControls или нет. Это просто ответит на вопрос, какой из двух подходов выбрать. Или я что-то упускаю?

Спасибо за понимание @KostyaTretyak и @ gaplo917! 👍

Если я правильно понял, можно резюмировать это следующим образом.

Если мы хотим использовать только одну единственную модель, можно использовать решение, подобное тому, которое предоставлено @KostyaTretyak . Однако оборотной стороной является то, что теперь мы больше не можем использовать объекты в FormControls. (Я знаю, что есть «хитрость», которая позволит это сделать. Но тогда наша модель снова не «чистая»; так что нам снова понадобятся 2 модели.)

Если мы хотим иметь возможность использовать объекты в FormControls, то, вероятно (!), Нет никакого способа обойтись без подхода, подобного тому, который я проиллюстрировал (или @ gaplo917). Обратной стороной является то, что вам в основном нужно 2 модели. Или, по крайней мере, используйте несколько вспомогательных типов для «извлечения» модели значения формы.

Итак, нам теперь просто нужно подумать о том, должны ли быть объекты в FormControls или нет. Это просто ответит на вопрос, какой из двух подходов выбрать. Или я что-то упускаю?

@MBuchalik На мой взгляд, если вы доверяете компилятору Typescript и сильно полагаетесь на функцию «вывода типов», вам не нужно иметь 2 модели. Наша внутренняя система имеет более 60 форм, некоторые из них очень сложные, вложенные с 3 уровнями глубины FormArray-FormGroup-FormArray и нам также не нужна явная модель для типа значения.

Есть только 2 типа моделей данных, с которыми можно поиграть:

  • Модель запроса / ответа данных API
  • Модель FormControl

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. (Я знаю, что есть «хитрость», которая позволит это сделать. Но тогда наша модель снова не «чистая»; так что нам снова понадобятся 2 модели.)

Здесь еще необходимо оценить, как часто нам нужно использовать объект для FormControl . Я думаю, что это можно оценить где-то в 5-30%. То есть, если использовать решения с одной моделью, мы можем покрыть 70-95% случаев использования FormControl . В остальном - просто дайте подсказку для TypeScript как дополнительного типа (см. Control<T> , называть это «второй моделью» некорректно):

interface FormModel {
  date: Control<Date>;
}

Можно ли назвать тип Control<T> хаком? - Да, наверное, это хакерство, но не грубое хакерство. Я не знаю случаев, когда этот тип не работал должным образом или имел побочные эффекты.

О, я вспомнил о побочных эффектах с 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>;
}

Благодаря @ArielGueta стала известна критическая проблема с типом Control<T> . То есть я даже не буду пытаться реализовать Control<T> в будущих Pull Requests для Angular, как я планировал ранее.

Благодаря @ArielGueta стала известна критическая проблема с типом Control<T> . То есть я даже не буду пытаться реализовать Control<T> в будущих Pull Requests для Angular, как я планировал ранее.

@KostyaTretyak Неправда. Критическая проблема показывает только то, что ваша реализация «ControlType» неверна.

Полная реализация «Типа управления» не вызывает проблем.

Живая демонстрация: https://codesandbox.io/s/lucid-bassi-ceo6t?file=/src/app/demo/forms/type -test.ts

Screenshot 2020-07-01 at 00 35 11

То есть я даже не буду пытаться реализовать Controlв будущем Pull Requests для Angular, как я и планировал ранее.

ОК, значит, ваш 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

Итак, вы говорите, что в этом примере «два» не должно быть необязательным? Думаю, это неправильный способ набирать значение формы. Значение формы включает только не отключенные поля. Итак, с моей точки зрения, это должно быть рекурсивное частичное. Вы не можете знать во время компиляции, какие поля будут отключены, а какие нет.

Итак, вы говорите, что в этом примере «два» не должно быть необязательным?

Какие? Нет.

Мой тест вашего решения:

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

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

Что я вижу вcodeandbox после наведения курсора мыши на value :

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

Здесь PartialFormGroupValue относится к условному типу PartialFormValue .

А, ладно, думаю, я понял. То есть вы имеете в виду, что шрифт трудно читать, не так ли? Первоначально я думал, что вы говорите об ошибке или что-то в этом роде.

Что ж, большинство IDE по-прежнему будут просто предлагать доступные свойства после того, как вы продолжите вводить текст. Так что я не вижу здесь больших проблем. (Конечно, было бы лучше прочитать, если бы там было просто написано {two?: string} . Но я не думаю, что это очень важно. Это по крайней мере мое мнение.)

Если бы вы реализовали свой Control<T> , как бы вы затем удалили его из набора значения формы, не делая что-то вроде меня? И как сделать значение формы рекурсивным партиалом без использования вспомогательного типа?

Если вы реализовали свой Control, как бы вы затем удалили его из набора значения формы, не делая что-то вроде меня? И как сделать значение формы рекурсивным партиалом без использования вспомогательного типа?

В этом случае мое решение не лучше:

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

Я привел этот пример, потому что вы об этом просили:

Не могли бы вы привести примеры того, что плохо работает? (Особенно в тех случаях, когда, с вашей точки зрения, исправить нелегко?)

Кстати, я исправил критическую проблему с Control<T> .

Чтобы решить проблемы с привязкой HTML в Angular 10 и [formControl] , я пошел по следующему пути:

Как отмечалось в другом выпуске (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 type)

редактировать
Похоже, что это еще проще, если вам не нужно создавать классы, расширяющие 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 s, и создал статью, чтобы подвести итог всему:

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 рейтинги