Ошибка "validators.map не является функцией" в моем пользовательском элементе управления формы - PullRequest
0 голосов
/ 16 февраля 2020

Я создал семейство компонентов для использования в качестве элементов управления формы с ControlValueAccessor. При применении formControlName, formGroupName или formGroupArray к моим компонентам для передачи элементов управления формы я получаю сообщение об ошибке

validators.map не является функцией

Вот так настроен мой компонент

@Component({
  selector: 'view-box-form-component',
  templateUrl: './view-box-form.component.html',
  styleUrls: ['./view-box-form.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(()=>ViewBoxFormComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(()=>ViewBoxFormComponent)
    }
  ]
})

export class ViewBoxFormComponent implements OnInit, ControlValueAccessor {

  ViewBoxFormData: FormGroup;

  constructor() { }

  ngOnInit() {}

  public onTouched : ()=> void =()=>{};

  writeValue(val: any):void{ val && this.ViewBoxFormData.setValue(val, {emitEvent: false}); console.log(val); }

  registerOnChange(fn: any): void{ this.ViewBoxFormData.valueChanges.subscribe(fn); console.log(fn); }

  registerOnTouched(fn: any): void{ this.onTouched = fn; }

  setDisabledState?(isDisabled: boolean): void{ isDisabled ? this.ViewBoxFormData.disable() : this.ViewBoxFormData.enable(); }

  validate(c: AbstractControl): ValidationErrors | null{
    return this.ViewBoxFormData.valid ? null : {invalidForm:{valid: false, message: 'field invalid'}};
  }


}

и это шаблон для этого компонента

<section [formGroup]="ViewBoxFormData" class="text-control-section">


    <label class="control-label">

        <p class="smallText"> x: </p>
        <input type="text" class="text-control smallText" formControlName="x" />

    </label>


    <label class="control-label">

        <p class="smallText"> y: </p>
        <input type="y" class="text-control smallText" formControlName="y" />

    </label>


    <label class="control-label">

        <p class="smallText"> width: </p>
        <input type="text" class="text-control smallText" formControlName="width" />

    </label>


    <label class="control-label">

        <p class="smallText"> height: </p>
        <input type="text" class="text-control smallText" formControlName="height" />

    </label>


</section>

Внутри шаблона для родительского компонента я реализую компонент следующим образом

<form [formGroup]="SvgFormData" (ngSubmit)="onSubmit()">

    <view-box-form-component formGroupName="viewBox"></view-box-form-component>

</form>

У меня также есть пара других компонентов, которые мне нужно передать FormArray с, которые я пытаюсь сделать с formArrayName="someControl". Вместо того чтобы создавать форму внутри компонентов, я генерирую ее с помощью функций, основанных на JSON объектах. Функция создания группы форм для данных окна просмотра выглядит следующим образом.

export function CreateViewBoxForm(data?: ViewBoxParameters): FormGroup{
    return new FormGroup({
      x      :  new FormControl((data ? data.x      : ''), Validators.required),
      y      :  new FormControl((data ? data.y      : ''), Validators.required),
      width  :  new FormControl((data ? data.width  : ''), Validators.required),
      height :  new FormControl((data ? data.height : ''), Validators.required)
    }) as FormGroup;
  }

Когда я console.log() заполненные данные формы все правильно определены со всеми правильными значениями, я просто не могу понять, как заставить их правильно переходить в мои компоненты. Кто-нибудь видит, что я делаю не так, или, если мне нужно что-то еще, чтобы это работало правильно?

ОБНОВЛЕНИЕ

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

демонстрация стекаблиц

1 Ответ

2 голосов
/ 16 февраля 2020

Я думаю, вам нужно добавить multi: true к вашему NG_VALIDATORS токену.


Кроме того, похоже, что ваш ViewBoxFormComponent не является простым FormControl, потому что внутри вашего компонента у вас есть другой экземпляр FormGroup. ИМО, это не совсем правильно.

FormControlDirective, FormControlName, FormGroupName, FormArrayName, FormGroupDirective - это директивы, основанные на управлении формой, и при объединении они образуют AbstractControlDirective дерево (потому что каждый наследует от AbstractControlDirective). Эти директивы являются мостом между уровнем представления (обрабатывается ControlValueAccessor) и уровнем модели (обрабатывается AbstractControl - базовый класс для FormControl, FormGroup, FormArray).

root такого дерева FormGroupDirective (обычно привязано к элементу form: <form [formGroup]="formGroupInstance">). Имея это в виду, ваше дерево AbstractControl будет выглядеть так:

   FG <- [formGroup]="SvgFormData"
    |
    FC <- formGroupName="viewBox"
  / | \
 /  |  \
FC  FC  FC (height, width, x etc...)

У FormControl не должно быть потомков. В этом случае, потому что ваш пользовательский компонент (ViewBoxFormComponent) создает другое дерево. Это не неправильно , но при таком подходе вам придется правильно обрабатывать оба дерева, чтобы не получить неожиданные результаты.

Экземпляр FormGroup, связанный с <section [formGroup]="ViewBoxFormData" class="text-control-section"> не будет зарегистрировано как потомок SvgFormData, поскольку FormGroupDirective не ведет себя как FormGroupName:

FormGroupDirective

constructor(
      @Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[],
      @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[]) {
    super();
  }

FormGroupName

  constructor(
      @Optional() @Host() @SkipSelf() parent: ControlContainer, // `ControlContainer` - token for `FormGroupDirective`/`FormGroupName`/`FormArrayName`
      @Optional() @Self() @Inject(NG_VALIDATORS) validators: any[],
      @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) {
    super();
    this._parent = parent;
    this._validators = validators;
    this._asyncValidators = asyncValidators;
  }

Если ваш пользовательский компонент не должен содержать простой экземпляр FormControl (он обрабатывает только автономный FormControl экземпляр с помощью директив formControl или ngModel), вы должны зарегистрировать его как группу форм-потомков.
Это может быть достигнуто следующим способом:

parent.component.ts

// The shape of the form is defined in one place!
// The children must only comply with the shape defined here

this.SvgFormData = this.fb.group({
  ViewBoxFormData: this.fb.group({
   height: this.fb.control(''),
   width: this.fb.control(''),
  /* ... */
  })
})

parent.component. html

<form [formGroup]="SvgFormData" (ngSubmit)="onSubmit()">

    <!-- this will be a child form group -->
    <view-box-form-component></view-box-form-component>

</form>

view-box-form-component.component.ts

@Component({
  selector: 'view-box-form-component',
  templateUrl: './view-box-form.component.html',
  styleUrls: ['./view-box-form.component.css'],
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(()=>ViewBoxFormComponent),
      multi: true,
    }
  ],
 viewProviders: [
   { provide: ControlContainer, useExisting: FormGroupDirective }
 ]
})

export class ViewBoxFormComponent implements OnInit {

  constructor() { }

  ngOnInit() {}


  validate(c: AbstractControl): ValidationErrors | null{
    /* ... */
  }
}

Как видите, в этом нет необходимости предоставить CONTROL_VALUE_ACCESSOR больше, потому что форма формы будет определена только в одном месте (то есть: контейнер родительской формы). Все, что вам нужно сделать в вашем дочернем контейнере формы, это предоставить правильные пути, которые соответствуют форме, определенной в родительском:

view-box-form-component.component. html

<section formGroupName="ViewBoxFormData" class="text-control-section">


    <label class="control-label">

        <p class="smallText"> x: </p>
        <input type="text" class="text-control smallText" formControlName="x" />

    </label>


    <label class="control-label">

        <p class="smallText"> y: </p>
        <input type="y" class="text-control smallText" formControlName="y" />

    </label>


    <label class="control-label">

        <p class="smallText"> width: </p>
        <input type="text" class="text-control smallText" formControlName="width" />

    </label>


    <label class="control-label">

        <p class="smallText"> height: </p>
        <input type="text" class="text-control smallText" formControlName="height" />

    </label>


</section>

Что viewProviders позволяет вам использовать токен (в данном случае ControlContainer), который объявлен для элемента host:

// FormGroupName

constructor (@Optional() @Host() @SkipSelf() parent: ControlContainer,) {}

Примечание: тот же подход может быть примененным к FormArrayName.

Вы можете найти больше о AbstractControl дереве и о том, как все работает вместе, в Тщательное исследование Angular Формы .

...