Skip to content

Commit b1d6fa2

Browse files
committed
Rename modifiers (snake_case) | missing doc
1 parent 00c5e3c commit b1d6fa2

File tree

6 files changed

+87
-26
lines changed

6 files changed

+87
-26
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# CHANGELOG
22

3+
## 2.28.0
4+
5+
- `LiveComponent` Add new `data-model` modifiers:
6+
- `min_length(N)`, `max_length(N)`: restrict updates based on input length
7+
- `min_value(N)`, `max_value(N)`: restrict updates based on numeric value
8+
- Useful with `debounce()` to avoid unnecessary Ajax requests
9+
10+
```twig
11+
<input data-model="debounce(300)|min_length(3)|name" type="text">
12+
```
13+
314
## 2.27.0
415

516
- Add events assertions in `InteractsWithLiveComponents`:

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2364,16 +2364,16 @@ function getModelBinding (modelDirective) {
23642364
case 'debounce':
23652365
debounce = modifier.value ? Number.parseInt(modifier.value) : true;
23662366
break;
2367-
case 'minlength':
2367+
case 'min_length':
23682368
minLength = modifier.value ? Number.parseInt(modifier.value) : null;
23692369
break;
2370-
case 'maxlength':
2370+
case 'max_length':
23712371
maxLength = modifier.value ? Number.parseInt(modifier.value) : null;
23722372
break;
2373-
case 'minvalue':
2373+
case 'min_value':
23742374
minValue = modifier.value ? Number.parseFloat(modifier.value) : null;
23752375
break;
2376-
case 'maxvalue':
2376+
case 'max_value':
23772377
maxValue = modifier.value ? Number.parseFloat(modifier.value) : null;
23782378
break;
23792379
default:

src/LiveComponent/assets/src/Directive/get_model_binding.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,22 @@ export default function (modelDirective: Directive): ModelBinding {
4848

4949
break;
5050

51-
case 'minlength':
51+
case 'min_length':
5252
minLength = modifier.value ? Number.parseInt(modifier.value) : null;
5353

5454
break;
5555

56-
case 'maxlength':
56+
case 'max_length':
5757
maxLength = modifier.value ? Number.parseInt(modifier.value) : null;
5858

5959
break;
6060

61-
case 'minvalue':
61+
case 'min_value':
6262
minValue = modifier.value ? Number.parseFloat(modifier.value) : null;
6363

6464
break;
6565

66-
case 'maxvalue':
66+
case 'max_value':
6767
maxValue = modifier.value ? Number.parseFloat(modifier.value) : null;
6868

6969
break;

src/LiveComponent/assets/test/Directive/get_model_binding.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ describe('get_model_binding', () => {
4747
});
4848
});
4949

50-
it('parses minlength and maxlength modifiers', () => {
51-
const directive = parseDirectives('minlength(3)|maxlength(20)|username')[0];
50+
it('parses min_length and max_length modifiers', () => {
51+
const directive = parseDirectives('min_length(3)|max_length(20)|username')[0];
5252
expect(getModelBinding(directive)).toEqual({
5353
modelName: 'username',
5454
innerModelName: null,
@@ -62,8 +62,8 @@ describe('get_model_binding', () => {
6262
});
6363
});
6464

65-
it('parses minvalue and maxvalue modifiers', () => {
66-
const directive = parseDirectives('minvalue(18)|maxvalue(65)|age')[0];
65+
it('parses min_value and max_value modifiers', () => {
66+
const directive = parseDirectives('min_value(18)|max_value(65)|age')[0];
6767
expect(getModelBinding(directive)).toEqual({
6868
modelName: 'age',
6969
innerModelName: null,
@@ -78,7 +78,7 @@ describe('get_model_binding', () => {
7878
});
7979

8080
it('handles mixed modifiers correctly', () => {
81-
const directive = parseDirectives('on(change)|norender|debounce(100)|minvalue(18)|maxvalue(65)|age:years')[0];
81+
const directive = parseDirectives('on(change)|norender|debounce(100)|min_value(18)|max_value(65)|age:years')[0];
8282
expect(getModelBinding(directive)).toEqual({
8383
modelName: 'age',
8484
innerModelName: 'years',
@@ -93,7 +93,7 @@ describe('get_model_binding', () => {
9393
});
9494

9595
it('handles empty modifier values gracefully', () => {
96-
const directive = parseDirectives('minlength|maxlength|username')[0];
96+
const directive = parseDirectives('min_length|max_length|username')[0];
9797
const binding = getModelBinding(directive);
9898
expect(binding.minLength).toBeNull();
9999
expect(binding.maxLength).toBeNull();

src/LiveComponent/assets/test/controller/model.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -977,12 +977,12 @@ describe('LiveController data-model Tests', () => {
977977
expect(test.element).toHaveTextContent('Rating is: 5');
978978
});
979979

980-
it('does not update model if input value length is less than minlength', async () => {
980+
it('does not update model if input value length is less than min_length', async () => {
981981
const test = await createTest(
982982
{ username: '' },
983983
(data: any) => `
984984
<div ${initComponent(data)}>
985-
<input data-model="minlength(3)|username" value="${data.username}" />
985+
<input data-model="min_length(3)|username" value="${data.username}" />
986986
Username: ${data.username}
987987
</div>
988988
`
@@ -994,12 +994,12 @@ describe('LiveController data-model Tests', () => {
994994
expect(test.component.valueStore.getOriginalProps()).toEqual({ username: '' });
995995
});
996996

997-
it('updates model if input value length satisfies minlength', async () => {
997+
it('updates model if input value length satisfies min_length', async () => {
998998
const test = await createTest(
999999
{ username: '' },
10001000
(data: any) => `
10011001
<div ${initComponent(data)}>
1002-
<input data-model="minlength(3)|username" value="${data.username}" />
1002+
<input data-model="min_length(3)|username" value="${data.username}" />
10031003
Username: ${data.username}
10041004
</div>
10051005
`
@@ -1011,12 +1011,12 @@ describe('LiveController data-model Tests', () => {
10111011
await waitFor(() => expect(test.element).toHaveTextContent('Username: abc'));
10121012
});
10131013

1014-
it('does not update model if input value length exceeds maxlength', async () => {
1014+
it('does not update model if input value length exceeds max_length', async () => {
10151015
const test = await createTest(
10161016
{ username: '' },
10171017
(data: any) => `
10181018
<div ${initComponent(data)}>
1019-
<input data-model="maxlength(5)|username" value="${data.username}" />
1019+
<input data-model="max_length(5)|username" value="${data.username}" />
10201020
Username: ${data.username}
10211021
</div>
10221022
`
@@ -1026,12 +1026,12 @@ describe('LiveController data-model Tests', () => {
10261026
expect(test.component.valueStore.getOriginalProps()).toEqual({ username: '' });
10271027
});
10281028

1029-
it('updates model if number input value is within minvalue/maxvalue range', async () => {
1029+
it('updates model if number input value is within min_value/max_value range', async () => {
10301030
const test = await createTest(
10311031
{ age: '' },
10321032
(data: any) => `
10331033
<div ${initComponent(data)}>
1034-
<input data-model="minvalue(18)|maxvalue(65)|age" type="number" />
1034+
<input data-model="min_value(18)|max_value(65)|age" type="number" />
10351035
Age: ${data.age}
10361036
</div>
10371037
`
@@ -1046,12 +1046,12 @@ describe('LiveController data-model Tests', () => {
10461046
await waitFor(() => expect(test.element).toHaveTextContent('Age: 30'));
10471047
});
10481048

1049-
it('does not update model if number input value is less than minvalue', async () => {
1049+
it('does not update model if number input value is less than min_value', async () => {
10501050
const test = await createTest(
10511051
{ age: '' },
10521052
(data: any) => `
10531053
<div ${initComponent(data)}>
1054-
<input data-model="minvalue(18)|age" type="number" />
1054+
<input data-model="min_value(18)|age" type="number" />
10551055
Age: ${data.age}
10561056
</div>
10571057
`
@@ -1064,12 +1064,12 @@ describe('LiveController data-model Tests', () => {
10641064
expect(test.component.valueStore.getOriginalProps()).toEqual({ age: '' });
10651065
});
10661066

1067-
it('does not update model if number input value exceeds maxvalue', async () => {
1067+
it('does not update model if number input value exceeds max_value', async () => {
10681068
const test = await createTest(
10691069
{ age: '' },
10701070
(data: any) => `
10711071
<div ${initComponent(data)}>
1072-
<input data-model="maxvalue(65)|age" type="number" />
1072+
<input data-model="max_value(65)|age" type="number" />
10731073
Age: ${data.age}
10741074
</div>
10751075
`

src/LiveComponent/doc/index.rst

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,56 @@ This can be useful along with a button that triggers a render on click:
384384
<input data-model="norender|coupon">
385385
<button data-action="live#$render">Apply</button>
386386

387+
Input Model Validation Modifiers
388+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
389+
390+
.. versionadded:: 2.28
391+
392+
When binding data using the ``data-model`` directive, you can apply input validation modifiers such as:
393+
394+
- ``min_length(<value>)``: Ensures the input length is at least the given number of characters.
395+
- ``max_length(<value>)``: Ensures the input length does not exceed the given number of characters.
396+
- ``min_value(<value>)``: Ensures the input value is not less than the given numeric value.
397+
- ``max_value(<value>)``: Ensures the input value is not greater than the given numeric value.
398+
399+
These modifiers help reduce unnecessary server requests and provide a lightweight form of frontend validation.
400+
401+
Example:
402+
403+
.. code-block:: html
404+
405+
<input data-model="min_length(3)|username" type="text" value="" />
406+
<input data-model="min_value(10)|max_value(100)|quantity" type="number" value="20" />
407+
408+
In the example above:
409+
410+
- The first input will not trigger a model update until at least 3 characters are entered.
411+
- The second input will only trigger updates when the number is between 10 and 100 (inclusive).
412+
413+
Supported Input Types
414+
---------------------
415+
416+
These modifiers are only evaluated on compatible input types:
417+
418+
- ``min_length``, ``max_length``: Only valid for ``<input type="text">``, ``type="email"``, ``type="password"``, ``type="search"``, ``type="url"``, and ``<textarea>`` elements.
419+
- ``min_value``, ``max_value``: Only valid for ``<input type="number">``, ``type="range"``, and similar numeric fields.
420+
421+
Combining with debounce
422+
-----------------------
423+
424+
These validation modifiers can be combined with ``debounce``:
425+
426+
.. code-block:: html
427+
428+
<input data-model="min_length(4)|debounce(300)|comment" type="text" />
429+
430+
This prevents sending multiple Ajax requests for short inputs and ensures only meaningful data changes are transmitted after 300ms of inactivity. It's especially useful for reducing server load and avoiding over-rendering while typing.
431+
432+
Why is this feature useful?
433+
---------------------------
434+
435+
Imagine a user typing in a search input or numeric field. Without these modifiers, each keystroke could trigger a re-render and Ajax request. With modifiers like ``min_length(3)`` or ``min_value(5)``, you ensure that only valid, actionable data reaches the server — improving performance and user experience.
436+
387437
Forcing a Re-Render Explicitly
388438
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
389439

0 commit comments

Comments
 (0)