Skip to content

Commit 0c0f095

Browse files
lorenloren
authored andcommitted
Add external validation support to BlnAutocompleteSelect and enhance FormBuilder with validator support for autocomplete select fields
1 parent ebadd1b commit 0c0f095

3 files changed

Lines changed: 113 additions & 29 deletions

File tree

src/components/BlnAutocompleteSelect.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export interface BlnAutocompleteSelectProps {
2727
minSearchChars: number;
2828
noResultsText: string;
2929
loadingText: string;
30+
/** Optional externe Validierungsfunktion. Nur als Property (nicht als Attribut) setzbar. */
31+
validator?: (value: string | string[], el: BlnAutocompleteSelect) => boolean | { valid: boolean; message?: string };
32+
3033
}
3134

3235
@customElement("bln-autocomplete-select")
@@ -63,6 +66,9 @@ export class BlnAutocompleteSelect extends TailwindElement {
6366

6467
// Options (programmatisch gesetzt)
6568
@property({attribute: false}) options: BlnAutocompleteSelectProps["options"] = [];
69+
/** Externe Validierungsfunktion: nur als Property setzbar (attribute: false). */
70+
@property({attribute: false}) validator?: BlnAutocompleteSelectProps['validator'];
71+
6672

6773
// Internal state
6874
@state() private _searchTerm = "";
@@ -81,8 +87,40 @@ export class BlnAutocompleteSelect extends TailwindElement {
8187
if (changed.has('options') || changed.has('_searchTerm')) {
8288
this.updateFilteredOptions();
8389
}
90+
if (changed.has('value')) {
91+
// When value changes programmatically, also re-run validation if provided
92+
this.runValidation();
93+
}
8494
}
8595

96+
private runValidation() {
97+
if (!this.validator) return;
98+
99+
try {
100+
const result = this.validator(this.value, this);
101+
if (typeof result === 'boolean') {
102+
this.isValid = result;
103+
} else if (result && typeof result === 'object') {
104+
this.isValid = result.valid;
105+
// Optional: Fehlermeldung im hint oder error anzeigen
106+
// if (result.message) { ... }
107+
}
108+
this.dispatchEvent(new CustomEvent('validitychange', {
109+
detail: {
110+
valid: this.isValid,
111+
value: this.value
112+
},
113+
bubbles: true,
114+
composed: true
115+
}));
116+
} catch (e) {
117+
console.error('Validation error:', e);
118+
this.isValid = false;
119+
}
120+
}
121+
122+
123+
86124
private updateFilteredOptions() {
87125
if (!this._searchTerm || this._searchTerm.length < this.minSearchChars) {
88126
this._filteredOptions = [...this.options];
@@ -165,10 +203,13 @@ export class BlnAutocompleteSelect extends TailwindElement {
165203
this.closeDropdown();
166204
}
167205

206+
// Run external validation if provided
207+
this.runValidation();
168208
this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
169209
this.dispatchEvent(new Event("input", {bubbles: true, composed: true}));
170210
}
171211

212+
172213
private closeDropdown() {
173214
this._isOpen = false;
174215
this._focusedIndex = -1;
@@ -216,10 +257,13 @@ export class BlnAutocompleteSelect extends TailwindElement {
216257
e.stopPropagation();
217258
if (Array.isArray(this.value)) {
218259
this.value = this.value.filter(v => v !== optionValue);
260+
// Run external validation if provided
261+
this.runValidation();
219262
this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
220263
}
221264
}
222265

266+
223267
protected render(): TemplateResult {
224268
const describedBy = [
225269
this.ariaDescribedby || "",

src/components/FormBuilder.ts

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -318,36 +318,37 @@ class FormBuilder {
318318
return this;
319319
}
320320

321-
// Convenience: add a BlnAutocompleteSelect
322-
addBlnAutocompleteSelect(props: Partial<BlnAutocompleteSelectProps> = {}) {
323-
const tpl = html`<bln-autocomplete-select
324-
.label=${props.label ?? ''}
325-
.cornerHint=${props.cornerHint ?? ''}
326-
.hint=${props.hint ?? ''}
327-
.name=${props.name ?? ''}
328-
.placeholder=${props.placeholder ?? ''}
329-
.searchPlaceholder=${props.searchPlaceholder ?? 'Suchen...'}
330-
.value=${props.value ?? ''}
331-
.disabled=${props.disabled ?? false}
332-
.required=${props.required ?? false}
333-
.multiple=${props.multiple ?? false}
334-
.size=${props.size ?? 'medium'}
335-
.class=${props.class ?? ''}
336-
.isValid=${props.isValid ?? undefined}
337-
.retroDesign=${props.retroDesign ?? false}
338-
.minSearchChars=${props.minSearchChars ?? 1}
339-
.noResultsText=${props.noResultsText ?? 'Keine Ergebnisse gefunden'}
340-
.loadingText=${props.loadingText ?? 'Laden...'}
341-
.options=${props.options ?? []}
342-
.ariaLabel=${props.ariaLabel ?? ''}
343-
.ariaLabelledby=${props.ariaLabelledby ?? ''}
344-
.ariaDescribedby=${props.ariaDescribedby ?? ''}
345-
></bln-autocomplete-select>`;
346-
this.fields.push(tpl);
347-
return this;
348-
}
321+
addBlnAutocompleteSelect(props: Partial<BlnAutocompleteSelectProps> = {}) {
322+
const tpl = html`<bln-autocomplete-select
323+
.label=${props.label ?? ''}
324+
.cornerHint=${props.cornerHint ?? ''}
325+
.hint=${props.hint ?? ''}
326+
.name=${props.name ?? ''}
327+
.placeholder=${props.placeholder ?? ''}
328+
.searchPlaceholder=${props.searchPlaceholder ?? 'Suchen...'}
329+
.value=${props.value ?? ''}
330+
.disabled=${props.disabled ?? false}
331+
.required=${props.required ?? false}
332+
.multiple=${props.multiple ?? false}
333+
.size=${props.size ?? 'medium'}
334+
.class=${props.class ?? ''}
335+
.isValid=${props.isValid ?? undefined}
336+
.retroDesign=${props.retroDesign ?? false}
337+
.minSearchChars=${props.minSearchChars ?? 1}
338+
.noResultsText=${props.noResultsText ?? 'Keine Ergebnisse gefunden'}
339+
.loadingText=${props.loadingText ?? 'Laden...'}
340+
.options=${props.options ?? []}
341+
.ariaLabel=${props.ariaLabel ?? ''}
342+
.ariaLabelledby=${props.ariaLabelledby ?? ''}
343+
.ariaDescribedby=${props.ariaDescribedby ?? ''}
344+
.validator=${props.validator ?? undefined}
345+
></bln-autocomplete-select>`;
346+
this.fields.push(tpl);
347+
return this;
348+
}
349+
349350

350-
// Convenience: add a BlnButton
351+
// Convenience: add a BlnButton
351352
addBlnButton(label: string, props: Partial<BlnButtonProps> = {}, onClick?: (e: MouseEvent) => void) {
352353
const tpl = html`<bln-button
353354
.variant=${(props.variant as any) ?? 'solid'}

src/stories/Configure.mdx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,6 +2297,45 @@ export const RightArrow = () => <svg
22972297
[zum Inhaltsverzeichnis](#start)
22982298
</div>
22992299
</div>
2300+
<div className="sb-container">
2301+
<div className='sb-section-title'>
2302+
<a name="blnModalDialog"></a>
2303+
# Ein vollständig zugänglicher modaler Dialog mit konfigurierbaren Eigenschaften.
2304+
2305+
**Hauptfunktionen:**
2306+
- Konfigurierbare Überschrift, Text, Eingabefeld und Buttons
2307+
- Event-System für Rückgabewerte und Benutzerinteraktionen
2308+
- Vollständige Barrierefreiheit (Fokusmanagement, Tastaturnavigation, ARIA)
2309+
- Modal-Hintergrund mit konfigurierbarem Verhalten
2310+
- Validierung für Eingabefelder
2311+
2312+
**Accessibility Features:**
2313+
- Fokus-Trap: Fokus bleibt innerhalb des Modals
2314+
- Tastaturnavigation: Escape, Tab, Enter
2315+
- ARIA-Attribute: role="dialog", aria-modal="true"
2316+
- Automatische Fokuswiederherstellung beim Schließen
2317+
- Screen Reader Unterstützung
2318+
2319+
**Event-System:**
2320+
- `open`: Modal wird geöffnet
2321+
- `close`: Modal wird geschlossen
2322+
- `button-click`: Button wurde geklickt (mit buttonId und inputValue)
2323+
- `input-change`: Eingabefeld wurde geändert
2324+
2325+
**Tastaturnavigation:**
2326+
- **Escape**: Modal schließen (wenn aktiviert)
2327+
- **Tab/Shift+Tab**: Zwischen fokussierbaren Elementen wechseln
2328+
- **Enter**: Primary Button aktivieren (wenn nicht im Eingabefeld)
2329+
2330+
2331+
Beispiel:
2332+
2333+
2334+
</div>
2335+
2336+
2337+
</div>
2338+
23002339
<div className="sb-container">
23012340
<div className='sb-section-title'>
23022341
<a name="formBuilder"></a>

0 commit comments

Comments
 (0)