вложенный пользовательский компонент FormArray не связывается с дочерней формой с помощью FormArrayName

#angular #typescript #angular-reactive-forms #controlvalueaccessor

#angular #typescript #угловые реактивные формы #controlvalueaccessor

Вопрос:

Я попытался создать 2 вложенные формы, используя CVA. проблема в том, что второй из не инициализируется данными, когда я привязываю его к FormControl.

Stackblitz

введите описание изображения здесь

У меня есть ОСНОВНАЯ ФОРМА:

 this.requestForm = this.fb.group({
  garageId: 0,
  routes: new FormArray([
    new FormGroup({
      addressPointId: new FormControl,
      municipalityId: new FormControl,
      regionId: new FormControl,
      rvId: new FormControl,
      sequenceNumber: new FormControl,
      settlementId: new FormControl,
      regionName: new FormControl,
      municipalityName: new FormControl,
      settlementName: new FormControl,
      description: new FormControl,
    })
  ]),
  endDateTime: 0,
});
  

В основной форме html я связываю маршруты с помощью formArrayName .

  <app-cva-form-array formArrayName="routes"></app-cva-form-array>
  

Компонент CVA-FORM-ARRAY имеет.

 form = new FormArray([
new FormGroup({
  addressPointId: new FormControl,
  municipalityId: new FormControl,
  regionId: new FormControl,
  rvId: new FormControl,
  sequenceNumber: new FormControl,
  settlementId: new FormControl,
  regionName: new FormControl,
  municipalityName: new FormControl,
  settlementName: new FormControl,
  description: new FormControl,
})
]);
  

Отсюда все работает просто отлично. Я привязываю каждую группу форм в массиве к дочернему компоненту CVA-FORM.

 <app-cva-form [formControl]="route" (blur)="onTouched()"></app-cva-form>
  

CVA-FORM
для каждой группы форм я создал отдельный компонент на случай, если я хочу использовать сам компонент, а не весь массив.

   form: FormGroup = new FormGroup({
    regionName: new FormControl,
    regionId: new FormControl,
    municipalityName: new FormControl,
    municipalityId: new FormControl,
    sequenceNumber: new FormControl,
    settlementName: new FormControl,
    settlementId: new FormControl,
    addressPointId: new FormControl,
    description: new FormControl,
    rvId: new FormControl,
  });
  

привязка main-form <—to—> app-cva-form-array по какой-то причине не работает.

Идея этих форм взята из выступления Кары на angulaconnect.
вот ее слайды.

помогите, пожалуйста!

Комментарии:

1. Мне непонятно, вы спрашиваете о фоновой форме?

2. ДА. если вы посмотрите на json. вы увидите, что основная форма (фоновая форма) не синхронизирована с другими.

3. Взгляните на ответ posetd

Ответ №1:

Когда вы используете «пользовательский элемент управления формой», вам необходимо учитывать, что вы передаете элемент управления cursom Form с помощью элемента управления Form (не FormArray, не FormGroup). FormControl имеет в качестве значения массив или объект, но вас это не должно смущать.(*)

Вы можете увидеть в работе в stackblitz

Это ваша форма, как

 //in main.form
this.requestForm = new FormGroup({
  garageId: new FormControl(0),
  routes: new FormControl(routes), //<--routes will be an array of object
  endDateTime: new FormControl(0)
})

//in cva-form-array
this.form=new FormArray([new FormControl(...)]); //<-this.form is a 
                             //formArray of FormControls NOT of formGroup

//finally in your cva-form
this.form=new FormGroup({});
this.form=formGroup({
      addressPointId: new FormControl(),
      municipalityId: new FormControl(),
      ...
})
  

Я создал const для экспорта просто в код. МОЙ const expor

 export const dataI = {
  addressPointId: "",
  municipalityId: "",
  regionId: "",
  rvId: "",
  sequenceNumber: "",
  settlementId: "",
  regionName: "",
  municipalityName: "",
  settlementName: "",
  description: "",
}
  

Итак, в MainForm мы имеем

   ngOnInit() {
    let routes:any[]=[];
    routes.push({...dataI});
    this.requestForm = new FormGroup({
      garageId: new FormControl(0),
      routes: new FormControl(routes),
      endDateTime: new FormControl(0)
    })
  }
<mat-card [formGroup]="requestForm" style="background: #8E8D8A">
    <app-cva-form-array formControlName="routes"></app-cva-form-array>
</mat-card>
  

В массиве cvc-form создайте FormArray, когда мы зададим значение

   writeValue(v: any) {
    this.form=new FormArray([]);
    for (let value of v)
        this.form.push(new FormControl(value))

    this.form.valueChanges.subscribe(res=>
    {
      if (this.onChange)
        this.onChange(this.form.value)
    })
  }

    <form [formGroup]="form" >
        <mat-card *ngFor="let route of form.controls; 
            let routeIndex = index; let routeLast = last;">
           <button (click)="deleteRoute(routeIndex)">
             cancel
           </button>
           <app-cva-form [formControl]="route" (blur)="onTouched()"></app-cva-form>
      </form>
  

Наконец, cva-форма

   writeValue(v: any) {
    this.form=new FormGroup({});
    Object.keys(dataI).forEach(x=>{
      this.form.addControl(x,new FormControl())
    })

    this.form.setValue(v, { emitEvent: false });
    this.form.valueChanges.subscribe(res=>{
       if (this.onChanged)
        this.onChanged(this.form.value)
    })
  }

<div [formGroup]="form">
  <mat-form-field class="locationDate">
    <input formControlName="regionName">
    <mat-autocomplete #region="matAutocomplete" 
      (optionSelected)="selectedLocation($event)">
      <mat-option *ngFor="let region of regions" 
      [value]="region">
        {{region.regionName}}
      </mat-option>
    </mat-autocomplete>
  </mat-form-field>
  <mat-form-field class="locationDate">
    <input formControlName="municipalityName" 
      [matAutocomplete]="municipality"
      (blur)="onTouched()"
      [readonly]="checked || this.form.value.regionId < 1">
   ....
   </form>
  

(*) Да, мы привыкли видеть, что FormControl имеет в качестве значения строку или число, но никто не запрещает нам, чтобы значение было объектом или массивом (например, ng-bootstrap DatePicker хранит объект {год: .. месяц: .., день ..},mat-multiselect хранит массив, …)

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

   getForm(data: any): FormGroup {
    data = data || {} as IData;
    return new FormGroup({
      garageId: new FormControl(data.garageId),
      routes: new FormControl(data.routes),
      endDateTime: new FormControl(data.endDateTime)
    })
  }
  

где IData — это интерфейс

 export interface IData {
  garageId: number;
  routes: IDetail[];
  endDateTime: any
}
  

и выберите другой интерфейс

 export interface IDetail {
  addressPointId: string;
  ...
  description: string;
}
  

Тогда у нас могут быть сложные данные, такие как (извините за большой объект)

 let data = {
  garageId: 1,
  routes: [{
    addressPointId: "adress",
    municipalityId: "municipallyty",
    regionId: "regionId",
    rvId: "rvId",
    sequenceNumber: "sequenceNumber",
    settlementId: "settlementId",
    regionName: "regionName",
    municipalityName: "municipalityName",
    settlementName: "settlementName",
    description: "description",
  },
  {
    addressPointId: "another adress",
    municipalityId: "another municipallyty",
    regionId: "another regionId",
    rvId: "another rvId",
    sequenceNumber: "another sequenceNumber",
    settlementId: "another settlementId",
    regionName: "another regionName",
    municipalityName: "another municipalityName",
    settlementName: "another settlementName",
    description: "another description",
  }],
  endDateTime: new Date()
}
  

Тогда нужно только сделать

 this.requestForm = this.getForm(data);
  

Stackblitz, если он обновлен

Комментарии:

1. Спасибо за ответ! могу ли я использовать интерфейс вместо const? Я не хочу, чтобы форма-массив была привязана к основной форме. Или вы думаете, что у меня должен быть const в другом файле?

2. здравствуйте, я попытался поместить решение в свой проект. Я пытаюсь загрузить исходные данные, которые уже существуют. Я следовал тому же коду, что и ваш. но я заметил, что когда я инициализирую данные, они не передаются дочерним элементам, и когда я пытаюсь добавить другие маршруты, он помещает индекс перед объектами массива. вот так «маршруты»: { «0»: { «RegionID»: «» … «municipalityId»: «» } } . это также может быть проблемой с инициализацией. знаете ли вы, какое может быть решение?

3. @Vato, я обновил ответ и stackblitz, надеюсь, это поможет. Я пытался использовать интерфейс или константу, но безрезультатно:(, но, конечно, интерфейсы и константы могут быть вне основной формы

4. @Eliseo, спасибо, он отлично работает. но он все равно добавляет индекс перед массивами после добавления адреса. он создает массивы { 0: {} 1: {} … } и т.д. перед добавлением массив выглядит нормально [{ },{ },{ }]

5. чтобы избежать изменения выражения… иногда это полезно setTimeOut(()=>{...your code..})

Ответ №2:

Я считаю, что проблема здесь в том, что formArrayName это не вход для NG_VALUE_ACCESSOR/DefaultValueAccessor .

Также обратите внимание:

Ее примеры статичны parent->multiple children … значение от 1 до многих и не динамическое. Вы пытаетесь создать статические parent для многих динамических child->grandChild отношений, построенных из formArray , затем пытаетесь динамически связать grandChild форму с родительской formArrayIndex формой, которая была передана через child grandChild . Ваш stackblitz отклоняется от структуры, которую она преподает, и, безусловно, представляет некоторые новые проблемы, не рассмотренные в лекции.

Возможным решением может быть изучение того, как перебирать FormArray на parent уровне и создавать ваши child->grandchild отношения из этого цикла, таким образом, вы не передаете весь массив вниз, а только formGroup то, что применимо.

 <h1>MAIN FORM</h1>
    {{ requestForm.value | json }}
    <div *ngFor="let route of requestForm.get('routes').controls">
        <app-cva-form-array formControl="route" (onChildFormValueChange)="onFormChange($event)"></app-cva-form-array>
    </div>
  

Селекторы

  • ввод: нет([type=checkbox])[formControlName]
  • текстовая область [formControlName]
  • ввод: нет ([type=checkbox])[FormControl]
  • текстовая область [FormControl]
  • ввод: нет([type=checkbox])[ngModel]
  • текстовая область[ngModel]
  • [ngDefaultControl]

https://angular.io/api/forms/DefaultValueAccessor#selectors


Единственными вариантами ввода являются formControlName , formControl , ngModel и ngDefaultControl

Это причина formArrayName , по которой, однако, не main-form
<--> cva-form-array
formControl будет работать child-child to child level , поскольку вы передаете единственное formControl число в свой app-cva-form , из своего app-cva-form-array через *ngFor цикл.

 <mat-card *ngFor="let route of getForm.controls;
   <app-cva-form [formControl]="route" (blur)="onTouched()"></app-cva-form>
  

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

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

Ответ №3:

Вам необходимо передать обновленные данные формы из дочернего компонента в родительский компонент. Я использовал this.form.valueChanges() метод для обнаружения изменений, а затем передал значение формы родительскому компоненту.

Родительский компонент:

HTML-код:

 <app-cva-form-array formArrayName="routes" (onChildFormValueChange)="onFormChange($event)"></app-cva-form-array>
  

TS-код:

 public onFormChange(form): void {
    this.requestForm = form;
}
  

Дочерний компонент:

HTML-код:

 No change:)
  

TS-код:

 @Output() onChildFormValueChange: EventEmitter<any> = new EventEmitter<any>();

registerEvent() {
    this.form.valueChanges.subscribe(() => {
      this.onFormValueChange()
    });
}

public onFormValueChange(): void {
    this.onChildFormValueChange.emit(this.form);
}
  

и вызовите registerEvent метод в конструкторе, например:

 constructor(){
  this.registerEvent();
}
  

Working_Stackblitz

Комментарии:

1. это дает только одностороннюю привязку. хотя я знаю, как сделать 2 способа, это все еще не то, что я ищу. если вы проверите, как cva-form-array привязывается к cva-form, я хочу добиться аналогичной привязки для main-form <—> cva-form-array . цель использования cva — упростить код и не использовать дополнительные функции.

2. Я добавил внешние ресурсы о происхождении кода в нижней части вопроса.

3. @Vato Нет, это двусторонняя привязка

4. Если вы измените основную форму, cva-form-array не изменится. если вы напишете RegionName: new FormControl(‘asdfa’), в main-form он оставляет имена регионов дочерних компонентов null.

5. stackblitz.com/edit/angular-b6nm8c здесь, если вы добавляете массив из родительского, он не подключается к дочернему, а только наоборот. но в любом случае, как я уже сказал, ни одна из функций не должна требоваться с использованием cva.