From 6c38e1c08992d9da81a83e49a3e5caf215b5e984 Mon Sep 17 00:00:00 2001
From: jojohoch <joachim.hoch@iqb.hu-berlin.de>
Date: Thu, 28 Oct 2021 10:06:18 +0200
Subject: [PATCH] Refactor form-element-component.directive.ts

* Remove FormService Injection
* Move FormService and form types to player
* Move interface ValueChangeElement to uI-element.ts
* Use interface InputElementValue instead of
string | number | boolean | null
---
 .../element-components/audio.component.ts     |  2 +-
 .../compound-elements/likert.component.ts     |  3 +-
 .../control-bar/control-bar.component.ts      |  2 +-
 .../element-components/video.component.ts     |  2 +-
 .../form-element-component.directive.ts       | 26 +++-------
 projects/common/models/uI-element.ts          |  9 +++-
 .../canvas/canvas-element-overlay.ts          |  3 +-
 .../components/element/element.component.ts   | 51 +++++++++++++++----
 .../src/app/components/page/page.component.ts |  2 +-
 .../components/section/section.component.ts   |  2 +-
 .../unit-state/unit-state.component.ts        |  4 +-
 .../{common => player/src/app/models}/form.ts |  5 --
 .../src/app/services}/form.service.ts         |  2 +-
 .../src/app/services/unit-state.service.ts    |  3 +-
 14 files changed, 64 insertions(+), 52 deletions(-)
 rename projects/{common => player/src/app/models}/form.ts (76%)
 rename projects/{common => player/src/app/services}/form.service.ts (97%)

diff --git a/projects/common/element-components/audio.component.ts b/projects/common/element-components/audio.component.ts
index 74267d9b5..d07463380 100644
--- a/projects/common/element-components/audio.component.ts
+++ b/projects/common/element-components/audio.component.ts
@@ -1,7 +1,7 @@
 import { Component, EventEmitter, Output } from '@angular/core';
 import { ElementComponent } from '../element-component.directive';
 import { AudioElement } from '../models/audio-element';
-import { ValueChangeElement } from '../form';
+import { ValueChangeElement } from '../models/uI-element';
 
 @Component({
   selector: 'app-audio',
diff --git a/projects/common/element-components/compound-elements/likert.component.ts b/projects/common/element-components/compound-elements/likert.component.ts
index 85d34ba97..955633abb 100644
--- a/projects/common/element-components/compound-elements/likert.component.ts
+++ b/projects/common/element-components/compound-elements/likert.component.ts
@@ -1,8 +1,7 @@
 import { Component, EventEmitter, Output } from '@angular/core';
 import { FormGroup } from '@angular/forms';
 import { LikertElement } from '../../models/compound-elements/likert-element';
-import { ValueChangeElement } from '../../form';
-import { InputElementValue } from '../../models/uI-element';
+import { InputElementValue, ValueChangeElement } from '../../models/uI-element';
 import { LikertElementRow } from '../../models/compound-elements/likert-element-row';
 
 @Component({
diff --git a/projects/common/element-components/control-bar/control-bar.component.ts b/projects/common/element-components/control-bar/control-bar.component.ts
index bd97d67fb..5159636a8 100644
--- a/projects/common/element-components/control-bar/control-bar.component.ts
+++ b/projects/common/element-components/control-bar/control-bar.component.ts
@@ -4,7 +4,7 @@ import {
 import { MatSliderChange } from '@angular/material/slider';
 import { AudioElement } from '../../models/audio-element';
 import { VideoElement } from '../../models/video-element';
-import { ValueChangeElement } from '../../form';
+import { ValueChangeElement } from '../../models/uI-element';
 
 @Component({
   selector: 'app-control-bar',
diff --git a/projects/common/element-components/video.component.ts b/projects/common/element-components/video.component.ts
index 74874bd8d..0398d550a 100644
--- a/projects/common/element-components/video.component.ts
+++ b/projects/common/element-components/video.component.ts
@@ -1,7 +1,7 @@
 import { Component, EventEmitter, Output } from '@angular/core';
 import { ElementComponent } from '../element-component.directive';
 import { VideoElement } from '../models/video-element';
-import { ValueChangeElement } from '../form';
+import { ValueChangeElement } from '../models/uI-element';
 
 @Component({
   selector: 'app-video',
diff --git a/projects/common/form-element-component.directive.ts b/projects/common/form-element-component.directive.ts
index 1703b60c8..b0cf4cc0d 100644
--- a/projects/common/form-element-component.directive.ts
+++ b/projects/common/form-element-component.directive.ts
@@ -6,44 +6,30 @@ import {
 } from '@angular/forms';
 import { Subject } from 'rxjs';
 import { pairwise, startWith, takeUntil } from 'rxjs/operators';
-import { FormService } from './form.service';
-import { ValueChangeElement } from './form';
 import { ElementComponent } from './element-component.directive';
-import { InputElement } from './models/uI-element';
+import { InputElement, InputElementValue, ValueChangeElement } from './models/uI-element';
 
 @Directive()
 export abstract class FormElementComponent extends ElementComponent implements OnInit, OnDestroy {
   @Output() formValueChanged = new EventEmitter<ValueChangeElement>();
+  @Output() setValidators = new EventEmitter<ValidatorFn[]>();
   parentForm!: FormGroup;
-  defaultValue!: string | number | boolean | undefined;
+  defaultValue!: InputElementValue;
   elementFormControl!: FormControl;
 
   private ngUnsubscribe = new Subject<void>();
 
-  constructor(private formService: FormService) {
-    super();
-  }
-
   ngOnInit(): void {
-    this.formService.registerFormControl({
-      id: this.elementModel.id,
-      formControl: new FormControl((this.elementModel as InputElement).value),
-      formGroup: this.parentForm
-    });
     this.elementFormControl = this.formControl;
     this.updateFormValue((this.elementModel as InputElement).value);
-    this.formService.setValidators({
-      id: this.elementModel.id,
-      validators: this.validators,
-      formGroup: this.parentForm
-    });
+    this.setValidators.emit(this.validators);
     this.elementFormControl.valueChanges
       .pipe(
         startWith((this.elementModel as InputElement).value),
         pairwise(),
         takeUntil(this.ngUnsubscribe)
       )
-      .subscribe(([prevValue, nextValue] : [string | number | boolean | undefined, string | number | boolean]) => {
+      .subscribe(([prevValue, nextValue]: [InputElementValue, InputElementValue]) => {
         if (nextValue != null) { // invalid input on number fields generates event with null TODO find a better solution
           this.formValueChanged.emit({ id: this.elementModel.id, values: [prevValue, nextValue] });
         }
@@ -65,7 +51,7 @@ export abstract class FormElementComponent extends ElementComponent implements O
       new FormControl({});
   }
 
-  updateFormValue(newValue: string | number | boolean | null): void {
+  updateFormValue(newValue: InputElementValue): void {
     this.elementFormControl?.setValue(newValue, { emitEvent: false });
   }
 
diff --git a/projects/common/models/uI-element.ts b/projects/common/models/uI-element.ts
index ea5231369..89747ecd1 100644
--- a/projects/common/models/uI-element.ts
+++ b/projects/common/models/uI-element.ts
@@ -6,8 +6,13 @@ export type UIElementType = 'text' | 'button' | 'text-field' | 'text-area' | 'ch
 | 'dropdown' | 'radio' | 'image' | 'audio' | 'video' | 'likert' | 'likert_row';
 export type InputElementValue = string | number | boolean | null;
 
+export interface ValueChangeElement {
+  id: string;
+  values: [InputElementValue, InputElementValue];
+}
+
 export abstract class UIElement {
-  [index: string]: string | number | boolean | string[] | AnswerOption[] | LikertRow[] | null | ((...args: any) => any);
+  [index: string]: InputElementValue | string[] | AnswerOption[] | LikertRow[] | ((...args: any) => any);
   type!: UIElementType;
 
   id: string = 'id_placeholder';
@@ -35,7 +40,7 @@ export abstract class UIElement {
 
   // This can be overwritten by elements if they need to handle some property specifics. Likert does.
   setProperty(property: string,
-              value: string | number | boolean | string[] | AnswerOption[] | LikertRow[] | null): void {
+              value: InputElementValue | string[] | AnswerOption[] | LikertRow[]): void {
     this[property] = value;
   }
 }
diff --git a/projects/editor/src/app/unit-view/page-view/canvas/canvas-element-overlay.ts b/projects/editor/src/app/unit-view/page-view/canvas/canvas-element-overlay.ts
index 145f72401..fc70ebff1 100644
--- a/projects/editor/src/app/unit-view/page-view/canvas/canvas-element-overlay.ts
+++ b/projects/editor/src/app/unit-view/page-view/canvas/canvas-element-overlay.ts
@@ -8,10 +8,9 @@ import { takeUntil } from 'rxjs/operators';
 import { UnitService } from '../../../unit.service';
 import * as ElementFactory from '../../../../../../common/util/element.factory';
 import { FormElementComponent } from '../../../../../../common/form-element-component.directive';
-import { ValueChangeElement } from '../../../../../../common/form';
 import { ElementComponent } from '../../../../../../common/element-component.directive';
 import { SelectionService } from '../../../selection.service';
-import { InputElement, UIElement } from '../../../../../../common/models/uI-element';
+import { InputElement, UIElement, ValueChangeElement } from '../../../../../../common/models/uI-element';
 
 @Directive()
 export abstract class CanvasElementOverlay implements OnInit, OnDestroy {
diff --git a/projects/player/src/app/components/element/element.component.ts b/projects/player/src/app/components/element/element.component.ts
index b22cbcd8b..2556a170a 100644
--- a/projects/player/src/app/components/element/element.component.ts
+++ b/projects/player/src/app/components/element/element.component.ts
@@ -2,18 +2,24 @@ import {
   Component, OnInit, Input, ComponentFactoryResolver,
   ViewChild, ViewContainerRef
 } from '@angular/core';
-import { FormBuilder, FormGroup } from '@angular/forms';
+import {
+  FormBuilder, FormControl, FormGroup, ValidatorFn
+} from '@angular/forms';
 import { takeUntil } from 'rxjs/operators';
 import { Subject } from 'rxjs';
 import * as ElementFactory from '../../../../../common/util/element.factory';
 import { KeyboardService } from '../../services/keyboard.service';
 import { TextFieldComponent } from '../../../../../common/element-components/text-field.component';
 import { TextAreaComponent } from '../../../../../common/element-components/text-area.component';
-import { FormService } from '../../../../../common/form.service';
-import { ValueChangeElement } from '../../../../../common/form';
+import { FormService } from '../../services/form.service';
 import { UnitStateService } from '../../services/unit-state.service';
 import { MarkingService } from '../../services/marking.service';
-import { InputElementValue, UIElement } from '../../../../../common/models/uI-element';
+import {
+  InputElement,
+  InputElementValue,
+  UIElement,
+  ValueChangeElement
+} from '../../../../../common/models/uI-element';
 import { TextFieldElement } from '../../../../../common/models/text-field-element';
 import { FormElementComponent } from '../../../../../common/form-element-component.directive';
 
@@ -66,13 +72,6 @@ export class ElementComponent implements OnInit {
 
     this.unitStateService.registerElement(elementComponent.elementModel.id, elementComponent.elementModel.value);
 
-    if (this.elementModel.type === 'likert') {
-      elementComponent.getChildElementValues()
-        .forEach((element: { id: string, value: InputElementValue }) => (
-          this.unitStateService.registerElement(element.id, element.value)
-        ));
-    }
-
     if (elementComponent.applySelection) {
       elementComponent.applySelection
         .pipe(takeUntil(this.ngUnsubscribe))
@@ -94,6 +93,36 @@ export class ElementComponent implements OnInit {
       elementComponent.parentForm = elementForm;
       this.registerFormGroup(elementForm);
 
+      this.formService.registerFormControl({
+        id: this.elementModel.id,
+        formControl: new FormControl((this.elementModel as InputElement).value),
+        formGroup: elementForm
+      });
+
+      if (this.elementModel.type === 'likert') {
+        elementComponent.getChildElementValues()
+          .forEach((element: { id: string, value: InputElementValue }) => {
+            this.unitStateService.registerElement(element.id, element.value);
+            this.formService.registerFormControl({
+              id: element.id,
+              formControl: new FormControl((element as InputElement).value),
+              formGroup: elementForm
+            });
+          });
+      }
+
+      if (elementComponent.setValidators) {
+        elementComponent.setValidators
+          .pipe(takeUntil(this.ngUnsubscribe))
+          .subscribe((validators: ValidatorFn[]) => {
+            this.formService.setValidators({
+              id: this.elementModel.id,
+              validators: validators,
+              formGroup: elementForm
+            });
+          });
+      }
+
       elementComponent.formValueChanged
         .pipe(takeUntil(this.ngUnsubscribe))
         .subscribe((changeElement: ValueChangeElement) => {
diff --git a/projects/player/src/app/components/page/page.component.ts b/projects/player/src/app/components/page/page.component.ts
index 43b820f80..884707906 100644
--- a/projects/player/src/app/components/page/page.component.ts
+++ b/projects/player/src/app/components/page/page.component.ts
@@ -2,7 +2,7 @@ import {
   Component, Input, OnInit, Output, EventEmitter
 } from '@angular/core';
 import { FormBuilder, FormGroup } from '@angular/forms';
-import { FormService } from '../../../../../common/form.service';
+import { FormService } from '../../services/form.service';
 import { UnitStateService } from '../../services/unit-state.service';
 import { Page } from '../../../../../common/models/page';
 
diff --git a/projects/player/src/app/components/section/section.component.ts b/projects/player/src/app/components/section/section.component.ts
index 4d6283c4b..785c6d2f8 100644
--- a/projects/player/src/app/components/section/section.component.ts
+++ b/projects/player/src/app/components/section/section.component.ts
@@ -3,7 +3,7 @@ import {
 } from '@angular/core';
 import { FormBuilder, FormGroup } from '@angular/forms';
 import { DOCUMENT } from '@angular/common';
-import { FormService } from '../../../../../common/form.service';
+import { FormService } from '../../services/form.service';
 import { Section } from '../../../../../common/models/section';
 import { UnitStateService } from '../../services/unit-state.service';
 
diff --git a/projects/player/src/app/components/unit-state/unit-state.component.ts b/projects/player/src/app/components/unit-state/unit-state.component.ts
index 860332937..d08e486c9 100644
--- a/projects/player/src/app/components/unit-state/unit-state.component.ts
+++ b/projects/player/src/app/components/unit-state/unit-state.component.ts
@@ -8,14 +8,14 @@ import {
 import { Subject } from 'rxjs';
 import { takeUntil } from 'rxjs/operators';
 import { TranslateService } from '@ngx-translate/core';
-import { FormService } from '../../../../../common/form.service';
+import { FormService } from '../../services/form.service';
 import { VeronaSubscriptionService } from '../../services/verona-subscription.service';
 import { VeronaPostService } from '../../services/verona-post.service';
 import { MessageService } from '../../../../../common/message.service';
 import { MetaDataService } from '../../services/meta-data.service';
 import {
   FormControlElement, FormControlValidators, ChildFormGroup
-} from '../../../../../common/form';
+} from '../../models/form';
 import {
   PlayerConfig, Progress, UnitState, VopNavigationDeniedNotification
 } from '../../models/verona';
diff --git a/projects/common/form.ts b/projects/player/src/app/models/form.ts
similarity index 76%
rename from projects/common/form.ts
rename to projects/player/src/app/models/form.ts
index bde7833e7..4c467ef62 100644
--- a/projects/common/form.ts
+++ b/projects/player/src/app/models/form.ts
@@ -1,10 +1,5 @@
 import { FormControl, FormGroup, ValidatorFn } from '@angular/forms';
 
-export interface ValueChangeElement {
-  id: string;
-  values: [string | number | boolean | null | undefined, string | number | boolean];
-}
-
 export interface FormControlElement {
   id: string;
   formControl: FormControl;
diff --git a/projects/common/form.service.ts b/projects/player/src/app/services/form.service.ts
similarity index 97%
rename from projects/common/form.service.ts
rename to projects/player/src/app/services/form.service.ts
index 9a5043a49..4980ad78f 100644
--- a/projects/common/form.service.ts
+++ b/projects/player/src/app/services/form.service.ts
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
 import { Observable, Subject } from 'rxjs';
 import {
   FormControlElement, FormControlValidators, ChildFormGroup
-} from './form';
+} from '../models/form';
 
 @Injectable({
   providedIn: 'root'
diff --git a/projects/player/src/app/services/unit-state.service.ts b/projects/player/src/app/services/unit-state.service.ts
index 0e127aa13..665a1feb8 100644
--- a/projects/player/src/app/services/unit-state.service.ts
+++ b/projects/player/src/app/services/unit-state.service.ts
@@ -6,8 +6,7 @@ import {
   UnitStateElementCodeStatus,
   UnitStateElementCodeStatusValue
 } from '../models/verona';
-import { ValueChangeElement } from '../../../../common/form';
-import { InputElementValue } from '../../../../common/models/uI-element';
+import { InputElementValue, ValueChangeElement } from '../../../../common/models/uI-element';
 
 @Injectable({
   providedIn: 'root'
-- 
GitLab