From 1be9609d30b2e3105ed1de1a19c088ee2c2fff47 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Thu, 22 Aug 2024 14:47:47 +0200 Subject: [PATCH] load internal value and validate formcontroll on init --- .../components/dropdown/dropdown.component.ts | 197 ++++++++++++------ 1 file changed, 135 insertions(+), 62 deletions(-) diff --git a/frontend/src/app/shared/components/dropdown/dropdown.component.ts b/frontend/src/app/shared/components/dropdown/dropdown.component.ts index 8bf8a41..08869e8 100644 --- a/frontend/src/app/shared/components/dropdown/dropdown.component.ts +++ b/frontend/src/app/shared/components/dropdown/dropdown.component.ts @@ -1,15 +1,16 @@ import { CommonModule } from '@angular/common'; import { Component, - ElementRef, forwardRef, - HostListener, input, InputSignal, output, OutputEmitterRef, signal, WritableSignal, + OnInit, + HostListener, + ElementRef, } from '@angular/core'; import { AbstractControl, @@ -39,29 +40,79 @@ import { }, ], }) -export class DropdownComponent implements ControlValueAccessor, Validator { +export class DropdownComponent + implements ControlValueAccessor, Validator, OnInit +{ + // Input properties + /** + * The label text for the dropdown. + */ public label: InputSignal = input.required(); + /** + * The error message to display when the dropdown selection is invalid. + */ public errorMessage: InputSignal = input.required(); + /** + * The placeholder text for the dropdown input. + */ public placeholder: InputSignal = input.required(); + /** + * The list of items to display in the dropdown. + */ public items: InputSignal = input.required(); + /** + * The text to display when the dropdown list is empty. + */ public emptyStateText: InputSignal = input(''); + /** + * The text to display as a divider in the dropdown list. + */ public dividerText: InputSignal = input(''); + /** + * The text to display for adding a new item to the dropdown list. + */ public newItemText: InputSignal = input(''); + /** + * Whether the dropdown selection is required. + */ public required: InputSignal = input(false); + /** + * Hint text to display in the top right of the dropdown. + */ public hintTopRight: InputSignal = input(''); + /** + * Hint text to display in the bottom left of the dropdown. + */ public hintBottomLeft: InputSignal = input(''); + /** + * Hint text to display in the bottom right of the dropdown. + */ public hintBottomRight: InputSignal = input(''); + // Output properties + /** + * Event emitted when an item is selected from the dropdown. + */ + public itemSelected: OutputEmitterRef = output(); + /** + * Event emitted when a new item is submitted to be added to the dropdown. + */ + public submitNewItems: OutputEmitterRef = output(); + /** + * Event emitted when an item is selected for editing. + */ + public itemEdit: OutputEmitterRef = output(); + // Internal state public searchTerm: WritableSignal = signal(''); public showDropdown: WritableSignal = signal(false); public filteredItems: WritableSignal = signal([]); public selectedItem: WritableSignal = signal( null ); - public itemSelected: OutputEmitterRef = output(); - public submitNewItems: OutputEmitterRef = output(); - public itemEdit: OutputEmitterRef = output(); public touched: WritableSignal = signal(false); private isMouseInDropdown: WritableSignal = signal(false); + private internalValue: WritableSignal = signal( + null + ); public constructor(private readonly elementRef: ElementRef) {} @@ -72,15 +123,13 @@ export class DropdownComponent implements ControlValueAccessor, Validator { } } + public ngOnInit(): void { + this.updateStateFromInternalValue(); + } + public writeValue(value: string | null): void { - if (value && this.isValidOption(value)) { - this.searchTerm.set(value); - this.selectedItem.set(value); - } else { - this.searchTerm.set(''); - this.selectedItem.set(null); - } - this.touched.set(false); + this.internalValue.set(value); + this.updateStateFromInternalValue(); } public registerOnChange(fn: (value: string) => void): void { @@ -106,19 +155,6 @@ export class DropdownComponent implements ControlValueAccessor, Validator { return null; } - public onDropdownMouseEnter(): void { - this.isMouseInDropdown.set(true); - } - - public onDropdownMouseLeave(): void { - this.isMouseInDropdown.set(false); - } - - public closeDropdown(): void { - this.showDropdown.set(false); - this.isMouseInDropdown.set(false); - } - public toggleDropdown(event: MouseEvent): void { event.preventDefault(); event.stopPropagation(); @@ -127,10 +163,8 @@ export class DropdownComponent implements ControlValueAccessor, Validator { this.showDropdown.set(newDropdownState); if (newDropdownState) { - // Wenn das Dropdown geöffnet wird, aktualisieren wir die filteredItems this.updateFilteredItems(); } else { - // Wenn wir das Dropdown schließen, verhindern wir, dass das Eingabefeld den Fokus erhält (event.target as HTMLElement).blur(); } } @@ -147,32 +181,43 @@ export class DropdownComponent implements ControlValueAccessor, Validator { if (exactMatch) { this.selectedItem.set(exactMatch); + this.internalValue.set(exactMatch); this.onChange(exactMatch); this.itemSelected.emit(exactMatch); } else { this.selectedItem.set(null); + this.internalValue.set(null); this.onChange(''); this.itemSelected.emit(''); } this.showDropdown.set(true); - this.onTouched(); + this.markAsTouched(); } - public getInputClass(): string { - if (this.touched()) { - if (this.selectedItem()) { - return 'input-success'; - } else if ( - this.searchTerm().trim() !== '' && - !this.isValidOption(this.searchTerm()) - ) { - return 'input-error'; - } else if (this.required() && this.searchTerm().trim() === '') { - return 'input-error'; - } + public selectItem(item: string): void { + if (this.isValidOption(item)) { + this.searchTerm.set(item); + this.selectedItem.set(item); + this.internalValue.set(item); + this.closeDropdown(); + this.itemSelected.emit(item); + this.onChange(item); + this.markAsTouched(); } - return ''; + } + + public submitNewItem(): void { + this.closeDropdown(); + this.submitNewItems.emit(true); + } + + public onInputClear(): void { + this.searchTerm.set(''); + this.selectedItem.set(null); + this.internalValue.set(null); + this.onChange(''); + this.markAsTouched(); } public markAsTouched(): void { @@ -185,31 +230,27 @@ export class DropdownComponent implements ControlValueAccessor, Validator { public editItem(item: string, event: MouseEvent): void { event.preventDefault(); event.stopPropagation(); - // TODO: Implement edit item functionality this.itemEdit.emit(item); } - public selectItem(item: string): void { - if (this.isValidOption(item)) { - this.searchTerm.set(item); - this.selectedItem.set(item); - this.closeDropdown(); - this.itemSelected.emit(item); - this.onChange(item); - this.onTouched(); + public getInputClass(): string { + if (!this.touched() && !this.hasValidValue()) { + return ''; } - } - public submitNewItem(): void { - this.closeDropdown(); - this.submitNewItems.emit(true); - } + const validationResult = this.validate({ + value: this.internalValue(), + } as AbstractControl); - public onInputClear(): void { - this.searchTerm.set(''); - this.selectedItem.set(null); - this.onChange(''); - this.onTouched(); + if (validationResult === null) { + return 'input-success'; + } else if ( + validationResult['required'] || + validationResult['invalidOption'] + ) { + return 'input-error'; + } + return ''; } public onFocus(): void { @@ -227,6 +268,14 @@ export class DropdownComponent implements ControlValueAccessor, Validator { }, 1); } + public onDropdownMouseEnter(): void { + this.isMouseInDropdown.set(true); + } + + public onDropdownMouseLeave(): void { + this.isMouseInDropdown.set(false); + } + private isValidOption(value: string): boolean { return this.items().includes(value); } @@ -240,6 +289,30 @@ export class DropdownComponent implements ControlValueAccessor, Validator { this.filteredItems.set(matchingItems); } + private updateStateFromInternalValue(): void { + const value = this.internalValue(); + + if (value && this.isValidOption(value)) { + this.searchTerm.set(value); + this.selectedItem.set(value); + } else { + this.searchTerm.set(''); + this.selectedItem.set(null); + } + this.updateFilteredItems(); + } + + private hasValidValue(): boolean { + const value = this.internalValue(); + + return value !== null && this.isValidOption(value); + } + + private closeDropdown(): void { + this.showDropdown.set(false); + this.isMouseInDropdown.set(false); + } + private onChange: (value: string) => void = () => {}; private onTouched: () => void = () => {}; }