load internal value and validate formcontroll on init

This commit is contained in:
Igor Hrenowitsch Propisnov 2024-08-22 14:47:47 +02:00
parent e141241aba
commit 1be9609d30
1 changed files with 135 additions and 62 deletions

View File

@ -1,15 +1,16 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
Component, Component,
ElementRef,
forwardRef, forwardRef,
HostListener,
input, input,
InputSignal, InputSignal,
output, output,
OutputEmitterRef, OutputEmitterRef,
signal, signal,
WritableSignal, WritableSignal,
OnInit,
HostListener,
ElementRef,
} from '@angular/core'; } from '@angular/core';
import { import {
AbstractControl, 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<string> = input.required<string>(); public label: InputSignal<string> = input.required<string>();
/**
* The error message to display when the dropdown selection is invalid.
*/
public errorMessage: InputSignal<string> = input.required<string>(); public errorMessage: InputSignal<string> = input.required<string>();
/**
* The placeholder text for the dropdown input.
*/
public placeholder: InputSignal<string> = input.required<string>(); public placeholder: InputSignal<string> = input.required<string>();
/**
* The list of items to display in the dropdown.
*/
public items: InputSignal<string[]> = input.required<string[]>(); public items: InputSignal<string[]> = input.required<string[]>();
/**
* The text to display when the dropdown list is empty.
*/
public emptyStateText: InputSignal<string> = input<string>(''); public emptyStateText: InputSignal<string> = input<string>('');
/**
* The text to display as a divider in the dropdown list.
*/
public dividerText: InputSignal<string> = input<string>(''); public dividerText: InputSignal<string> = input<string>('');
/**
* The text to display for adding a new item to the dropdown list.
*/
public newItemText: InputSignal<string> = input<string>(''); public newItemText: InputSignal<string> = input<string>('');
/**
* Whether the dropdown selection is required.
*/
public required: InputSignal<boolean> = input<boolean>(false); public required: InputSignal<boolean> = input<boolean>(false);
/**
* Hint text to display in the top right of the dropdown.
*/
public hintTopRight: InputSignal<string> = input<string>(''); public hintTopRight: InputSignal<string> = input<string>('');
/**
* Hint text to display in the bottom left of the dropdown.
*/
public hintBottomLeft: InputSignal<string> = input<string>(''); public hintBottomLeft: InputSignal<string> = input<string>('');
/**
* Hint text to display in the bottom right of the dropdown.
*/
public hintBottomRight: InputSignal<string> = input<string>(''); public hintBottomRight: InputSignal<string> = input<string>('');
// Output properties
/**
* Event emitted when an item is selected from the dropdown.
*/
public itemSelected: OutputEmitterRef<string> = output<string>();
/**
* Event emitted when a new item is submitted to be added to the dropdown.
*/
public submitNewItems: OutputEmitterRef<boolean> = output<boolean>();
/**
* Event emitted when an item is selected for editing.
*/
public itemEdit: OutputEmitterRef<string> = output<string>();
// Internal state
public searchTerm: WritableSignal<string> = signal(''); public searchTerm: WritableSignal<string> = signal('');
public showDropdown: WritableSignal<boolean> = signal<boolean>(false); public showDropdown: WritableSignal<boolean> = signal<boolean>(false);
public filteredItems: WritableSignal<string[]> = signal<string[]>([]); public filteredItems: WritableSignal<string[]> = signal<string[]>([]);
public selectedItem: WritableSignal<string | null> = signal<string | null>( public selectedItem: WritableSignal<string | null> = signal<string | null>(
null null
); );
public itemSelected: OutputEmitterRef<string> = output<string>();
public submitNewItems: OutputEmitterRef<boolean> = output<boolean>();
public itemEdit: OutputEmitterRef<string> = output<string>();
public touched: WritableSignal<boolean> = signal(false); public touched: WritableSignal<boolean> = signal(false);
private isMouseInDropdown: WritableSignal<boolean> = signal<boolean>(false); private isMouseInDropdown: WritableSignal<boolean> = signal<boolean>(false);
private internalValue: WritableSignal<string | null> = signal<string | null>(
null
);
public constructor(private readonly elementRef: ElementRef) {} public constructor(private readonly elementRef: ElementRef) {}
@ -72,15 +123,13 @@ export class DropdownComponent implements ControlValueAccessor, Validator {
} }
} }
public writeValue(value: string | null): void { public ngOnInit(): void {
if (value && this.isValidOption(value)) { this.updateStateFromInternalValue();
this.searchTerm.set(value);
this.selectedItem.set(value);
} else {
this.searchTerm.set('');
this.selectedItem.set(null);
} }
this.touched.set(false);
public writeValue(value: string | null): void {
this.internalValue.set(value);
this.updateStateFromInternalValue();
} }
public registerOnChange(fn: (value: string) => void): void { public registerOnChange(fn: (value: string) => void): void {
@ -106,19 +155,6 @@ export class DropdownComponent implements ControlValueAccessor, Validator {
return null; 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 { public toggleDropdown(event: MouseEvent): void {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -127,10 +163,8 @@ export class DropdownComponent implements ControlValueAccessor, Validator {
this.showDropdown.set(newDropdownState); this.showDropdown.set(newDropdownState);
if (newDropdownState) { if (newDropdownState) {
// Wenn das Dropdown geöffnet wird, aktualisieren wir die filteredItems
this.updateFilteredItems(); this.updateFilteredItems();
} else { } else {
// Wenn wir das Dropdown schließen, verhindern wir, dass das Eingabefeld den Fokus erhält
(event.target as HTMLElement).blur(); (event.target as HTMLElement).blur();
} }
} }
@ -147,32 +181,43 @@ export class DropdownComponent implements ControlValueAccessor, Validator {
if (exactMatch) { if (exactMatch) {
this.selectedItem.set(exactMatch); this.selectedItem.set(exactMatch);
this.internalValue.set(exactMatch);
this.onChange(exactMatch); this.onChange(exactMatch);
this.itemSelected.emit(exactMatch); this.itemSelected.emit(exactMatch);
} else { } else {
this.selectedItem.set(null); this.selectedItem.set(null);
this.internalValue.set(null);
this.onChange(''); this.onChange('');
this.itemSelected.emit(''); this.itemSelected.emit('');
} }
this.showDropdown.set(true); this.showDropdown.set(true);
this.onTouched(); this.markAsTouched();
} }
public getInputClass(): string { public selectItem(item: string): void {
if (this.touched()) { if (this.isValidOption(item)) {
if (this.selectedItem()) { this.searchTerm.set(item);
return 'input-success'; this.selectedItem.set(item);
} else if ( this.internalValue.set(item);
this.searchTerm().trim() !== '' && this.closeDropdown();
!this.isValidOption(this.searchTerm()) this.itemSelected.emit(item);
) { this.onChange(item);
return 'input-error'; this.markAsTouched();
} else if (this.required() && this.searchTerm().trim() === '') {
return 'input-error';
} }
} }
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 { public markAsTouched(): void {
@ -185,31 +230,27 @@ export class DropdownComponent implements ControlValueAccessor, Validator {
public editItem(item: string, event: MouseEvent): void { public editItem(item: string, event: MouseEvent): void {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
// TODO: Implement edit item functionality
this.itemEdit.emit(item); this.itemEdit.emit(item);
} }
public selectItem(item: string): void { public getInputClass(): string {
if (this.isValidOption(item)) { if (!this.touched() && !this.hasValidValue()) {
this.searchTerm.set(item); return '';
this.selectedItem.set(item);
this.closeDropdown();
this.itemSelected.emit(item);
this.onChange(item);
this.onTouched();
}
} }
public submitNewItem(): void { const validationResult = this.validate({
this.closeDropdown(); value: this.internalValue(),
this.submitNewItems.emit(true); } as AbstractControl);
}
public onInputClear(): void { if (validationResult === null) {
this.searchTerm.set(''); return 'input-success';
this.selectedItem.set(null); } else if (
this.onChange(''); validationResult['required'] ||
this.onTouched(); validationResult['invalidOption']
) {
return 'input-error';
}
return '';
} }
public onFocus(): void { public onFocus(): void {
@ -227,6 +268,14 @@ export class DropdownComponent implements ControlValueAccessor, Validator {
}, 1); }, 1);
} }
public onDropdownMouseEnter(): void {
this.isMouseInDropdown.set(true);
}
public onDropdownMouseLeave(): void {
this.isMouseInDropdown.set(false);
}
private isValidOption(value: string): boolean { private isValidOption(value: string): boolean {
return this.items().includes(value); return this.items().includes(value);
} }
@ -240,6 +289,30 @@ export class DropdownComponent implements ControlValueAccessor, Validator {
this.filteredItems.set(matchingItems); 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 onChange: (value: string) => void = () => {};
private onTouched: () => void = () => {}; private onTouched: () => void = () => {};
} }