Angular Реактивные формы с вложенными массивами форм

Я новичок в Angular 2 и решил, что лучший способ научиться - пройти официальные руководства по Angular.

Я прошел руководство по реактивным формам https://angular.io/guide/reactive-forms

демонстрационная ссылка: https://stackblitz.com/angular/jammvmbrpxle

Хотя содержание в целом было довольно хорошим, я застрял в том, как бы я реализовал более сложную форму. В приведенном примере каждый герой обладает потенциалом для множества адресов. Сам адрес является плоским объектом.

Что делать, если в адресах указана дополнительная информация, такая как цвет и тип номеров, расположенных по адресу.

export class Address {
    street = '';
    city   = '';
    state  = '';
    zip    = '';
    rooms = Room[];
}

export class Room {
     type = '';
}

так, чтобы модель формы выглядела так...

createForm() {
this.heroForm = this.fb.group({
  name: '',
  secretLairs: this.fb.array([
      this.fb.group({
          street: '',
          city: '',
          state: '',
          zip: '',
          rooms: this.fb.array([
              this.fb.group({
                 type: ''
          })]),
      })]),
  power: '',
  sidekick: ''
});

}

ОБНОВЛЕНИЕ - завершенный код, который работает с ngOnChanges

герой-detail.component.ts

createForm() {
    this.heroForm = this.fb.group({
      name: '',
      secretLairs: this.fb.array([
        this.fb.group({
          street: '',
          city: '',
          state: '',
          zip: '',
          rooms: this.fb.array([
            this.fb.group({
              type: ''
            })
          ])
        })
      ]),
      power: '',
      sidekick: ''
    });
  }

  ngOnChanges() {
    this.heroForm.reset({
      name: this.hero.name,
    });
    this.setAddresses(this.hero.addresses);
  }

  setAddresses(addresses: Address[]) {
    let control = this.fb.array([]);
    addresses.forEach(x => {
      control.push(this.fb.group({
        street: x.street,
        city: x.city,
        state: x.state,
        zip: x.zip,
        rooms: this.setRooms(x) }))
    })
    this.heroForm.setControl('secretLairs', control);
  }

  setRooms(x) {
    let arr = new FormArray([])
    x.rooms.forEach(y => {
      arr.push(this.fb.group({ 
        type: y.type 
      }))
    })
    return arr;
  }

hero-detail.component.html (часть массива вложенных форм)

<div formArrayName="secretLairs" class="well well-lg">
  <div *ngFor="let address of heroForm.get('secretLairs').controls; let i=index" [formGroupName]="i" >
    <!-- The repeated address template -->
    <h4>Address #{{i + 1}}</h4>
    <div style="margin-left: 1em;">
      <div class="form-group">
        <label class="center-block">Street:
          <input class="form-control" formControlName="street">
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">City:
          <input class="form-control" formControlName="city">
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">State:
          <select class="form-control" formControlName="state">
            <option *ngFor="let state of states" [value]="state">{{state}}</option>
          </select>
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">Zip Code:
          <input class="form-control" formControlName="zip">
        </label>
      </div>
    </div>
    <br>
    <!-- End of the repeated address template -->
    <div formArrayName="rooms" class="well well-lg">
      <div *ngFor="let room of address.get('rooms').controls; let j=index" [formGroupName]="j" >
          <h4>Room #{{j + 1}}</h4>
          <div class="form-group">
            <label class="center-block">Type:
              <input class="form-control" formControlName="type">
            </label>
          </div>
      </div>
    </div>
  </div>
  <button (click)="addLair()" type="button">Add a Secret Lair</button>
</div>

Ответ 1

Это не сильно отличается от того, чтобы иметь вложенный formarray. В основном вы просто дублируете код, который у вас есть... с вложенным массивом :) Итак, вот пример:

myForm: FormGroup;

constructor(private fb: FormBuilder) {
  this.myForm = this.fb.group({
    // you can also set initial formgroup inside if you like
    companies: this.fb.array([])
  })
}

addNewCompany() {
  let control = <FormArray>this.myForm.controls.companies;
  control.push(
    this.fb.group({
      company: [''],
      // nested form array, you could also add a form group initially
      projects: this.fb.array([])
    })
  )
}

deleteCompany(index) {
  let control = <FormArray>this.myForm.controls.companies;
  control.removeAt(index)
}

Итак, это добавление и удаление для самого внешнего массива форм, поэтому добавление и удаление групп форм во вложенном массиве форм просто дублирует код. Откуда из шаблона мы передаем текущую группу форм, в какой массив вы хотите добавить (в данном случае) новый проект/удалить проект.

addNewProject(control) {
  control.push(
    this.fb.group({
      projectName: ['']
  }))
}

deleteProject(control, index) {
  control.removeAt(index)
}

И шаблон тем же способом, вы итерируете свой внешний формат, а затем внутри этого итерируете свой внутренний массив форм:

<form [formGroup]="myForm">
  <div formArrayName="companies">
    <div *ngFor="let comp of myForm.get('companies').controls; let i=index">
    <h3>COMPANY {{i+1}}: </h3>
    <div [formGroupName]="i">
      <input formControlName="company" />
      <button (click)="deleteCompany(i)">
         Delete Company
      </button>
      <div formArrayName="projects">
        <div *ngFor="let project of comp.get('projects').controls; let j=index">
          <h4>PROJECT {{j+1}}</h4>
          <div [formGroupName]="j">
            <input formControlName="projectName" />
            <button (click)="deleteProject(comp.controls.projects, j)">
              Delete Project
            </button>
          </div>
        </div>
        <button (click)="addNewProject(comp.controls.projects)">
          Add new Project
        </button>
      </div>
    </div>
  </div>
</div>

DEMO

EDIT:

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

data = {
  companies: [
    {
      company: "example comany",
      projects: [
        {
          projectName: "example project",
        }
      ]
    }
  ]
}

Мы вызываем setCompanies, чтобы установить значения для нашей формы:

setCompanies() {
  let control = <FormArray>this.myForm.controls.companies;
  this.data.companies.forEach(x => {
    control.push(this.fb.group({ 
      company: x.company, 
      projects: this.setProjects(x) }))
  })
}

setProjects(x) {
  let arr = new FormArray([])
  x.projects.forEach(y => {
    arr.push(this.fb.group({ 
      projectName: y.projectName 
    }))
  })
  return arr;
}

Ответ 2

Вот что я сделал Я использовал angularflexlayout и angular материал, вы можете использовать любую библиотеку, просто хотел показать вам функциональность

     <form [formGroup]="editForm" novalidate fxLayout="column"
        autocomplete="off">

    <div fxLayout="row wrap" fxLayoutGap="2em">
      <mat-form-field [fxFlex]="30">
        <mat-label>Name</mat-label>
        <input matInput formControlName="name" width="800px"/>
      </mat-form-field>

    </div>
    <div fxLayout="column" fxLayoutGap="3em">
      <div fxLayout="row wrap" fxFlex="40" fxLayoutGap="2em">
        <div formArrayName="phones" fxFlex="50" fxLayoutGap="8px" *ngFor="let phone of Phones.controls;let i= index">
          <mat-form-field fxFlex="100" [formGroupName]="i">
            <mat-label>Phone</mat-label>
            <input matInput formControlName="phone"/>
          </mat-form-field>
        </div>
        <button type="button" mat-stroked-button color="primary" (click)="addPhone()">add</button>
      </div>
      <div fxLayout="row wrap" fxFlex="40" fxLayoutGap="2em">
        <div formArrayName="emails" fxFlex="50" fxLayoutGap="8px" *ngFor="let email of Emails.controls;let i= index">
          <mat-form-field fxFlex="100" [formGroupName]="i">
            <mat-label>Email</mat-label>
            <input matInput formControlName="email"/>
          </mat-form-field>
        </div>
        <button type="button" mat-stroked-button color="primary" (click)="addEmail()">add</button>
      </div>
    </div>

    <div class="mr-2" fxLayoutAlign="end" mat-dialog-actions>
      <button type="button" (click)="cancelDialog()" mat-button
              mat-dialog-close>Cancel
      </button>

      <button type="button" (click)="onSubmit()"
              mat-raised-button
              color="primary">
        Submit
      </button>
    </div>

  </form>

тогда angular контроллер

  editForm: FormGroup;
  phones: FormArray;
  emails: FormArray;

  createForm() {
    this.editForm = this.fb.group({
      name: [''],
        phones: this.fb.array([this.createPhone()]),
      emails: this.fb.array([this.createEmail()]),
     });

  }

  get Phones() {
    return this.editForm.get('phones') as FormArray;
  }

  get Emails() {
    return this.editForm.get('emails') as FormArray;
  }

 createPhone() {
    return this.fb.group(({
      phone: '',
    }));
  }

  createEmail() {
    return this.fb.group(({
      email: ''
    }));
  }


  addPhone(): void {
    this.phones = this.editListingForm.get('phones') as FormArray;
    this.phones.push(this.createPhone());
  }

  addEmail(): void {
    this.emails = this.editListingForm.get('emails') as FormArray;
    this.emails.push(this.createEmail());
  }