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