Skip to content

Commit c36162c

Browse files
committed
Sending data to the server as JSON instead of urlencoded data
1 parent e2df324 commit c36162c

File tree

8 files changed

+70
-199
lines changed

8 files changed

+70
-199
lines changed

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 13 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,18 @@ 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({ data: this.dataValue });
12101160
}
12111161
this._onLoadingStart();
12121162
const paramsString = params.toString();
@@ -1358,8 +1308,9 @@ class default_1 extends Controller {
13581308
element.removeAttribute(attribute);
13591309
});
13601310
}
1361-
_willDataFitInUrl() {
1362-
return Object.values(this.dataValue).join(',').length < 1500;
1311+
_willDataFitInUrl(dataJson, params) {
1312+
const urlEncodedJsonData = new URLSearchParams(dataJson).toString();
1313+
return (urlEncodedJsonData + params.toString()).length < 1500;
13631314
}
13641315
_executeMorphdom(newHtml) {
13651316
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.data.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

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

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,9 @@ describe('LiveController rendering Tests', () => {
6464
const data = { name: 'Ryan' };
6565
const { element } = await startStimulus(template(data));
6666

67-
fetchMock.get(
68-
// name=Ryan is sent to the server
69-
'http://localhost/_components/my_component?name=Ryan',
70-
// server changes that data
71-
template({ name: 'Kevin' }),
72-
{ delay: 100 }
73-
);
67+
mockRerender({name: 'Ryan'}, template, (data: any) => {
68+
data.name = 'Kevin';
69+
}, { delay: 100 });
7470
// type into the input that is not bound to a model
7571
userEvent.type(getByLabelText(element, 'Comments:'), '!!');
7672
getByText(element, 'Reload').click();
@@ -85,13 +81,11 @@ describe('LiveController rendering Tests', () => {
8581
const data = { name: 'Ryan' };
8682
const { element } = await startStimulus(template(data));
8783

88-
fetchMock.get(
89-
// name=Ryan is sent to the server
90-
'http://localhost/_components/my_component?name=Ryan',
91-
// server changes that data
92-
template({ name: 'Kevin' }),
93-
{ delay: 100 }
94-
);
84+
// name=Ryan is sent to the server
85+
mockRerender({name: 'Ryan'}, template, (data: any) => {
86+
data.name = 'Kevin';
87+
}, { delay: 100 });
88+
9589
// type into the input that is not bound to a model
9690
const input = getByLabelText(element, 'Comments:');
9791
input.setAttribute('data-live-ignore', '');
@@ -113,11 +107,14 @@ describe('LiveController rendering Tests', () => {
113107
template(data, true)
114108
);
115109

116-
fetchMock.get(
117-
'http://localhost/_components/my_component?name=Ryan',
118-
template({ name: 'Kevin' }, true),
110+
mockRerender(
111+
{ name: 'Ryan' },
112+
// re-render but passing true as the second arg
113+
(data: any) => template(data, true),
114+
(data: any) => { data.name = 'Kevin'; },
119115
{ delay: 100 }
120116
);
117+
121118
const input = getByLabelText(element, 'Comments:');
122119
input.setAttribute('data-live-ignore', '');
123120
userEvent.type(input, '!!');
@@ -132,9 +129,12 @@ describe('LiveController rendering Tests', () => {
132129
const data = { name: 'Ryan' };
133130
const { element } = await startStimulus(template(data));
134131

135-
fetchMock.get('end:?name=Ryan', '<div>aloha!</div>', {
136-
delay: 100
137-
});
132+
mockRerender(
133+
{ name: 'Ryan' },
134+
() => '<div>aloha!</div>',
135+
() => {},
136+
{ delay: 100 }
137+
);
138138

139139
getByText(element, 'Reload').click();
140140
// imitate navigating away

0 commit comments

Comments
 (0)