Skip to content

Commit 7a73cf2

Browse files
committed
fix(input): update validity on input number increment
1 parent 34e5d82 commit 7a73cf2

File tree

6 files changed

+106
-40
lines changed

6 files changed

+106
-40
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
},
112112
"dependencies": {
113113
"@dvcol/common-utils": "^1.18.3",
114-
"@dvcol/svelte-utils": "^1.3.0",
114+
"@dvcol/svelte-utils": "^1.4.0",
115115
"svelte": "^5.1.9",
116116
"vite": "^5.4.10"
117117
},

pnpm-lock.yaml

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/inputs/NeoInput.svelte

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import NeoValidation from '~/inputs/NeoValidation.svelte';
1212
import {
1313
type NeoInputContext,
14+
type NeoInputHTMLElement,
1415
NeoInputLabelPosition,
1516
type NeoInputMethods,
1617
type NeoInputProps,
@@ -38,7 +39,7 @@
3839
// States
3940
id = label ? `neo-input-${crypto.randomUUID()}` : undefined,
4041
ref = $bindable(),
41-
value = $bindable(''),
42+
value = $bindable(undefined),
4243
valid = $bindable(undefined),
4344
dirty = $bindable(false),
4445
touched = $bindable(false),
@@ -130,20 +131,23 @@
130131
onblur?.(e);
131132
};
132133
133-
const checkValidity = (update: { dirty?: boolean; valid?: boolean } = { dirty: true, valid: true }) => {
134+
const validate: NeoInputMethods['validate'] = (update: { dirty?: boolean; valid?: boolean } = { dirty: true, valid: true }) => {
134135
if (update.dirty) dirty = value !== initial;
135-
if (!update.valid) return;
136+
if (!update.valid) return { touched, dirty, valid, value, initial };
136137
valid = ref?.checkValidity();
137138
validationMessage = ref?.validationMessage;
139+
return { touched, dirty, valid, value, initial };
138140
};
139141
140142
const onInput: FormEventHandler<HTMLInputElement> = e => {
141-
checkValidity({ dirty: dirtyOnInput, valid: validateOnInput });
143+
touched = true;
144+
validate({ dirty: dirtyOnInput, valid: validateOnInput });
142145
oninput?.(e);
146+
console.info('onInput', { touched, dirty, valid, value, initial });
143147
};
144148
145149
const onChange: FormEventHandler<HTMLInputElement> = e => {
146-
checkValidity();
150+
validate();
147151
onchange?.(e);
148152
};
149153
@@ -161,10 +165,10 @@
161165
export const mark: NeoInputMethods['mark'] = (state: NeoInputState) => {
162166
if (state.touched !== undefined) touched = state.touched;
163167
if (state.valid !== undefined) valid = state.valid;
164-
if (state.dirty === undefined) return onmark?.({ touched, dirty, valid, value });
168+
if (state.dirty === undefined) return onmark?.({ touched, dirty, valid, value, initial });
165169
dirty = state.dirty;
166170
if (!dirty) initial = value;
167-
return onmark?.({ touched, dirty, valid, value });
171+
return onmark?.({ touched, dirty, valid, value, initial });
168172
};
169173
170174
const focus = () => {
@@ -175,18 +179,34 @@
175179
/**
176180
* Clear the input state
177181
*/
178-
export const clear: NeoInputMethods['clear'] = (state?: NeoInputState) => {
182+
export const clear: NeoInputMethods['clear'] = (state?: NeoInputState, event?: InputEvent) => {
183+
if (event) ref?.dispatchEvent(event);
179184
value = '';
180185
focus();
181186
if (!state) {
182-
setTimeout(() => checkValidity());
183-
return onclear?.({ touched, dirty, valid, value });
187+
setTimeout(() => validate());
188+
return onclear?.({ touched, dirty, valid, value, initial }, event);
184189
}
185190
initial = value;
186191
setTimeout(() => mark({ touched: false, dirty: false, ...state }));
187-
return onclear?.({ touched, dirty, valid, value });
192+
return onclear?.({ touched, dirty, valid, value, initial }, event);
188193
};
189194
195+
/**
196+
* Change the value of the input
197+
*/
198+
export const change: NeoInputMethods['change'] = (_value: HTMLInputElement['value'], event?: InputEvent) => {
199+
if (event) ref?.dispatchEvent(event);
200+
value = _value;
201+
focus();
202+
return validate();
203+
};
204+
205+
$effect(() => {
206+
if (!ref) return;
207+
Object.assign(ref, { mark, clear, change, validate });
208+
});
209+
190210
const hasValue = $derived(value !== undefined && (typeof value === 'string' ? !!value.length : value !== null));
191211
const affix = $derived(clearable || loading !== undefined || validation);
192212
const close = $derived(clearable && (focused || hovered) && hasValue && !disabled && !readonly);
@@ -225,15 +245,18 @@
225245
const showMessage = $derived(message || errorMessage || error || validation);
226246
const messageId = $derived(showMessage ? (messageProps?.id ?? `neo-input-message-${crypto.randomUUID()}`) : undefined);
227247
228-
const context = $derived<NeoInputContext<HTMLInputElement>>({
248+
const context = $derived<NeoInputContext<NeoInputHTMLElement>>({
229249
// Ref
230250
ref,
231251
232252
// Methods
233253
mark,
234254
clear,
255+
change,
256+
validate,
235257
236258
// State
259+
initial,
237260
value,
238261
touched,
239262
dirty,

src/lib/inputs/NeoNumberStep.svelte

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
let {
1515
// State
1616
ref = $bindable(),
17-
value = $bindable(''),
17+
value = $bindable(0),
1818
valid = $bindable(undefined),
1919
dirty = $bindable(false),
2020
touched = $bindable(false),
@@ -87,6 +87,10 @@
8787
text,
8888
style,
8989
...rest?.buttonProps,
90+
onblur: (e: FocusEvent & { currentTarget: EventTarget & any }) => {
91+
ref?.validate?.();
92+
rest?.buttonProps?.onblur?.(e);
93+
},
9094
});
9195
9296
const affix = $derived(rest.clearable || rest.loading !== undefined || rest.validation);
@@ -127,7 +131,10 @@
127131

128132
<style lang="scss">
129133
.neo-number-step {
130-
:global(.neo-input) {
134+
:global(.neo-input[type='number']) {
135+
/* Hide arrows -Firefox */
136+
appearance: textfield;
137+
131138
/* Hide arrows - Chrome, Safari, Edge, Opera */
132139
&::-webkit-outer-spin-button,
133140
&::-webkit-inner-spin-button {
@@ -136,11 +143,6 @@
136143
}
137144
}
138145
139-
/* Hide arrows -Firefox */
140-
:global(.neo-input[type='number']) {
141-
appearance: textfield;
142-
}
143-
144146
&:not(.neo-label) {
145147
:global(.neo-input) {
146148
text-align: center;

src/lib/inputs/NeoTextarea.svelte

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
NeoInputLabelPosition,
1515
type NeoInputMethods,
1616
type NeoInputState,
17+
type NeoTextareaHTMLElement,
1718
type NeoTextareaProps,
1819
} from '~/inputs/neo-input.model.js';
1920
import { toAction, toActionProps, toTransition, toTransitionProps } from '~/utils/action.utils.js';
@@ -37,7 +38,7 @@
3738
// States
3839
id = label ? `neo-textarea-${crypto.randomUUID()}` : undefined,
3940
ref = $bindable(),
40-
value = $bindable(''),
41+
value = $bindable(undefined),
4142
valid = $bindable(undefined),
4243
dirty = $bindable(false),
4344
touched = $bindable(false),
@@ -127,20 +128,22 @@
127128
onblur?.(e);
128129
};
129130
130-
const checkValidity = (update: { dirty?: boolean; valid?: boolean } = { dirty: true, valid: true }) => {
131+
const validate: NeoInputMethods['validate'] = (update: { dirty?: boolean; valid?: boolean } = { dirty: true, valid: true }) => {
131132
if (update.dirty) dirty = value !== initial;
132-
if (!update.valid) return;
133+
if (!update.valid) return { touched, dirty, valid, value, initial };
133134
valid = ref?.checkValidity();
134135
validationMessage = ref?.validationMessage;
136+
return { touched, dirty, valid, value };
135137
};
136138
137139
const onInput: FormEventHandler<HTMLTextAreaElement> = e => {
138-
checkValidity({ dirty: dirtyOnInput, valid: validateOnInput });
140+
touched = true;
141+
validate({ dirty: dirtyOnInput, valid: validateOnInput });
139142
oninput?.(e);
140143
};
141144
142145
const onChange: FormEventHandler<HTMLTextAreaElement> = e => {
143-
checkValidity();
146+
validate();
144147
onchange?.(e);
145148
};
146149
@@ -172,18 +175,33 @@
172175
/**
173176
* Clear the input state
174177
*/
175-
export const clear: NeoInputMethods['clear'] = (state?: NeoInputState) => {
178+
export const clear: NeoInputMethods['clear'] = (state?: NeoInputState, event?: InputEvent) => {
176179
value = '';
177180
focus();
178181
if (!state) {
179-
setTimeout(() => checkValidity());
180-
return onclear?.({ touched, dirty, valid, value });
182+
setTimeout(() => validate());
183+
return onclear?.({ touched, dirty, valid, value }, event);
181184
}
182185
initial = value;
183186
setTimeout(() => mark({ touched: false, dirty: false, ...state }));
184-
return onclear?.({ touched, dirty, valid, value });
187+
return onclear?.({ touched, dirty, valid, value }, event);
188+
};
189+
190+
/**
191+
* Change the value of the input
192+
*/
193+
export const change: NeoInputMethods['change'] = (_value: HTMLInputElement['value'], event?: InputEvent) => {
194+
if (event) ref?.dispatchEvent(event);
195+
value = _value;
196+
focus();
197+
return validate();
185198
};
186199
200+
$effect(() => {
201+
if (!ref) return;
202+
Object.assign(ref, { mark, clear, change, validate });
203+
});
204+
187205
const affix = $derived(clearable || loading !== undefined || validation);
188206
const close = $derived(clearable && (focused || hovered) && value?.length && !disabled && !readonly);
189207
const isFloating = $derived(floating && !focused && !value?.length && !disabled && !readonly);
@@ -255,13 +273,15 @@
255273
const showMessage = $derived(message || errorMessage || error || validation);
256274
const messageId = $derived(showMessage ? (messageProps?.id ?? `neo-textarea-message-${crypto.randomUUID()}`) : undefined);
257275
258-
const context = $derived<NeoInputContext<HTMLTextAreaElement>>({
276+
const context = $derived<NeoInputContext<NeoTextareaHTMLElement>>({
259277
// Ref
260278
ref,
261279
262280
// Methods
263281
mark,
264282
clear,
283+
change,
284+
validate,
265285
266286
// State
267287
value,

src/lib/inputs/neo-input.model.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export type NeoInputState = {
2929
* The input/textarea value.
3030
*/
3131
value?: string;
32+
/**
33+
* The initial input/textarea value.
34+
*/
35+
initial?: string;
3236
};
3337

3438
export type NeoInputStyles = {
@@ -90,8 +94,20 @@ export type NeoInputMethods = {
9094
* Clear the input. If a state is provided, the input state will be updated accordingly.
9195
* If a partial state is provided, the input state will be reinitialized and the provided state will be merged.
9296
* @param state
97+
* @param event
98+
*/
99+
clear: (state?: NeoInputState, event?: InputEvent) => unknown;
100+
/**
101+
* Change the input value.
102+
* @param value
103+
* @param event
93104
*/
94-
clear: (state?: NeoInputState) => unknown;
105+
change: (value: HTMLInputElement['value'], event?: InputEvent) => NeoInputState;
106+
/**
107+
* Check the input validity.
108+
* @param update whether to check the input dirty and/or valid state.
109+
*/
110+
validate: (update?: { dirty?: boolean; valid?: boolean }) => NeoInputState;
95111
};
96112

97113
export type NeoInputElevation = ShadowElevation;
@@ -182,8 +198,9 @@ export type NeoCommonInputProps<T extends HTMLElement> = {
182198
/**
183199
* Callback when the input is cleared.
184200
* @param state
201+
* @param event
185202
*/
186-
onclear?: (state: NeoInputState) => unknown;
203+
onclear?: (state: NeoInputState, event?: InputEvent) => unknown;
187204

188205
// Other props
189206

@@ -239,7 +256,7 @@ export type NeoCommonInputProps<T extends HTMLElement> = {
239256
NeoInputStyles &
240257
HTMLActionProps;
241258

242-
export type NeoInputProps<T extends HTMLInputElement = HTMLInputElement> = {
259+
export type NeoInputProps<T extends HTMLInputElement = NeoInputHTMLElement> = {
243260
// Snippets
244261

245262
/**
@@ -277,7 +294,8 @@ export type NeoTextAreaResize = {
277294
*/
278295
max?: number;
279296
};
280-
export type NeoTextareaProps<T extends HTMLTextAreaElement = HTMLTextAreaElement> = {
297+
298+
export type NeoTextareaProps<T extends HTMLTextAreaElement = NeoTextareaHTMLElement> = {
281299
/**
282300
* Automatically increments/decrements the textarea rows to fit the content.
283301
*
@@ -289,3 +307,6 @@ export type NeoTextareaProps<T extends HTMLTextAreaElement = HTMLTextAreaElement
289307
autoResize?: boolean | NeoTextAreaResize;
290308
} & NeoCommonInputProps<T> &
291309
HTMLTextareaAttributes;
310+
311+
export type NeoInputHTMLElement = HTMLInputElement & Partial<NeoInputMethods>;
312+
export type NeoTextareaHTMLElement = HTMLTextAreaElement & Partial<NeoInputMethods>;

0 commit comments

Comments
 (0)