Skip to content

Commit 73dbf08

Browse files
lorenloren
authored andcommitted
Add external validation support to BlnInput via validator property with validitychange event. Update tests and documentation with examples.
1 parent 5c49618 commit 73dbf08

File tree

3 files changed

+83
-0
lines changed

3 files changed

+83
-0
lines changed

src/components/BlnInput.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,34 @@ describe('<bln-input>', () => {
140140
expect(input.getAttribute('autocomplete')).toBe('email');
141141
});
142142
});
143+
144+
it('unterstützt externe Validierung über validator-Property', async () => {
145+
const el = await mount({ label: 'Test' }) as any;
146+
// Set validator as property (cannot be attribute)
147+
el.validator = (v: string) => ({ valid: v.length >= 3, message: 'Mindestens 3 Zeichen' });
148+
await el.updateComplete;
149+
const input = el.shadowRoot!.querySelector('input')! as HTMLInputElement;
150+
151+
// Empty -> neutral (no icons, no error)
152+
expect(el.isValid).toBeUndefined();
153+
expect(el.shadowRoot!.querySelector('lucide-icon[name="Check"]')).toBeFalsy();
154+
expect(el.shadowRoot!.querySelector('lucide-icon[name="CircleAlert"]')).toBeFalsy();
155+
156+
// Type 2 chars -> invalid
157+
input.value = 'ab';
158+
input.dispatchEvent(new Event('input', { bubbles: true }));
159+
await el.updateComplete;
160+
expect(el.isValid).toBe(false);
161+
// icon rendered
162+
expect(el.shadowRoot!.querySelector('lucide-icon[name="CircleAlert"]')).toBeTruthy();
163+
// error shown
164+
const err = el.shadowRoot!.querySelector('span[role="alert"]');
165+
expect(err?.textContent).toContain('Mindestens 3 Zeichen');
166+
167+
// Type 3 chars -> valid
168+
input.value = 'abc';
169+
input.dispatchEvent(new Event('input', { bubbles: true }));
170+
await el.updateComplete;
171+
expect(el.isValid).toBe(true);
172+
expect(el.shadowRoot!.querySelector('lucide-icon[name="Check"]')).toBeTruthy();
173+
});

src/components/BlnInput.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export interface BlnInputProps {
3434
ariaLabel: string;
3535
ariaLabelledby: string;
3636
ariaDescribedby: string;
37+
/** Optional externe Validierungsfunktion. Nur als Property (nicht als Attribut) setzbar. */
38+
validator?: (value: string, el: BlnInput) => boolean | { valid: boolean; message?: string };
3739
}
3840

3941
@customElement('bln-input')
@@ -60,6 +62,8 @@ export class BlnInput extends TailwindElement {
6062
@property() step: BlnInputProps['step'] = undefined as any;
6163
@property() inputmode: BlnInputProps['inputmode'] = undefined as any;
6264
@property() autocomplete: BlnInputProps['autocomplete'] = "";
65+
/** Externe Validierungsfunktion: nur als Property setzbar (attribute: false). */
66+
@property({attribute: false}) validator?: BlnInputProps['validator'];
6367

6468
// Sizing/Styles
6569
@property() size: BlnInputProps['size'] = "medium";
@@ -80,15 +84,22 @@ export class BlnInput extends TailwindElement {
8084
private onInput = (e: Event) => {
8185
const input = e.currentTarget as HTMLInputElement;
8286
this.value = input.value;
87+
// Run external validation if provided
88+
this.runValidation();
8389
this.dispatchEvent(new Event('input', {bubbles: true, composed: true}));
8490
};
8591

8692
private onChange = (_e: Event) => {
93+
this.runValidation();
8794
this.dispatchEvent(new Event('change', {bubbles: true, composed: true}));
8895
};
8996

9097
protected willUpdate(changed: Map<string, any>) {
9198
if (changed.has('isValid')) this._isValidSet = true;
99+
if (changed.has('value')) {
100+
// When value changes programmatically, also re-run validation if provided
101+
this.runValidation();
102+
}
92103
}
93104

94105
protected render() {
@@ -174,6 +185,32 @@ export class BlnInput extends TailwindElement {
174185
${this.error ? html`<span id="${this._errorId}" class="mt-1 text-sm text-red-600" role="alert" aria-live="polite">${this.error}</span>` : ''}
175186
</div>`;
176187
}
188+
private runValidation() {
189+
if (!this.validator) return;
190+
const v = (this.value ?? '');
191+
// Neutral state for empty unless validator explicitly handles it
192+
if (v === '') {
193+
// Clear validity and error for neutral display
194+
this.isValid = undefined as any;
195+
this.error = '' as any;
196+
return;
197+
}
198+
try {
199+
const res = this.validator(v, this as any);
200+
const result = typeof res === 'boolean' ? { valid: res } : res ?? { valid: true };
201+
this.isValid = result.valid as any;
202+
// If invalid and message provided, set error; otherwise clear error to avoid stale messages
203+
this.error = (!result.valid && result.message) ? result.message : (!result.valid ? (this.error || '') : '');
204+
// Ensure we show validity icons once validator ran
205+
this._isValidSet = true;
206+
this.dispatchEvent(new CustomEvent('validitychange', { detail: { isValid: this.isValid, message: this.error }, bubbles: true, composed: true }));
207+
} catch (e) {
208+
// On validator error, do not break UI; mark invalid with generic message
209+
this.isValid = false as any;
210+
this.error = this.error || 'Ungültiger Wert';
211+
this._isValidSet = true;
212+
}
213+
}
177214
}
178215

179216
const nothing = undefined;

src/stories/Configure.mdx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,21 @@ export const RightArrow = () => <svg
137137
- Styling: Utility-/Tailwind-Klassen via class und Retro-Design
138138
- Ereignisse: input, change, focus, blur, keydown, keyup
139139

140+
### Externe Validierung über eine Funktion
141+
Über die Property validator kann eine eigene Prüf-Funktion von außen gesetzt werden (nur als JS-Property, nicht als HTML-Attribut).
142+
Die Funktion wird bei Eingaben (input/change) und bei programmatischen Wertänderungen aufgerufen. Sie kann entweder
143+
true/false zurückgeben oder ein Objekt mit den Feldern valid (boolean) und message (string, optional).
144+
Bei leerem Wert bleibt der Zustand neutral (keine grünen/roten Icons). Ein Ereignis validitychange wird mit den aktuellen Werten ausgelöst.
145+
146+
Beispiel:
147+
```html
148+
<bln-input id="u" label="Username"></bln-input>
149+
<script type="module">{`
150+
const el = document.getElementById('u');
151+
el.validator = (v) => ({ valid: v.length >= 3, message: 'Mindestens 3 Zeichen' });
152+
`}</script>
153+
```
154+
140155
## Schnellstart
141156

142157
Ein einfaches Textfeld mit Label und Platzhalter:

0 commit comments

Comments
 (0)