Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7258d38
Initial plan
Copilot Feb 21, 2026
7f98008
fix: skip value propagation for number inputs in intermediate decimal…
Copilot Feb 21, 2026
fee6312
fix: only skip writeValue for Angular badInput; revert Vue and packag…
Copilot Feb 21, 2026
d6827b3
fix: also guard writeValue against comma input where badInput stays f…
Copilot Feb 24, 2026
019869a
fix: issue with form-components for angular
nmerget Feb 25, 2026
b1ac2e4
auto update snapshots (#6149)
github-actions[bot] Feb 25, 2026
30b34f5
Merge remote-tracking branch 'origin/main' into copilot/fix-input-fie…
nmerget Feb 25, 2026
8c3929a
fix: ensure input fields default to empty string when no value is pro…
nmerget Feb 25, 2026
4f1f5b8
fix: issue with react value
nmerget Feb 26, 2026
b0fd6dd
Merge remote-tracking branch 'origin/main' into copilot/fix-input-fie…
nmerget Feb 26, 2026
11c1f1c
fix: issue with test
nmerget Feb 26, 2026
6b8c7e1
refactor: added changeset
mfranzke Mar 3, 2026
45505d7
auto update snapshots (#6231)
github-actions[bot] Mar 3, 2026
9e5849d
fix: call propagateChange before writeValue guard in handleFrameworkE…
Copilot Mar 13, 2026
0fe9235
Merge remote-tracking branch 'origin/main' into copilot/fix-input-fie…
nmerget Apr 8, 2026
ca879a1
fix: enhance number input handling to skip invalid states and improve…
nmerget Apr 8, 2026
6ece0c0
Merge remote-tracking branch 'refs/remotes/origin/main' into copilot/…
nmerget Apr 8, 2026
b93258e
fix: remove unnecessary 'dev' flags from package-lock.json
nmerget Apr 8, 2026
19136eb
chore: update changset
nmerget Apr 8, 2026
662f094
fix: update input field value handling to use undefined instead of em…
nmerget Apr 8, 2026
5d3c765
Merge branch 'main' into copilot/fix-input-field-decimal-separator
nmerget Apr 8, 2026
030503f
fix: update input field value handling to use undefined instead of em…
nmerget Apr 8, 2026
12917a7
Merge branch 'main' into copilot/fix-input-field-decimal-separator
nmerget Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/wicked-planets-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@db-ux/core-components": patch
"@db-ux/react-core-components": patch
"@db-ux/wc-core-components": patch
"@db-ux/v-core-components": patch
"@db-ux/ngx-core-components": patch
---

fix(number input): prevent from clearing on intermediate decimal entry
fix(input,textarea): allow using `undefined` as `value`
21 changes: 6 additions & 15 deletions packages/components/scripts/post-build/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,11 @@ export const getComponents = (): Component[] => [
],
stencil: [
{ from: 'HTMLElement', to: 'HTMLSelectElement' },
{ from: 'value={', to: '/* @ts-ignore */\nvalue={' }
{ from: 'value={', to: '/* @ts-ignore */\nvalue={' },
{
from: 'this.value ?? this._value ?? ""',
to: 'this.value ?? this._value ?? undefined'
}
]
},
config: {
Expand Down Expand Up @@ -371,20 +375,7 @@ export const getComponents = (): Component[] => [
vue: [{ from: ', index', to: '' }],
stencil: [{ from: 'HTMLElement', to: 'HTMLInputElement' }],
react: [{ from: /HTMLAttributes/g, to: 'InputHTMLAttributes' }],
angular: [
{ from: '<HTMLElement>', to: '<HTMLInputElement>' },
{
from: 'writeValue(value: any) {',
to:
'writeValue(value: any) {\n' +
'if (!value && value !== "" && (this.type() === "date" ||\n' +
' this.type() === "time" ||\n' +
' this.type() === "week" ||\n' +
' this.type() === "month" ||\n' +
' this.type() === "datetime-local"\n' +
' )) return;'
}
]
angular: [{ from: '<HTMLElement>', to: '<HTMLInputElement>' }]
},
config: {
vue: {
Expand Down
2 changes: 1 addition & 1 deletion packages/components/scripts/post-build/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export default DB${upperComponentName};`
/* We need to overwrite the internal state._value property just for react to have controlled components.
* It works for Angular & Vue, so we overwrite it only for React. */
{
from: 'props.value ?? _value',
from: 'props.value ?? _value ?? ""',
to: 'props.value'
}
];
Expand Down
22 changes: 16 additions & 6 deletions packages/components/src/components/input/input.lite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,13 @@ export default function DBInput(props: DBInputProps) {
});

useTarget({
angular: () => handleFrameworkEventAngular(state, event),
angular: () =>
handleFrameworkEventAngular(
state,
event,
'value',
state._value
),
vue: () => handleFrameworkEventVue(() => {}, event)
});
state.handleValidation();
Expand All @@ -161,7 +167,13 @@ export default function DBInput(props: DBInputProps) {
});

useTarget({
angular: () => handleFrameworkEventAngular(state, event),
angular: () =>
handleFrameworkEventAngular(
state,
event,
'value',
state._value
),
vue: () => handleFrameworkEventVue(() => {}, event)
});
state.handleValidation();
Expand Down Expand Up @@ -235,9 +247,7 @@ export default function DBInput(props: DBInputProps) {
}, [state._id]);

onUpdate(() => {
if (props.value !== undefined) {
state._value = props.value;
}
state._value = props.value;
}, [props.value]);

onUpdate(() => {
Expand Down Expand Up @@ -305,7 +315,7 @@ export default function DBInput(props: DBInputProps) {
disabled={getBoolean(props.disabled, 'disabled')}
required={getBoolean(props.required, 'required')}
step={getStep(props.step)}
value={props.value ?? state._value}
value={props.value ?? state._value ?? ''}
maxLength={getNumber(props.maxLength, props.maxlength)}
minLength={getNumber(props.minLength, props.minlength)}
max={getInputValue(props.max, props.type)}
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/components/select/select.lite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export default function DBSelect(props: DBSelectProps) {
id={state._id}
name={props.name}
size={props.size}
value={props.value ?? state._value}
value={props.value ?? state._value ?? ''}
autocomplete={props.autocomplete}
multiple={props.multiple}
onInput={(event: ChangeEvent<HTMLSelectElement>) =>
Expand Down
6 changes: 2 additions & 4 deletions packages/components/src/components/textarea/textarea.lite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,7 @@ export default function DBTextarea(props: DBTextareaProps) {
}, [state._id]);

onUpdate(() => {
if (props.value !== undefined) {
state._value = props.value;
}
state._value = props.value;
}, [props.value]);

onUpdate(() => {
Expand Down Expand Up @@ -288,7 +286,7 @@ export default function DBTextarea(props: DBTextareaProps) {
onFocus={(event: InteractionEvent<HTMLTextAreaElement>) =>
state.handleFocus(event)
}
value={props.value ?? state._value}
value={props.value ?? state._value ?? ''}
aria-describedby={props.ariaDescribedBy ?? state._descByIds}
placeholder={props.placeholder ?? DEFAULT_PLACEHOLDER}
rows={getNumber(props.rows, DEFAULT_ROWS)}
Expand Down
172 changes: 172 additions & 0 deletions packages/components/src/utils/form-components.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { describe, expect, it, vi } from 'vitest';
import {
handleFrameworkEventAngular,
handleFrameworkEventVue
} from './form-components';

const createNumberEvent = (
value: string,
badInput: boolean,
inputType?: string
) => ({
inputType,
target: {
type: 'number',
value,
validity: { badInput }
}
});

const createTextEvent = (value: string) => ({
target: {
type: 'text',
value,
validity: { badInput: false }
}
});

describe('handleFrameworkEventAngular', () => {
it('calls propagateChange and writeValue for valid number value', () => {
const component = {
propagateChange: vi.fn(),
writeValue: vi.fn()
};
const event = createNumberEvent('1.5', false);
handleFrameworkEventAngular(component, event);
expect(component.propagateChange).toHaveBeenCalledWith('1.5');
expect(component.writeValue).toHaveBeenCalledWith('1.5');
});

it('skips propagateChange and writeValue when "." is typed (intermediate state)', () => {
const component = {
propagateChange: vi.fn(),
writeValue: vi.fn()
};
const event = {
type: 'input',
data: '.',
inputType: 'insertText',
target: { type: 'number', value: '1.' }
};
handleFrameworkEventAngular(component, event, 'value', '1');
expect(component.propagateChange).not.toHaveBeenCalled();
expect(component.writeValue).not.toHaveBeenCalled();
});

it('skips propagateChange and writeValue when "," is typed (intermediate state)', () => {
const component = {
propagateChange: vi.fn(),
writeValue: vi.fn()
};
const event = {
type: 'input',
data: ',',
inputType: 'insertText',
target: { type: 'number', value: '' }
};
handleFrameworkEventAngular(component, event, 'value', '1');
expect(component.propagateChange).not.toHaveBeenCalled();
expect(component.writeValue).not.toHaveBeenCalled();
});

it.each(['e', 'E', '+', '-'])(
'skips propagateChange and writeValue when "%s" is typed (intermediate state)',
(char) => {
const component = {
propagateChange: vi.fn(),
writeValue: vi.fn()
};
const event = {
type: 'input',
data: char,
inputType: 'insertText',
target: { type: 'number', value: '' }
};
handleFrameworkEventAngular(component, event, 'value', '1');
expect(component.propagateChange).not.toHaveBeenCalled();
expect(component.writeValue).not.toHaveBeenCalled();
}
);

it('skips propagateChange and writeValue when deleting content and lastValue has decimal', () => {
const component = {
propagateChange: vi.fn(),
writeValue: vi.fn()
};
const event = {
type: 'input',
data: null,
inputType: 'deleteContentBackward',
target: { type: 'number', value: '' }
};
handleFrameworkEventAngular(component, event, 'value', '1.5');
expect(component.propagateChange).not.toHaveBeenCalled();
expect(component.writeValue).not.toHaveBeenCalled();
});

it('calls propagateChange and writeValue when number input is cleared via backspace (no decimal in lastValue)', () => {
const component = {
propagateChange: vi.fn(),
writeValue: vi.fn()
};
const event = createNumberEvent('', false, 'deleteContentBackward');
handleFrameworkEventAngular(component, event);
expect(component.propagateChange).toHaveBeenCalledWith('');
expect(component.writeValue).toHaveBeenCalledWith('');
});

it('skips propagateChange and writeValue for number change events', () => {
const component = {
propagateChange: vi.fn(),
writeValue: vi.fn()
};
const event = {
type: 'change',
target: { type: 'number', value: '5' }
};
handleFrameworkEventAngular(component, event);
expect(component.propagateChange).not.toHaveBeenCalled();
expect(component.writeValue).not.toHaveBeenCalled();
});

it('calls propagateChange and writeValue for text input type', () => {
const component = {
propagateChange: vi.fn(),
writeValue: vi.fn()
};
const event = createTextEvent('hello');
handleFrameworkEventAngular(component, event);
expect(component.propagateChange).toHaveBeenCalledWith('hello');
expect(component.writeValue).toHaveBeenCalledWith('hello');
});
});

describe('handleFrameworkEventVue', () => {
it('emits update:value for valid number value', () => {
const emit = vi.fn();
const event = createNumberEvent('1.5', false);
handleFrameworkEventVue(emit, event);
expect(emit).toHaveBeenCalledWith('update:value', '1.5');
});

it('emits update:value with empty string when number input has badInput (intermediate state like "1.")', () => {
const emit = vi.fn();
const event = createNumberEvent('', true);
handleFrameworkEventVue(emit, event);
expect(emit).toHaveBeenCalledWith('update:value', '');
});

it('emits update:value when number input is cleared (empty, no badInput)', () => {
const emit = vi.fn();
const event = createNumberEvent('', false);
handleFrameworkEventVue(emit, event);
expect(emit).toHaveBeenCalledWith('update:value', '');
});

it('emits update:value for text input type', () => {
const emit = vi.fn();
const event = createTextEvent('hello');
handleFrameworkEventVue(emit, event);
expect(emit).toHaveBeenCalledWith('update:value', 'hello');
});
});
40 changes: 37 additions & 3 deletions packages/components/src/utils/form-components.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,47 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { delay } from './index';

const specialNumberCharacters = ['.', ',', 'e', 'E', '+', '-'];

export const handleFrameworkEventAngular = (
component: any,
event: any,
modelValue: string = 'value'
modelValue: string = 'value',
lastValue?: any
): void => {
component.propagateChange(event.target[modelValue]);
component.writeValue(event.target[modelValue]);
const value = event.target[modelValue];
const type = event.target?.type;

if (
!value &&
value !== '' &&
['date', 'time', 'week', 'month', 'datetime-local'].includes(type)
) {
// If value is empty and type date we skip `writingValue` function
return;
}

if (type === 'number') {
if (event.type === 'input') {
if (
specialNumberCharacters.includes(event.data) ||
(specialNumberCharacters.some((specialCharacter) =>
lastValue?.toString().includes(specialCharacter)
) &&
event.inputType === 'deleteContentBackward')
) {
// Skip `writingValue` function if number type and input event
// and `.` or `,` or 'e', 'E', '+', '-' was typed
// or content was deleted but last number had a `.`
return;
}
} else if (event.type === 'change') {
// Skip `writingValue` function if number type and change event
return;
}
}
component.propagateChange(value);
component.writeValue(value);
};

export const handleFrameworkEventVue = (
Expand Down
Loading