From f7686521a2a22c49b96824d3ffb2542a69f97249 Mon Sep 17 00:00:00 2001
From: jojohoch <joachim.hoch@iqb.hu-berlin.de>
Date: Tue, 27 Jul 2021 10:03:44 +0200
Subject: [PATCH] [player] Use `PageComponent` as child formGroup

* Register `PageComponent` as child formGroup, so that values for
any page can be sent to the host.
* Register formControls as controls of the page
---
 .../form-element-component.directive.ts       |  6 +--
 projects/common/form.service.ts               | 11 ++++-
 projects/common/form.ts                       |  8 +++-
 .../src/app/components/form.component.ts      | 41 ++++++++++---------
 .../src/app/components/page.component.ts      | 36 ++++++++++++++--
 .../app/components/player-state.component.ts  |  4 +-
 6 files changed, 77 insertions(+), 29 deletions(-)

diff --git a/projects/common/form-element-component.directive.ts b/projects/common/form-element-component.directive.ts
index 5efe26e78..58f9d3764 100644
--- a/projects/common/form-element-component.directive.ts
+++ b/projects/common/form-element-component.directive.ts
@@ -25,13 +25,13 @@ export abstract class FormElementComponent extends ElementComponent implements O
   ngOnInit(): void {
     const formControl = new FormControl(this.elementModel.value, this.getValidations());
     const id = this.elementModel.id;
-    this.formService.registerFormControl({ id, formControl });
+    this.formService.registerFormControl({ id, formControl, formGroup: this.parentForm });
     this.elementFormControl = this.getFormControl(id);
     this.elementFormControl.valueChanges
       .pipe(
-        takeUntil(this.ngUnsubscribe),
         startWith(this.elementModel.value),
-        pairwise()
+        pairwise(),
+        takeUntil(this.ngUnsubscribe)
       )
       .subscribe(([prevValue, nextValue] : [string | number | boolean | undefined, string | number | boolean]) => {
         if (nextValue != null) { // invalid input on number fields generates event with null TODO find a better solution
diff --git a/projects/common/form.service.ts b/projects/common/form.service.ts
index 61d1dd048..fa30ea975 100644
--- a/projects/common/form.service.ts
+++ b/projects/common/form.service.ts
@@ -1,6 +1,6 @@
 import { Injectable } from '@angular/core';
 import { Observable, Subject } from 'rxjs';
-import { FormControlElement, ValueChangeElement } from './form';
+import { FormControlElement, FormGroupPage, ValueChangeElement } from './form';
 
 @Injectable({
   providedIn: 'root'
@@ -8,6 +8,7 @@ import { FormControlElement, ValueChangeElement } from './form';
 export class FormService {
   private _elementValueChanged = new Subject<ValueChangeElement>();
   private _controlAdded = new Subject<FormControlElement>();
+  private _groupAdded = new Subject<FormGroupPage>();
 
   get elementValueChanged(): Observable<ValueChangeElement> {
     return this._elementValueChanged.asObservable();
@@ -17,6 +18,10 @@ export class FormService {
     return this._controlAdded.asObservable();
   }
 
+  get groupAdded(): Observable<FormGroupPage> {
+    return this._groupAdded.asObservable();
+  }
+
   changeElementValue(elementValues: ValueChangeElement): void {
     this._elementValueChanged.next(elementValues);
   }
@@ -24,4 +29,8 @@ export class FormService {
   registerFormControl(control: FormControlElement): void {
     this._controlAdded.next(control);
   }
+
+  registerFormGroup(group: FormGroupPage): void {
+    this._groupAdded.next(group);
+  }
 }
diff --git a/projects/common/form.ts b/projects/common/form.ts
index 31a950618..d22e47eab 100644
--- a/projects/common/form.ts
+++ b/projects/common/form.ts
@@ -1,4 +1,4 @@
-import { FormControl } from '@angular/forms';
+import { FormControl, FormGroup } from '@angular/forms';
 
 export interface ValueChangeElement {
   id: string;
@@ -8,4 +8,10 @@ export interface ValueChangeElement {
 export interface FormControlElement {
   id: string;
   formControl: FormControl;
+  formGroup: FormGroup;
+}
+
+export interface FormGroupPage {
+  id: string;
+  formGroup: FormGroup;
 }
diff --git a/projects/player/src/app/components/form.component.ts b/projects/player/src/app/components/form.component.ts
index 09a9bdea6..b4dce6d80 100644
--- a/projects/player/src/app/components/form.component.ts
+++ b/projects/player/src/app/components/form.component.ts
@@ -1,11 +1,11 @@
 import { Component, Input, OnDestroy } from '@angular/core';
-import { FormGroup } from '@angular/forms';
+import { FormArray, FormBuilder, FormGroup } from '@angular/forms';
 import { Subject } from 'rxjs';
 import { takeUntil } from 'rxjs/operators';
 import { FormService } from '../../../../common/form.service';
 import { VeronaSubscriptionService } from '../services/verona-subscription.service';
 import { VeronaPostService } from '../services/verona-post.service';
-import { FormControlElement, ValueChangeElement } from '../../../../common/form';
+import { FormGroupPage, ValueChangeElement } from '../../../../common/form';
 import {
   PlayerConfig, UnitState, VopNavigationDeniedNotification
 } from '../models/verona';
@@ -25,36 +25,37 @@ import { UnitPage } from '../../../../common/unit';
 export class FormComponent implements OnDestroy {
   @Input() pages: UnitPage[] = [];
   @Input() playerConfig!: PlayerConfig;
-  form = new FormGroup({});
+  form!: FormGroup;
   private ngUnsubscribe = new Subject<void>();
 
-  constructor(private formService: FormService,
+  constructor(private formBuilder: FormBuilder,
+              private formService: FormService,
               private veronaSubscriptionService: VeronaSubscriptionService,
               private veronaPostService: VeronaPostService) {
+    this.form = this.formBuilder.group({
+      pages: this.formBuilder.array([])
+    });
     this.initSubscriptions();
   }
 
   get validPages():Record<string, string>[] {
-    return this.pages.map((page:UnitPage, index:number) => {
-      const validPage: Record<string, string> = {};
-      validPage[`page${index}`] = `Seite ${index + 1}`;
-      return validPage;
-    });
+    return this.pages.map((page:UnitPage): Record<string, string> => (
+      { [page.id]: page.label }));
   }
 
   private initSubscriptions(): void {
     this.formService.elementValueChanged
       .pipe(takeUntil(this.ngUnsubscribe))
       .subscribe((value: ValueChangeElement): void => this.onElementValueChanges(value));
-    this.formService.controlAdded
+    this.formService.groupAdded
       .pipe(takeUntil(this.ngUnsubscribe))
-      .subscribe((control: FormControlElement): void => this.addControl(control));
+      .subscribe((group: FormGroupPage): void => this.addGroup(group));
     this.veronaSubscriptionService.vopNavigationDeniedNotification
       .pipe(takeUntil(this.ngUnsubscribe))
       .subscribe((message: VopNavigationDeniedNotification): void => this.onNavigationDenied(message));
     this.form.valueChanges
       .pipe(takeUntil(this.ngUnsubscribe))
-      .subscribe((formValues: any): void => this.onFormChanges(formValues));
+      .subscribe((formValues: { pages: Record<string, string>[] }): void => this.onFormChanges(formValues));
   }
 
   private onNavigationDenied(message: VopNavigationDeniedNotification): void {
@@ -63,8 +64,9 @@ export class FormComponent implements OnDestroy {
     this.form.markAllAsTouched();
   }
 
-  private addControl(control: FormControlElement): void {
-    this.form.addControl(control.id, control.formControl);
+  private addGroup(group: FormGroupPage): void {
+    const pages: FormArray = this.form.get('pages') as FormArray;
+    pages.push(new FormGroup({ [group.id]: group.formGroup }));
   }
 
   private onElementValueChanges = (value: ValueChangeElement): void => {
@@ -72,14 +74,15 @@ export class FormComponent implements OnDestroy {
     console.log(`player: onElementValueChanges - ${value.id}: ${value.values[0]} -> ${value.values[1]}`);
   };
 
-  private onFormChanges(formValues: unknown): void {
+  private onFormChanges(formValues: { pages: Record<string, string>[] }): void {
     // eslint-disable-next-line no-console
     console.log('player: onFormChanges', formValues);
-    // TODO: map by page and? section not all
     const unitState: UnitState = {
-      dataParts: {
-        all: JSON.stringify(formValues)
-      }
+      dataParts: formValues.pages
+        .reduce((obj, page): Record<string, string> => {
+          obj[Object.keys(page)[0]] = JSON.stringify(page[Object.keys(page)[0]]);
+          return obj;
+        }, {})
     };
     this.veronaPostService.sendVopStateChangedNotification({ unitState });
   }
diff --git a/projects/player/src/app/components/page.component.ts b/projects/player/src/app/components/page.component.ts
index d7a414a70..883f8f56d 100644
--- a/projects/player/src/app/components/page.component.ts
+++ b/projects/player/src/app/components/page.component.ts
@@ -1,12 +1,18 @@
-import { Component, Input } from '@angular/core';
+import {
+  Component, Input, OnDestroy, OnInit
+} from '@angular/core';
 import { FormGroup } from '@angular/forms';
+import { takeUntil } from 'rxjs/operators';
+import { Subject } from 'rxjs';
 import { UnitPage } from '../../../../common/unit';
+import { FormService } from '../../../../common/form.service';
+import { FormControlElement } from '../../../../common/form';
 
 @Component({
   selector: 'app-page',
   template: `
       <app-section *ngFor="let section of page.sections; let i = index"
-                   [parentForm]="parentForm"
+                   [parentForm]="pageForm"
                    [section]="section"
                    [ngStyle]="{
                 position: 'relative',
@@ -19,7 +25,31 @@ import { UnitPage } from '../../../../common/unit';
   `
 })
 
-export class PageComponent {
+export class PageComponent implements OnInit, OnDestroy {
   @Input() page!: UnitPage;
   @Input() parentForm!: FormGroup;
+  pageForm!: FormGroup;
+  private ngUnsubscribe = new Subject<void>();
+
+  constructor(private formService: FormService) {}
+
+  private addControl(control: FormControlElement): void {
+    // we need to check that the control belongs to the page
+    if (this.pageForm === control.formGroup) {
+      this.pageForm.addControl(control.id, control.formControl);
+    }
+  }
+
+  ngOnInit(): void {
+    this.pageForm = new FormGroup({});
+    this.formService.registerFormGroup({ id: this.page.id, formGroup: this.pageForm });
+    this.formService.controlAdded.pipe(
+      takeUntil(this.ngUnsubscribe)
+    ).subscribe((control: FormControlElement): void => this.addControl(control));
+  }
+
+  ngOnDestroy(): void {
+    this.ngUnsubscribe.next();
+    this.ngUnsubscribe.complete();
+  }
 }
diff --git a/projects/player/src/app/components/player-state.component.ts b/projects/player/src/app/components/player-state.component.ts
index 1d7809731..0addbe8ad 100644
--- a/projects/player/src/app/components/player-state.component.ts
+++ b/projects/player/src/app/components/player-state.component.ts
@@ -19,7 +19,7 @@ import { VeronaPostService } from '../services/verona-post.service';
       <mat-tab-group [(selectedIndex)]="currentIndex"
                      (selectedIndexChange)="onSelectedIndexChange()"
                      mat-align-tabs="start">
-          <mat-tab *ngFor="let page of pages; let i = index" label="{{validPages[i]['page'+i]}}">
+          <mat-tab *ngFor="let page of pages; let i = index" label="{{page.label}}">
               <app-page [parentForm]="parenForm" [page]="page"></app-page>
           </mat-tab>
       </mat-tab-group>
@@ -38,7 +38,6 @@ export class PlayerStateComponent implements OnInit, OnDestroy {
 
   constructor(private veronaSubscriptionService: VeronaSubscriptionService,
               private veronaPostService: VeronaPostService) {
-    this.initSubscriptions();
   }
 
   private get state(): RunningState {
@@ -46,6 +45,7 @@ export class PlayerStateComponent implements OnInit, OnDestroy {
   }
 
   ngOnInit(): void {
+    this.initSubscriptions();
     this.sendVopStateChangedNotification();
   }
 
-- 
GitLab