Skip to content

Commit 2b808ea

Browse files
committed
Send data back and forth as JSON
1 parent e2df324 commit 2b808ea

File tree

15 files changed

+111
-222
lines changed

15 files changed

+111
-222
lines changed

.php-cs-fixer.dist.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
->append([__FILE__])
2525
->notPath('#/Fixtures/#')
2626
->notPath('#/app/var/#')
27+
->notPath('#/var/cache/#')
2728
->notPath('Turbo/Attribute/Broadcast.php') // Need https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues/4702
2829
)
2930
;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
"plugin:@typescript-eslint/recommended"
4646
],
4747
"rules": {
48-
"@typescript-eslint/no-explicit-any": "off"
48+
"@typescript-eslint/no-explicit-any": "off",
49+
"@typescript-eslint/no-empty-function": "off"
4950
},
5051
"env": {
5152
"browser": true

src/LiveComponent/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## 2.1.0
44

5+
- Your component's live "data" is now send over Ajax as a JSON string.
6+
Previously data was sent as pure query parameters or as pure POST data.
7+
However, this made it impossible to keep certain data types, like
8+
distinguishing between `null` and `''`. This has no impact on end-users.
9+
510
- Added `data-live-ignore` attribute. If included in an element, that element
611
will not be updated on re-render.
712

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 14 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -898,61 +898,6 @@ function combineSpacedArray(parts) {
898898
return finalParts;
899899
}
900900

901-
const buildFormKey = function (key, parentKeys) {
902-
let fieldName = '';
903-
[...parentKeys, key].forEach((name) => {
904-
fieldName += fieldName ? `[${name}]` : name;
905-
});
906-
return fieldName;
907-
};
908-
const addObjectToFormData = function (formData, data, parentKeys) {
909-
Object.keys(data).forEach((key => {
910-
let value = data[key];
911-
if (value === true) {
912-
value = 1;
913-
}
914-
if (value === false) {
915-
value = 0;
916-
}
917-
if (value === null) {
918-
return;
919-
}
920-
if (typeof value === 'object') {
921-
addObjectToFormData(formData, value, [...parentKeys, key]);
922-
return;
923-
}
924-
formData.append(buildFormKey(key, parentKeys), value);
925-
}));
926-
};
927-
const addObjectToSearchParams = function (searchParams, data, parentKeys) {
928-
Object.keys(data).forEach((key => {
929-
let value = data[key];
930-
if (value === true) {
931-
value = 1;
932-
}
933-
if (value === false) {
934-
value = 0;
935-
}
936-
if (value === null) {
937-
return;
938-
}
939-
if (typeof value === 'object') {
940-
addObjectToSearchParams(searchParams, value, [...parentKeys, key]);
941-
return;
942-
}
943-
searchParams.set(buildFormKey(key, parentKeys), value);
944-
}));
945-
};
946-
function buildFormData(data) {
947-
const formData = new FormData();
948-
addObjectToFormData(formData, data, []);
949-
return formData;
950-
}
951-
function buildSearchParams(searchParams, data) {
952-
addObjectToSearchParams(searchParams, data, []);
953-
return searchParams;
954-
}
955-
956901
function setDeepData(data, propertyPath, value) {
957902
const finalData = JSON.parse(JSON.stringify(data));
958903
let currentLevelData = finalData;
@@ -1200,13 +1145,19 @@ class default_1 extends Controller {
12001145
fetchOptions.headers['X-CSRF-TOKEN'] = this.csrfValue;
12011146
}
12021147
}
1203-
if (!action && this._willDataFitInUrl()) {
1204-
buildSearchParams(params, this.dataValue);
1205-
fetchOptions.method = 'GET';
1148+
let dataAdded = false;
1149+
if (!action) {
1150+
const dataJson = JSON.stringify(this.dataValue);
1151+
if (this._willDataFitInUrl(dataJson, params)) {
1152+
params.set('data', dataJson);
1153+
fetchOptions.method = 'GET';
1154+
dataAdded = true;
1155+
}
12061156
}
1207-
else {
1157+
if (!dataAdded) {
12081158
fetchOptions.method = 'POST';
1209-
fetchOptions.body = buildFormData(this.dataValue);
1159+
fetchOptions.body = JSON.stringify(this.dataValue);
1160+
fetchOptions.headers['Content-Type'] = 'application/json';
12101161
}
12111162
this._onLoadingStart();
12121163
const paramsString = params.toString();
@@ -1358,8 +1309,9 @@ class default_1 extends Controller {
13581309
element.removeAttribute(attribute);
13591310
});
13601311
}
1361-
_willDataFitInUrl() {
1362-
return Object.values(this.dataValue).join(',').length < 1500;
1312+
_willDataFitInUrl(dataJson, params) {
1313+
const urlEncodedJsonData = new URLSearchParams(dataJson).toString();
1314+
return (urlEncodedJsonData + params.toString()).length < 1500;
13631315
}
13641316
_executeMorphdom(newHtml) {
13651317
function htmlToElement(html) {

src/LiveComponent/assets/src/http_data_helper.ts

Lines changed: 0 additions & 94 deletions
This file was deleted.

src/LiveComponent/assets/src/live_controller.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Controller } from '@hotwired/stimulus';
22
import morphdom from 'morphdom';
33
import { parseDirectives, Directive } from './directives_parser';
44
import { combineSpacedArray } from './string_utils';
5-
import { buildFormData, buildSearchParams } from './http_data_helper';
65
import { setDeepData, doesDeepPropertyExist, normalizeModelName } from './set_deep_data';
76
import { haveRenderedValuesChanged } from './have_rendered_values_changed';
87
import { normalizeAttributesForComparison } from './normalize_attributes_for_comparison';
@@ -315,12 +314,21 @@ export default class extends Controller {
315314
}
316315
}
317316

318-
if (!action && this._willDataFitInUrl()) {
319-
buildSearchParams(params, this.dataValue);
320-
fetchOptions.method = 'GET';
321-
} else {
317+
let dataAdded = false;
318+
if (!action) {
319+
const dataJson = JSON.stringify(this.dataValue);
320+
if (this._willDataFitInUrl(dataJson, params)) {
321+
params.set('data', dataJson);
322+
fetchOptions.method = 'GET';
323+
dataAdded = true;
324+
}
325+
}
326+
327+
// if GET can't be used, fallback to POST
328+
if (!dataAdded) {
322329
fetchOptions.method = 'POST';
323-
fetchOptions.body = buildFormData(this.dataValue);
330+
fetchOptions.body = JSON.stringify(this.dataValue);
331+
fetchOptions.headers['Content-Type'] = 'application/json';
324332
}
325333

326334
this._onLoadingStart();
@@ -531,9 +539,11 @@ export default class extends Controller {
531539
})
532540
}
533541

534-
_willDataFitInUrl() {
542+
_willDataFitInUrl(dataJson: string, params: URLSearchParams) {
543+
const urlEncodedJsonData = new URLSearchParams(dataJson).toString();
544+
535545
// if the URL gets remotely close to 2000 chars, it may not fit
536-
return Object.values(this.dataValue).join(',').length < 1500;
546+
return (urlEncodedJsonData + params.toString()).length < 1500;
537547
}
538548

539549
_executeMorphdom(newHtml: string) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ describe('LiveController Action Tests', () => {
6565
await waitFor(() => expect(element).toHaveTextContent('Comment Saved!'));
6666
expect(getByLabelText(element, 'Comments:')).toHaveValue('hi weaver');
6767

68-
expect(postMock.lastOptions().body.get('comments')).toEqual('hi WEAVER');
68+
const bodyData = JSON.parse(postMock.lastOptions().body);
69+
expect(bodyData.comments).toEqual('hi WEAVER');
6970
});
7071

7172
it('Sends action named args', async () => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ describe('LiveController parent -> child component tests', () => {
187187
const inputElement = getByLabelText(element, 'Content:');
188188
await userEvent.clear(inputElement);
189189
await userEvent.type(inputElement, 'changed content');
190-
mockRerender({value: 'changed content'}, childTemplate);
190+
mockRerender({value: 'changed content', error: null}, childTemplate);
191191

192192
await waitFor(() => expect(element).toHaveTextContent('Value in child: changed content'));
193193

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ describe('LiveController data-model Tests', () => {
4444
const data = { name: 'Ryan' };
4545
const { element, controller } = await startStimulus(template(data));
4646

47-
fetchMock.getOnce('end:?name=Ryan+WEAVER', template({ name: 'Ryan Weaver' }));
47+
mockRerender({name: 'Ryan WEAVER'}, template, (data: any) => {
48+
data.name = 'Ryan Weaver';
49+
});
4850

4951
await userEvent.type(getByLabelText(element, 'Name:'), ' WEAVER', {
5052
// this tests the debounce: characters have a 10ms delay
@@ -63,7 +65,7 @@ describe('LiveController data-model Tests', () => {
6365
const data = { name: 'Ryan' };
6466
const { element, controller } = await startStimulus(template(data));
6567

66-
fetchMock.getOnce('end:?name=Jan', template({ name: 'Jan' }));
68+
mockRerender({name: 'Jan'}, template);
6769

6870
userEvent.click(getByText(element, 'Change name to Jan'));
6971

@@ -87,11 +89,9 @@ describe('LiveController data-model Tests', () => {
8789
['guy', 150]
8890
];
8991
requests.forEach(([string, delay]) => {
90-
fetchMock.getOnce(
91-
`end:my_component?name=Ryan${string}`,
92-
template({ name: `Ryan${string}_` }),
93-
{ delay }
94-
);
92+
mockRerender({name: `Ryan${string}`}, template, (data: any) => {
93+
data.name = `Ryan${string}_`;
94+
}, { delay });
9595
});
9696

9797
await userEvent.type(getByLabelText(element, 'Name:'), 'guy', {
@@ -121,7 +121,7 @@ describe('LiveController data-model Tests', () => {
121121
delete inputElement.dataset.model;
122122
inputElement.setAttribute('name', 'name');
123123

124-
mockRerender({name: 'Ryan WEAVER'}, template, (data) => {
124+
mockRerender({name: 'Ryan WEAVER'}, template, (data: any) => {
125125
data.name = 'Ryan Weaver';
126126
});
127127

0 commit comments

Comments
 (0)