From 9dc8dbbd98d4503ad331eaa7db5fa56d106056a9 Mon Sep 17 00:00:00 2001
From: jojohoch <joachim.hoch@iqb.hu-berlin.de>
Date: Fri, 21 Apr 2023 16:40:31 +0200
Subject: [PATCH] [player] Fix  ExpressionChangedAfterItHasBeenCheckedError in
 TextArea

- Replace UpdateTextareaPipe with DynamicRowsDirective

The dynamic height of the textarea is calculated based on the available
width. However, accessing the width of the textarea often results in an
error.
For this reason, the calculation is now done using resize and change
events in conjunction with a timeout to wait for the textarea to render.
---
 .../input-elements/text-area.component.ts     | 24 ++++------
 .../directives/dynamic-rows.directive.ts      | 44 +++++++++++++++++++
 .../pipes/update-textarea-rows.pipe.spec.ts   |  8 ----
 .../common/pipes/update-textarea-rows.pipe.ts | 11 -----
 projects/common/shared.module.ts              |  7 +--
 5 files changed, 56 insertions(+), 38 deletions(-)
 create mode 100644 projects/common/directives/dynamic-rows.directive.ts
 delete mode 100644 projects/common/pipes/update-textarea-rows.pipe.spec.ts
 delete mode 100644 projects/common/pipes/update-textarea-rows.pipe.ts

diff --git a/projects/common/components/input-elements/text-area.component.ts b/projects/common/components/input-elements/text-area.component.ts
index 773d62b88..50caa1061 100644
--- a/projects/common/components/input-elements/text-area.component.ts
+++ b/projects/common/components/input-elements/text-area.component.ts
@@ -1,8 +1,5 @@
-import {
-  Component, Input, AfterViewInit
-} from '@angular/core';
+import { Component, Input } from '@angular/core';
 import { TextAreaElement } from 'common/models/elements/input-elements/text-area';
-import { delay, Observable, of } from 'rxjs';
 import { TextInputComponent } from 'common/directives/text-input-component.directive';
 
 @Component({
@@ -33,11 +30,11 @@ import { TextInputComponent } from 'common/directives/text-input-component.direc
                 autocorrect="off"
                 spellcheck="false"
                 value="{{elementModel.value}}"
-                [rows]="(isViewInitialized | async) && elementModel.hasDynamicRowCount ?
-                            (elementModel.expectedCharactersCount | updateTextareaRows:
-                                input.offsetWidth:
-                                elementModel.styling.fontSize) :
-                            elementModel.rowCount"
+                dynamicRows
+                [expectedCharactersCount]="elementModel.expectedCharactersCount"
+                [fontSize]="elementModel.styling.fontSize"
+                (dynamicRowsChange)="dynamicRows = $event"
+                [rows]="elementModel.hasDynamicRowCount && dynamicRows ? dynamicRows : elementModel.rows"
                 [attr.inputmode]="elementModel.showSoftwareKeyboard ? 'none' : 'text'"
                 [formControl]="elementFormControl"
                 [readonly]="elementModel.readOnly"
@@ -59,12 +56,7 @@ import { TextInputComponent } from 'common/directives/text-input-component.direc
     ':host ::ng-deep .no-label .mat-form-field-outline-gap {border-top-color: unset !important}'
   ]
 })
-export class TextAreaComponent extends TextInputComponent implements AfterViewInit {
+export class TextAreaComponent extends TextInputComponent {
   @Input() elementModel!: TextAreaElement;
-
-  isViewInitialized: Observable<boolean> = of(false);
-
-  ngAfterViewInit(): void {
-    this.isViewInitialized = of(true).pipe(delay(0));
-  }
+  dynamicRows: number = 0;
 }
diff --git a/projects/common/directives/dynamic-rows.directive.ts b/projects/common/directives/dynamic-rows.directive.ts
new file mode 100644
index 000000000..15224f3e7
--- /dev/null
+++ b/projects/common/directives/dynamic-rows.directive.ts
@@ -0,0 +1,44 @@
+import {
+  AfterViewInit, Directive, ElementRef, EventEmitter, HostListener, Input, OnChanges, Output, SimpleChanges
+} from '@angular/core';
+
+@Directive({
+  selector: '[dynamicRows]'
+})
+export class DynamicRowsDirective implements AfterViewInit, OnChanges {
+  @Input() fontSize!: number;
+  @Input() expectedCharactersCount!: number;
+  @Output() dynamicRowsChange: EventEmitter<number> = new EventEmitter<number>();
+
+  @HostListener('window:resize') onResize() {
+    // guard against resize before view is rendered
+    this.calculateDynamicRows();
+  }
+
+  constructor(public elementRef: ElementRef) {}
+
+  ngAfterViewInit(): void {
+    this.calculateDynamicRows();
+  }
+
+  calculateDynamicRows(): void {
+    // give the textarea time to render before calculating the dynamic row count
+    setTimeout(() => {
+      const averageCharWidth = this.fontSize / 2;
+      if (this.elementRef.nativeElement.offsetWidth) {
+        this.dynamicRowsChange.emit(
+          Math.ceil((
+            this.expectedCharactersCount * averageCharWidth) /
+            this.elementRef.nativeElement.offsetWidth
+          )
+        );
+      }
+    });
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.fontSize || changes.expectedCharactersCount) {
+      this.calculateDynamicRows();
+    }
+  }
+}
diff --git a/projects/common/pipes/update-textarea-rows.pipe.spec.ts b/projects/common/pipes/update-textarea-rows.pipe.spec.ts
deleted file mode 100644
index 0c99a0bbe..000000000
--- a/projects/common/pipes/update-textarea-rows.pipe.spec.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { UpdateTextareaRowsPipe } from './update-textarea-rows.pipe';
-
-describe('UpdateRowsPipe', () => {
-  it('create an instance', () => {
-    const pipe = new UpdateTextareaRowsPipe();
-    expect(pipe).toBeTruthy();
-  });
-});
diff --git a/projects/common/pipes/update-textarea-rows.pipe.ts b/projects/common/pipes/update-textarea-rows.pipe.ts
deleted file mode 100644
index 25b658cc7..000000000
--- a/projects/common/pipes/update-textarea-rows.pipe.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
-  name: 'updateTextareaRows'
-})
-export class UpdateTextareaRowsPipe implements PipeTransform {
-  transform(expectedCharactersCount: number, inputWidth: number, fontSize: number): number {
-    const averageCharWidth = fontSize / 2; // s. AverageCharWidth of dotNet
-    return Math.ceil((expectedCharactersCount * averageCharWidth) / inputWidth);
-  }
-}
diff --git a/projects/common/shared.module.ts b/projects/common/shared.module.ts
index 5abb50f1f..410036416 100644
--- a/projects/common/shared.module.ts
+++ b/projects/common/shared.module.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line max-classes-per-file
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
 
@@ -25,6 +26,7 @@ import { HotspotImageComponent } from 'common/components/input-elements/hotspot-
 import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
 import { ScrollPagesPipe } from 'common/pipes/scroll-pages.pipe';
 import { MathEditorModule } from 'common/math-editor.module';
+import { DynamicRowsDirective } from 'common/directives/dynamic-rows.directive';
 import { TextComponent } from './components/text/text.component';
 import { ButtonComponent } from './components/button/button.component';
 import { TextFieldComponent } from './components/input-elements/text-field.component';
@@ -71,7 +73,6 @@ import { GeometryComponent } from './components/geometry/geometry.component';
 import { MathAtanPipe } from './pipes/math-atan.pipe';
 import { MathDegreesPipe } from './pipes/math-degrees.pipe';
 import { ArrayIncludesPipe } from './pipes/array-includes.pipe';
-import { UpdateTextareaRowsPipe } from './pipes/update-textarea-rows.pipe';
 import { SpinnerComponent } from './components/spinner/spinner.component';
 import { GetValuePipe, MathFieldComponent } from './components/input-elements/math-field.component';
 
@@ -138,10 +139,10 @@ import { GetValuePipe, MathFieldComponent } from './components/input-elements/ma
     MathAtanPipe,
     MathDegreesPipe,
     ArrayIncludesPipe,
-    UpdateTextareaRowsPipe,
     SpinnerComponent,
     GetValuePipe,
-    MathFieldComponent
+    MathFieldComponent,
+    DynamicRowsDirective
   ],
   exports: [
     CommonModule,
-- 
GitLab