mirror of
https://github.com/symfony/ux.git
synced 2026-03-24 00:02:21 +01:00
fix-input-validation-modifiers
This commit is contained in:
13
src/LiveComponent/assets/dist/live_controller.js
vendored
13
src/LiveComponent/assets/dist/live_controller.js
vendored
@@ -2262,14 +2262,17 @@ var LiveControllerDefault = class LiveControllerDefault extends Controller {
|
||||
if (false === modelBinding.debounce) if (modelBinding.targetEventName === "input") modelBinding.debounce = true;
|
||||
else modelBinding.debounce = 0;
|
||||
const finalValue = getValueFromElement(element, this.component.valueStore);
|
||||
const finalValueIsEmpty = finalValue === "" || finalValue === null || finalValue === void 0;
|
||||
if (isTextualInputElement(element) || isTextareaElement(element)) {
|
||||
if (modelBinding.minLength !== null && typeof finalValue === "string" && finalValue.length < modelBinding.minLength) return;
|
||||
if (modelBinding.maxLength !== null && typeof finalValue === "string" && finalValue.length > modelBinding.maxLength) return;
|
||||
if (!finalValueIsEmpty && modelBinding.minLength !== null && typeof finalValue === "string" && finalValue.length < modelBinding.minLength) return;
|
||||
if (!finalValueIsEmpty && modelBinding.maxLength !== null && typeof finalValue === "string" && finalValue.length > modelBinding.maxLength) return;
|
||||
}
|
||||
if (isNumericalInputElement(element)) {
|
||||
const numericValue = Number(finalValue);
|
||||
if (modelBinding.minValue !== null && numericValue < modelBinding.minValue) return;
|
||||
if (modelBinding.maxValue !== null && numericValue > modelBinding.maxValue) return;
|
||||
if (!finalValueIsEmpty) {
|
||||
const numericValue = Number(finalValue);
|
||||
if (modelBinding.minValue !== null && numericValue < modelBinding.minValue) return;
|
||||
if (modelBinding.maxValue !== null && numericValue > modelBinding.maxValue) return;
|
||||
}
|
||||
}
|
||||
this.component.set(modelBinding.modelName, finalValue, modelBinding.shouldRender, modelBinding.debounce);
|
||||
}
|
||||
|
||||
@@ -435,8 +435,11 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
|
||||
|
||||
const finalValue = getValueFromElement(element, this.component.valueStore);
|
||||
|
||||
const finalValueIsEmpty = finalValue === '' || finalValue === null || finalValue === undefined;
|
||||
|
||||
if (isTextualInputElement(element) || isTextareaElement(element)) {
|
||||
if (
|
||||
!finalValueIsEmpty &&
|
||||
modelBinding.minLength !== null &&
|
||||
typeof finalValue === 'string' &&
|
||||
finalValue.length < modelBinding.minLength
|
||||
@@ -445,6 +448,7 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
|
||||
}
|
||||
|
||||
if (
|
||||
!finalValueIsEmpty &&
|
||||
modelBinding.maxLength !== null &&
|
||||
typeof finalValue === 'string' &&
|
||||
finalValue.length > modelBinding.maxLength
|
||||
@@ -454,14 +458,18 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
|
||||
}
|
||||
|
||||
if (isNumericalInputElement(element)) {
|
||||
const numericValue = Number(finalValue);
|
||||
// An empty input converts to 0 via Number(""), which could incorrectly trigger min_value rules.
|
||||
// Therefore, we bypass validation for numeric fields if they are empty.
|
||||
if (!finalValueIsEmpty) {
|
||||
const numericValue = Number(finalValue);
|
||||
|
||||
if (modelBinding.minValue !== null && numericValue < modelBinding.minValue) {
|
||||
return;
|
||||
}
|
||||
if (modelBinding.minValue !== null && numericValue < modelBinding.minValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (modelBinding.maxValue !== null && numericValue > modelBinding.maxValue) {
|
||||
return;
|
||||
if (modelBinding.maxValue !== null && numericValue > modelBinding.maxValue) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1137,4 +1137,93 @@ describe('LiveController data-model Tests', () => {
|
||||
await userEvent.type(input, '30');
|
||||
await waitFor(() => expect(test.element).toHaveTextContent('Age: 30'));
|
||||
});
|
||||
|
||||
it('bypasses min_length validation and updates model when input is cleared', async () => {
|
||||
const test = await createTest(
|
||||
{ username: 'starting_name' }, // Set an initial value
|
||||
(data: any) => `
|
||||
<div ${initComponent(data)}>
|
||||
<input data-model="min_length(3)|username" value="${data.username}" />
|
||||
Username: ${data.username}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
|
||||
// 1. First, try entering an invalid value (2 characters)
|
||||
const input = test.queryByDataModel('username');
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'ab');
|
||||
|
||||
// Model SHOULD NOT be updated, state should remain the same
|
||||
expect(test.component.valueStore.getOriginalProps()).toEqual({ username: 'starting_name' });
|
||||
|
||||
// 2. Now completely clear the input
|
||||
// An empty value ('') should bypass the min_length rule and be sent to the server
|
||||
test.expectsAjaxCall().expectUpdatedData({ username: '' });
|
||||
|
||||
await userEvent.clear(input);
|
||||
|
||||
// FIX: Ensure the old value is completely gone from the DOM to confirm re-render
|
||||
await waitFor(() => expect(test.element).not.toHaveTextContent('starting_name'));
|
||||
expect(test.component.valueStore.getOriginalProps()).toEqual({ username: '' });
|
||||
});
|
||||
|
||||
it('bypasses min_value validation and updates model when number input is cleared', async () => {
|
||||
const test = await createTest(
|
||||
{ age: '30' }, // Set a valid initial value
|
||||
(data: any) => `
|
||||
<div ${initComponent(data)}>
|
||||
<input data-model="min_value(18)|age" type="number" value="${data.age}" />
|
||||
Age: ${data.age}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
|
||||
// 1. First, enter an invalid value (<18)
|
||||
const input = test.queryByDataModel('age');
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, '15');
|
||||
|
||||
// Model SHOULD NOT be updated
|
||||
expect(test.component.valueStore.getOriginalProps()).toEqual({ age: '30' });
|
||||
|
||||
// 2. Completely clear the input
|
||||
test.expectsAjaxCall().expectUpdatedData({ age: '' });
|
||||
|
||||
await userEvent.clear(input);
|
||||
|
||||
// FIX: Ensure the old value '30' is no longer in the DOM
|
||||
await waitFor(() => expect(test.element).not.toHaveTextContent('Age: 30'));
|
||||
expect(test.component.valueStore.getOriginalProps()).toEqual({ age: '' });
|
||||
});
|
||||
|
||||
it('bypasses max_length validation and updates model when input is cleared', async () => {
|
||||
const test = await createTest(
|
||||
{ username: 'starting_name' }, // Set an initial value
|
||||
(data: any) => `
|
||||
<div ${initComponent(data)}>
|
||||
<input data-model="max_length(5)|username" value="${data.username}" />
|
||||
Username: ${data.username}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
|
||||
// 1. First, try entering an invalid value (6 characters)
|
||||
const input = test.queryByDataModel('username');
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'abcdef');
|
||||
|
||||
// Model SHOULD NOT be updated, state should remain the same
|
||||
expect(test.component.valueStore.getOriginalProps()).toEqual({ username: 'starting_name' });
|
||||
|
||||
// 2. Now completely clear the input
|
||||
// An empty value ('') should bypass the max_length rule and be sent to the server
|
||||
test.expectsAjaxCall().expectUpdatedData({ username: '' });
|
||||
|
||||
await userEvent.clear(input);
|
||||
|
||||
// FIX: Ensure the old value is completely gone from the DOM to confirm re-render
|
||||
await waitFor(() => expect(test.element).not.toHaveTextContent('starting_name'));
|
||||
expect(test.component.valueStore.getOriginalProps()).toEqual({ username: '' });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user