Skip to content

Commit 6a987eb

Browse files
committed
feature #203 [LiveComponent] consider data-value attribute (WhiteRabbitDE)
This PR was merged into the 2.x branch. Discussion ---------- [LiveComponent] consider data-value attribute | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Tickets | #195 | License | MIT Allow to set the value using data-value. This can be handy if the model is updated from an \<a\> or \<button\> to set it to a fixed value for example. Commits ------- 0ae0593 consider data-value attribute
2 parents 9549bb2 + 0ae0593 commit 6a987eb

File tree

4 files changed

+94
-5
lines changed

4 files changed

+94
-5
lines changed

src/LiveComponent/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,27 @@ code works identically to the previous example:
370370
If an element has _both_ `data-model` and `name` attributes, the
371371
`data-model` attribute takes precedence.
372372

373+
### Using data-value="" for non input elements
374+
375+
If you want to set the value of a model with an element that is not an input element and does not have a `value` attribute you can use `data-value`
376+
377+
```twig
378+
<div {{ init_live_component(this)>
379+
380+
<a
381+
data-action="live#update"
382+
data-model="min"
383+
data-value="10">
384+
Set min to 10
385+
</a>
386+
387+
// ...
388+
</div>
389+
```
390+
391+
If an element has _both_ `data-value` and `value` attributes, the
392+
`data-value` attribute takes precedence.
393+
373394
## Loading States
374395

375396
Often, you'll want to show (or hide) an element while a component is

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,11 +1057,11 @@ class default_1 extends Controller {
10571057
window.removeEventListener('beforeunload', this.markAsWindowUnloaded);
10581058
}
10591059
update(event) {
1060-
const value = event.target.value;
1060+
const value = this._getValueFromElement(event.target);
10611061
this._updateModelFromElement(event.target, value, true);
10621062
}
10631063
updateDefer(event) {
1064-
const value = event.target.value;
1064+
const value = this._getValueFromElement(event.target);
10651065
this._updateModelFromElement(event.target, value, false);
10661066
}
10671067
action(event) {
@@ -1111,6 +1111,17 @@ class default_1 extends Controller {
11111111
$render() {
11121112
this._makeRequest(null);
11131113
}
1114+
_getValueFromElement(element) {
1115+
const value = element.dataset.value || element.value;
1116+
if (!value) {
1117+
const clonedElement = (element.cloneNode());
1118+
if (!(clonedElement instanceof HTMLElement)) {
1119+
throw new Error('cloneNode() produced incorrect type');
1120+
}
1121+
throw new Error(`The update() method could not be called for "${clonedElement.outerHTML}": the element must either have a "data-value" or "value" attribute set.`);
1122+
}
1123+
return value;
1124+
}
11141125
_updateModelFromElement(element, value, shouldRender) {
11151126
const model = element.dataset.model || element.getAttribute('name');
11161127
if (!model) {

src/LiveComponent/assets/src/live_controller.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,13 @@ export default class extends Controller {
110110
* Called to update one piece of the model
111111
*/
112112
update(event: any) {
113-
const value = event.target.value;
113+
const value = this._getValueFromElement(event.target);
114114

115115
this._updateModelFromElement(event.target, value, true);
116116
}
117117

118118
updateDefer(event: any) {
119-
const value = event.target.value;
119+
const value = this._getValueFromElement(event.target);
120120

121121
this._updateModelFromElement(event.target, value, false);
122122
}
@@ -195,6 +195,22 @@ export default class extends Controller {
195195
this._makeRequest(null);
196196
}
197197

198+
_getValueFromElement(element: HTMLElement){
199+
const value = element.dataset.value || element.value;
200+
201+
if (!value) {
202+
const clonedElement = (element.cloneNode());
203+
// helps typescript know this is an HTMLElement
204+
if (!(clonedElement instanceof HTMLElement)) {
205+
throw new Error('cloneNode() produced incorrect type');
206+
}
207+
208+
throw new Error(`The update() method could not be called for "${clonedElement.outerHTML}": the element must either have a "data-value" or "value" attribute set.`);
209+
}
210+
211+
return value;
212+
}
213+
198214
_updateModelFromElement(element: HTMLElement, value: string, shouldRender: boolean) {
199215
const model = element.dataset.model || element.getAttribute('name');
200216

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import { clearDOM } from '@symfony/stimulus-testing';
1313
import { initLiveComponent, mockRerender, startStimulus } from '../tools';
14-
import { getByLabelText, waitFor } from '@testing-library/dom';
14+
import {getByLabelText, getByText, waitFor} from '@testing-library/dom';
1515
import userEvent from '@testing-library/user-event';
1616
import fetchMock from 'fetch-mock-jest';
1717

@@ -28,6 +28,7 @@ describe('LiveController data-model Tests', () => {
2828
value="${data.name}"
2929
>
3030
</label>
31+
<a data-action="live#update" data-model="name" data-value="Jan">Change name to Jan</a>
3132
</div>
3233
`;
3334

@@ -61,6 +62,25 @@ describe('LiveController data-model Tests', () => {
6162
expect(document.activeElement.dataset.model).toEqual('name');
6263
});
6364

65+
it('renders correctly with data-value and live#update', async () => {
66+
const data = { name: 'Ryan' };
67+
const { element, controller } = await startStimulus(template(data));
68+
69+
fetchMock.getOnce('end:?name=Jan', {
70+
html: template({ name: 'Jan' }),
71+
data: { name: 'Jan' }
72+
});
73+
74+
userEvent.click(getByText(element, 'Change name to Jan'));
75+
76+
await waitFor(() => expect(getByLabelText(element, 'Name:')).toHaveValue('Jan'));
77+
expect(controller.dataValue).toEqual({name: 'Jan'});
78+
79+
// assert all calls were done the correct number of times
80+
fetchMock.done();
81+
});
82+
83+
6484
it('correctly only uses the most recent render call results', async () => {
6585
const data = { name: 'Ryan' };
6686
const { element, controller } = await startStimulus(template(data));
@@ -148,6 +168,27 @@ describe('LiveController data-model Tests', () => {
148168
fetchMock.done();
149169
});
150170

171+
it('uses data-value when both value and data-value is present', async () => {
172+
const data = { name: 'Ryan' };
173+
const { element, controller } = await startStimulus(template(data));
174+
175+
// give element data-model="name" and name="first_name"
176+
const inputElement = getByLabelText(element, 'Name:');
177+
inputElement.dataset.value = 'first_name';
178+
179+
// ?name should be what's sent to the server
180+
mockRerender({name: 'first_name'}, template, (data) => {
181+
data.name = 'first_name';
182+
})
183+
184+
await userEvent.type(inputElement, ' WEAVER');
185+
186+
await waitFor(() => expect(inputElement).toHaveValue('first_name'));
187+
expect(controller.dataValue).toEqual({name: 'first_name'});
188+
189+
fetchMock.done();
190+
});
191+
151192
it('standardizes user[firstName] style models into post.name', async () => {
152193
const deeperModelTemplate = (data) => `
153194
<div

0 commit comments

Comments
 (0)