From 89c6ebdf4f789b9f327cac53e09700f6b9966fa2 Mon Sep 17 00:00:00 2001 From: jojohoch <joachim.hoch@iqb.hu-berlin.de> Date: Fri, 16 Jul 2021 16:41:42 +0200 Subject: [PATCH] [player] Prepare validation of form elements * When creating components for each element a `ValidationMessageComponent` is also generated dynamically * Prepare usage of default values for form elements * Add `form.ts` for form specific interfaces * Rename some variables and properties --- .../element-components/checkbox.component.ts | 6 ++- .../correction.component.ts | 32 +++++++-------- .../element-components/dropdown.component.ts | 4 +- .../radio-button-group.component.ts | 2 +- .../text-field.component.ts | 30 +++++++------- .../form-element-component.directive.ts | 28 ++++++++----- projects/common/form.service.ts | 16 ++++---- projects/common/form.ts | 11 +++++ projects/common/unit.ts | 5 --- projects/player/src/app/app.component.ts | 40 ++++++++++++------- projects/player/src/app/app.module.ts | 4 +- .../components/element-overlay.component.ts | 40 ++++++++++++++----- .../src/app/components/section.component.ts | 2 +- .../validation-message.component.ts | 28 +++++++++++++ 14 files changed, 162 insertions(+), 86 deletions(-) create mode 100644 projects/common/form.ts create mode 100644 projects/player/src/app/components/validation-message.component.ts diff --git a/projects/common/element-components/checkbox.component.ts b/projects/common/element-components/checkbox.component.ts index c68db4f50..38aee9c55 100644 --- a/projects/common/element-components/checkbox.component.ts +++ b/projects/common/element-components/checkbox.component.ts @@ -5,8 +5,8 @@ import { FormElementComponent } from '../form-element-component.directive'; @Component({ selector: 'app-checkbox', template: ` - <mat-checkbox class="example-margin" - [formControl]="formControl" + <mat-checkbox #checkbox class="example-margin" + [formControl]="formElementControl" [style.width.px]="elementModel.width" [style.height.px]="elementModel.height" [style.background-color]="elementModel.backgroundColor" @@ -22,4 +22,6 @@ import { FormElementComponent } from '../form-element-component.directive'; }) export class CheckboxComponent extends FormElementComponent { elementModel!: CheckboxElement; + // TODO: get from elementModel + defaultValue: boolean = true; } diff --git a/projects/common/element-components/compound-components/correction.component.ts b/projects/common/element-components/compound-components/correction.component.ts index a8743f85d..390d7c5c6 100644 --- a/projects/common/element-components/compound-components/correction.component.ts +++ b/projects/common/element-components/compound-components/correction.component.ts @@ -5,26 +5,26 @@ import { FormElementComponent } from '../../form-element-component.directive'; @Component({ selector: 'app-correction', template: ` - <div> - <p> - {{$any(elementModel).text}} - </p> - <div *ngFor="let sentence of elementModel.sentences" - fxLayout="column"> - <div fxLayout="row"> - <div *ngFor="let word of sentence.split(' ');" + <div> + <p> + {{$any(elementModel).text}} + </p> + <div *ngFor="let sentence of elementModel.sentences" fxLayout="column"> - <mat-form-field> - <input matInput type="text" - [formControl]="formControl"> - </mat-form-field> - <div> - {{word}} + <div fxLayout="row"> + <div *ngFor="let word of sentence.split(' ');" + fxLayout="column"> + <mat-form-field> + <input matInput type="text" + [formControl]="formElementControl"> + </mat-form-field> + <div> + {{word}} + </div> + </div> </div> </div> - </div> </div> - </div> `, styles: [ 'mat-form-field {margin: 5px}' diff --git a/projects/common/element-components/dropdown.component.ts b/projects/common/element-components/dropdown.component.ts index fa6a6544c..f6f19b4e5 100644 --- a/projects/common/element-components/dropdown.component.ts +++ b/projects/common/element-components/dropdown.component.ts @@ -15,9 +15,9 @@ import { FormElementComponent } from '../form-element-component.directive'; [style.font-weight]="elementModel.bold ? 'bold' : ''" [style.font-style]="elementModel.italic ? 'italic' : ''" [style.text-decoration]="elementModel.underline ? 'underline' : ''"> - {{$any(elementModel).label}} + {{$any(elementModel).label}} </mat-label> - <mat-select [formControl]="formControl"> + <mat-select [formControl]="formElementControl"> <mat-option *ngFor="let option of elementModel.options" [value]="option"> {{option}} </mat-option> diff --git a/projects/common/element-components/radio-button-group.component.ts b/projects/common/element-components/radio-button-group.component.ts index ff3f35d56..ca9be5510 100644 --- a/projects/common/element-components/radio-button-group.component.ts +++ b/projects/common/element-components/radio-button-group.component.ts @@ -16,7 +16,7 @@ import { FormElementComponent } from '../form-element-component.directive'; [style.text-decoration]="elementModel.underline ? 'underline' : ''"> <label id="radio-group-label">{{elementModel.label}}</label> <mat-radio-group aria-labelledby="radio-group-label" fxLayout="{{elementModel.alignment}}" - [formControl]="formControl"> + [formControl]="formElementControl"> <mat-radio-button *ngFor="let option of elementModel.options" [value]="option"> {{option}} </mat-radio-button> diff --git a/projects/common/element-components/text-field.component.ts b/projects/common/element-components/text-field.component.ts index 3b4278920..d83f27c03 100644 --- a/projects/common/element-components/text-field.component.ts +++ b/projects/common/element-components/text-field.component.ts @@ -5,21 +5,21 @@ import { FormElementComponent } from '../form-element-component.directive'; @Component({ selector: 'app-text-field', template: ` - <input *ngIf="elementModel.multiline === false" matInput - placeholder="{{elementModel.placeholder}}" - [formControl]="formControl"> - <textarea *ngIf="elementModel.multiline === true" matInput - placeholder="{{elementModel.placeholder}}" - [formControl]="formControl" - [style.width.px]="elementModel.width" - [style.height.px]="elementModel.height" - [style.background-color]="elementModel.backgroundColor" - [style.color]="elementModel.fontColor" - [style.font-family]="elementModel.font" - [style.font-size.px]="elementModel.fontSize" - [style.font-weight]="elementModel.bold ? 'bold' : ''" - [style.font-style]="elementModel.italic ? 'italic' : ''" - [style.text-decoration]="elementModel.underline ? 'underline' : ''"> + <input *ngIf="elementModel.multiline === false" matInput + placeholder="{{elementModel.placeholder}}" + [formControl]="formElementControl"> + <textarea *ngIf="elementModel.multiline === true" matInput + placeholder="{{elementModel.placeholder}}" + [formControl]="formElementControl" + [style.width.px]="elementModel.width" + [style.height.px]="elementModel.height" + [style.background-color]="elementModel.backgroundColor" + [style.color]="elementModel.fontColor" + [style.font-family]="elementModel.font" + [style.font-size.px]="elementModel.fontSize" + [style.font-weight]="elementModel.bold ? 'bold' : ''" + [style.font-style]="elementModel.italic ? 'italic' : ''" + [style.text-decoration]="elementModel.underline ? 'underline' : ''"> </textarea> ` }) diff --git a/projects/common/form-element-component.directive.ts b/projects/common/form-element-component.directive.ts index 3811b31dd..4b38ebc9d 100644 --- a/projects/common/form-element-component.directive.ts +++ b/projects/common/form-element-component.directive.ts @@ -1,27 +1,35 @@ import { Directive, OnInit } from '@angular/core'; -import { FormControl, FormGroup } from '@angular/forms'; -import { pairwise, startWith } from 'rxjs/operators'; +import { + FormControl, FormGroup, ValidatorFn +} from '@angular/forms'; +import { pairwise } from 'rxjs/operators'; import { UnitUIElement } from './unit'; import { FormService } from './form.service'; @Directive() export abstract class FormElementComponent implements OnInit { - elementModel!: UnitUIElement; + abstract elementModel: UnitUIElement; parentForm!: FormGroup; - formControl: FormControl = new FormControl(); + defaultValue!: unknown; + formElementControl!: FormControl; - constructor(private formService: FormService) { } + constructor(private formService: FormService) {} ngOnInit(): void { - this.formService.registerFormControl(this.elementModel.id); - this.formControl = this.getFormControl(this.elementModel.id); - this.formControl.valueChanges - .pipe(startWith(null), pairwise()) + const formControl = new FormControl(this.defaultValue, this.getValidations()); + const id = this.elementModel.id; + this.formService.registerFormControl({ id, formControl }); + this.formElementControl = this.getFormControl(id); + this.formElementControl.valueChanges + .pipe(pairwise()) .subscribe( ([prevValue, nextValue] : [unknown, unknown]) => this.onValueChange([prevValue, nextValue]) ); } + // TODO: get from elementModel examples, example: [Validators.requiredTrue, Validators.required] + private getValidations = (): ValidatorFn[] => []; + private getFormControl(id: string): FormControl { // workaround for editor return (this.parentForm) ? this.parentForm.controls[id] as FormControl : new FormControl(); @@ -29,6 +37,6 @@ export abstract class FormElementComponent implements OnInit { private onValueChange(values: [unknown, unknown]): void { const element = this.elementModel.id; - this.formService.changeElementValue({ element, values }); + this.formService.changeElementValue({ id: element, values }); } } diff --git a/projects/common/form.service.ts b/projects/common/form.service.ts index d667760b0..61d1dd048 100644 --- a/projects/common/form.service.ts +++ b/projects/common/form.service.ts @@ -1,27 +1,27 @@ import { Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; -import { ChangeElement } from './unit'; +import { FormControlElement, ValueChangeElement } from './form'; @Injectable({ providedIn: 'root' }) export class FormService { - private _elementValueChanged = new Subject<ChangeElement>(); - private _controlAdded = new Subject<string>(); + private _elementValueChanged = new Subject<ValueChangeElement>(); + private _controlAdded = new Subject<FormControlElement>(); - get elementValueChanged(): Observable<ChangeElement> { + get elementValueChanged(): Observable<ValueChangeElement> { return this._elementValueChanged.asObservable(); } - get controlAdded(): Observable<string> { + get controlAdded(): Observable<FormControlElement> { return this._controlAdded.asObservable(); } - changeElementValue(elementValues: ChangeElement): void { + changeElementValue(elementValues: ValueChangeElement): void { this._elementValueChanged.next(elementValues); } - registerFormControl(controlId: string): void { - this._controlAdded.next(controlId); + registerFormControl(control: FormControlElement): void { + this._controlAdded.next(control); } } diff --git a/projects/common/form.ts b/projects/common/form.ts new file mode 100644 index 000000000..0648b3bf2 --- /dev/null +++ b/projects/common/form.ts @@ -0,0 +1,11 @@ +import { FormControl } from '@angular/forms'; + +export interface ValueChangeElement { + id: string; + values: [unknown, unknown]; +} + +export interface FormControlElement { + id: string; + formControl: FormControl; +} diff --git a/projects/common/unit.ts b/projects/common/unit.ts index 4f6bc7886..09e532081 100644 --- a/projects/common/unit.ts +++ b/projects/common/unit.ts @@ -85,8 +85,3 @@ export interface AudioElement extends UnitUIElement { export interface VideoElement extends UnitUIElement { src: string; } - -export interface ChangeElement { - element: string; - values: [unknown, unknown]; -} diff --git a/projects/player/src/app/app.component.ts b/projects/player/src/app/app.component.ts index 388203bac..2e8d2e849 100644 --- a/projects/player/src/app/app.component.ts +++ b/projects/player/src/app/app.component.ts @@ -1,9 +1,10 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { FormControl, FormGroup } from '@angular/forms'; -import { ChangeElement, Unit } from '../../../common/unit'; +import { FormGroup } from '@angular/forms'; +import { Unit } from '../../../common/unit'; import { FormService } from '../../../common/form.service'; +import { FormControlElement, ValueChangeElement } from '../../../common/form'; interface StartData { unitDefinition: string; @@ -13,21 +14,23 @@ interface StartData { @Component({ selector: 'player-aspect', template: ` - <form *ngIf="form" [formGroup]="form"> + <form [formGroup]="form"> <mat-tab-group mat-align-tabs="start"> <mat-tab *ngFor="let page of unit.pages; let i = index" label="Seite {{i+1}}"> <app-page [parentForm]="form" [page]="page"></app-page> </mat-tab> </mat-tab-group> - <button class="form-item" mat-flat-button color="primary" (click)="submit()" [disabled]="!form.valid">Print - form.value - </button> - <pre>{{unit | json}}</pre> </form> + <button class="form-item" mat-flat-button color="primary" (click)="submit()">Print + form.value + </button> + <button class="form-item" mat-flat-button color="primary" (click)="markAsTouch()" >markAsTouch + </button> + <pre>{{unit | json}}</pre> ` }) export class AppComponent { - form!: FormGroup; + form: FormGroup = new FormGroup({}); unit: Unit = { pages: [] }; @@ -41,8 +44,10 @@ export class AppComponent { @Output() valueChanged = new EventEmitter<string>(); constructor(private formService: FormService) { - formService.elementValueChanged.subscribe((value: ChangeElement): void => this.onElementValueChanges(value)); - formService.controlAdded.subscribe((value: string): void => this.addControl(value)); + formService.elementValueChanged + .subscribe((value: ValueChangeElement): void => this.onElementValueChanges(value)); + formService.controlAdded + .subscribe((control: FormControlElement): void => this.addControl(control)); } private initForm(): void { @@ -50,24 +55,31 @@ export class AppComponent { this.form.valueChanges.subscribe(v => this.onFormChanges(v)); } - private addControl(id: string): void { - this.form.addControl(id, new FormControl()); + private addControl(control: FormControlElement): void { + this.form.addControl(control.id, control.formControl); } - private onElementValueChanges = (value: ChangeElement): void => { - console.log(`Player: onElementValueChanges - ${value.element}: ${value.values[0]} -> ${value.values[1]}`); + private onElementValueChanges = (value: ValueChangeElement): void => { + // eslint-disable-next-line no-console + console.log(`Player: onElementValueChanges - ${value.id}: ${value.values[0]} -> ${value.values[1]}`); }; private onFormChanges(value: unknown): void { const allValues: string = JSON.stringify(value); + // eslint-disable-next-line no-console console.log('Player: emit valueChanged', allValues); this.valueChanged.emit(allValues); } submit(): void { + // eslint-disable-next-line no-console console.log('Player: form.value', this.form.value); } + markAsTouch(): void { + this.form.markAllAsTouched(); + } + // exampleUnit = { // pages: [ // { diff --git a/projects/player/src/app/app.module.ts b/projects/player/src/app/app.module.ts index 4f3789549..6714b2e0e 100644 --- a/projects/player/src/app/app.module.ts +++ b/projects/player/src/app/app.module.ts @@ -9,13 +9,15 @@ import { PageComponent } from './components/page.component'; import { SectionComponent } from './components/section.component'; import { SharedModule } from '../../../common/app.module'; import { ElementOverlayComponent } from './components/element-overlay.component'; +import { ValidationMessageComponent } from './components/validation-message.component'; @NgModule({ declarations: [ AppComponent, PageComponent, SectionComponent, - ElementOverlayComponent + ElementOverlayComponent, + ValidationMessageComponent ], imports: [ BrowserModule, diff --git a/projects/player/src/app/components/element-overlay.component.ts b/projects/player/src/app/components/element-overlay.component.ts index 44f4e33b9..7a7431d11 100644 --- a/projects/player/src/app/components/element-overlay.component.ts +++ b/projects/player/src/app/components/element-overlay.component.ts @@ -1,35 +1,53 @@ import { - Component, ComponentFactoryResolver, Input, OnInit, ViewChild, ViewContainerRef + Component, ComponentFactory, ComponentFactoryResolver, ComponentRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { UnitUIElement } from '../../../../common/unit'; import * as ComponentUtils from '../../../../common/component-utils'; import { FormElementComponent } from '../../../../common/form-element-component.directive'; +import { ValidationMessageComponent } from './validation-message.component'; @Component({ selector: 'app-element-overlay', template: ` <div [style.position]="'absolute'" - [style.left.px]="element.xPosition" - [style.top.px]="element.yPosition"> - <ng-template #elementContainer></ng-template> + [style.left.px]="elementModel.xPosition" + [style.top.px]="elementModel.yPosition"> + <ng-template #elementComponentContainer></ng-template> + <ng-template #errorMessageComponentContainer></ng-template> </div> ` }) + export class ElementOverlayComponent implements OnInit { - @Input() element!: UnitUIElement; - @ViewChild('elementContainer', { read: ViewContainerRef, static: true }) private elementContainer!: ViewContainerRef; + @Input() elementModel!: UnitUIElement; + @ViewChild('elementComponentContainer', + { read: ViewContainerRef, static: true }) private elementComponentContainer!: ViewContainerRef; + + @ViewChild('validationMessageComponentContainer', + { read: ViewContainerRef, static: true }) private validationMessageComponentContainer!: ViewContainerRef; + parentForm!: FormGroup; constructor(private componentFactoryResolver: ComponentFactoryResolver) { } ngOnInit(): void { - const componentFactory = ComponentUtils.getComponentFactory(this.element.type, this.componentFactoryResolver); - const childComponent = this.elementContainer.createComponent(componentFactory).instance; - childComponent.elementModel = this.element; + // eslint-disable-next-line max-len + const elementComponentFactory = + ComponentUtils.getComponentFactory(this.elementModel.type, this.componentFactoryResolver); + const elementComponent = this.elementComponentContainer.createComponent(elementComponentFactory).instance; + elementComponent.elementModel = this.elementModel; + + if (elementComponent instanceof FormElementComponent) { + elementComponent.parentForm = this.parentForm; + + const validationMessageComponentFactory: ComponentFactory<ValidationMessageComponent> = + this.componentFactoryResolver.resolveComponentFactory(ValidationMessageComponent); + const validationMessageComponentRef: ComponentRef<ValidationMessageComponent> = + this.validationMessageComponentContainer.createComponent(validationMessageComponentFactory); - if (childComponent instanceof FormElementComponent) { - childComponent.parentForm = this.parentForm; + validationMessageComponentRef.instance.parentForm = this.parentForm; + validationMessageComponentRef.instance.elementModel = this.elementModel; } } } diff --git a/projects/player/src/app/components/section.component.ts b/projects/player/src/app/components/section.component.ts index c7400882d..5af3385b6 100644 --- a/projects/player/src/app/components/section.component.ts +++ b/projects/player/src/app/components/section.component.ts @@ -33,7 +33,7 @@ export class SectionComponent implements OnInit { this.componentFactoryResolver.resolveComponentFactory(ElementOverlayComponent); const overlayRef: ComponentRef<ElementOverlayComponent> = this.elementContainer.createComponent(overlayFactory); - overlayRef.instance.element = element; + overlayRef.instance.elementModel = element; overlayRef.instance.parentForm = this.parentForm; } } diff --git a/projects/player/src/app/components/validation-message.component.ts b/projects/player/src/app/components/validation-message.component.ts new file mode 100644 index 000000000..70d7de24d --- /dev/null +++ b/projects/player/src/app/components/validation-message.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { UnitUIElement } from '../../../../common/unit'; + +@Component({ + selector: 'app-error-message', + template: ` + <mat-error *ngIf="formElementControl && formElementControl.touched && formElementControl.errors"> + {{requiredMessage}} + </mat-error> + ` +}) + +export class ValidationMessageComponent implements OnInit { + elementModel!: UnitUIElement; + parentForm!: FormGroup; + formElementControl!: FormControl; + + ngOnInit(): void { + this.formElementControl = this.parentForm.controls[this.elementModel.id] as FormControl; + } + + // eslint-disable-next-line class-methods-use-this + get requiredMessage(): string { + // TODO: get from elementModel + return 'Name is required'; + } +} -- GitLab