Skip to content

Send data back and forth as JSON #264

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
->append([__FILE__])
->notPath('#/Fixtures/#')
->notPath('#/app/var/#')
->notPath('#/var/cache/#')
->notPath('Turbo/Attribute/Broadcast.php') // Need https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues/4702
)
;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off"
},
"env": {
"browser": true
Expand Down
5 changes: 5 additions & 0 deletions src/LiveComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## 2.1.0

- Your component's live "data" is now send over Ajax as a JSON string.
Previously data was sent as pure query parameters or as pure POST data.
However, this made it impossible to keep certain data types, like
distinguishing between `null` and `''`. This has no impact on end-users.

- Added `data-live-ignore` attribute. If included in an element, that element
will not be updated on re-render.

Expand Down
76 changes: 14 additions & 62 deletions src/LiveComponent/assets/dist/live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -898,61 +898,6 @@ function combineSpacedArray(parts) {
return finalParts;
}

const buildFormKey = function (key, parentKeys) {
let fieldName = '';
[...parentKeys, key].forEach((name) => {
fieldName += fieldName ? `[${name}]` : name;
});
return fieldName;
};
const addObjectToFormData = function (formData, data, parentKeys) {
Object.keys(data).forEach((key => {
let value = data[key];
if (value === true) {
value = 1;
}
if (value === false) {
value = 0;
}
if (value === null) {
return;
}
if (typeof value === 'object') {
addObjectToFormData(formData, value, [...parentKeys, key]);
return;
}
formData.append(buildFormKey(key, parentKeys), value);
}));
};
const addObjectToSearchParams = function (searchParams, data, parentKeys) {
Object.keys(data).forEach((key => {
let value = data[key];
if (value === true) {
value = 1;
}
if (value === false) {
value = 0;
}
if (value === null) {
return;
}
if (typeof value === 'object') {
addObjectToSearchParams(searchParams, value, [...parentKeys, key]);
return;
}
searchParams.set(buildFormKey(key, parentKeys), value);
}));
};
function buildFormData(data) {
const formData = new FormData();
addObjectToFormData(formData, data, []);
return formData;
}
function buildSearchParams(searchParams, data) {
addObjectToSearchParams(searchParams, data, []);
return searchParams;
}

function setDeepData(data, propertyPath, value) {
const finalData = JSON.parse(JSON.stringify(data));
let currentLevelData = finalData;
Expand Down Expand Up @@ -1200,13 +1145,19 @@ class default_1 extends Controller {
fetchOptions.headers['X-CSRF-TOKEN'] = this.csrfValue;
}
}
if (!action && this._willDataFitInUrl()) {
buildSearchParams(params, this.dataValue);
fetchOptions.method = 'GET';
let dataAdded = false;
if (!action) {
const dataJson = JSON.stringify(this.dataValue);
if (this._willDataFitInUrl(dataJson, params)) {
params.set('data', dataJson);
fetchOptions.method = 'GET';
dataAdded = true;
}
}
else {
if (!dataAdded) {
fetchOptions.method = 'POST';
fetchOptions.body = buildFormData(this.dataValue);
fetchOptions.body = JSON.stringify(this.dataValue);
fetchOptions.headers['Content-Type'] = 'application/json';
}
this._onLoadingStart();
const paramsString = params.toString();
Expand Down Expand Up @@ -1358,8 +1309,9 @@ class default_1 extends Controller {
element.removeAttribute(attribute);
});
}
_willDataFitInUrl() {
return Object.values(this.dataValue).join(',').length < 1500;
_willDataFitInUrl(dataJson, params) {
const urlEncodedJsonData = new URLSearchParams(dataJson).toString();
return (urlEncodedJsonData + params.toString()).length < 1500;
}
_executeMorphdom(newHtml) {
function htmlToElement(html) {
Expand Down
94 changes: 0 additions & 94 deletions src/LiveComponent/assets/src/http_data_helper.ts

This file was deleted.

26 changes: 18 additions & 8 deletions src/LiveComponent/assets/src/live_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Controller } from '@hotwired/stimulus';
import morphdom from 'morphdom';
import { parseDirectives, Directive } from './directives_parser';
import { combineSpacedArray } from './string_utils';
import { buildFormData, buildSearchParams } from './http_data_helper';
import { setDeepData, doesDeepPropertyExist, normalizeModelName } from './set_deep_data';
import { haveRenderedValuesChanged } from './have_rendered_values_changed';
import { normalizeAttributesForComparison } from './normalize_attributes_for_comparison';
Expand Down Expand Up @@ -315,12 +314,21 @@ export default class extends Controller {
}
}

if (!action && this._willDataFitInUrl()) {
buildSearchParams(params, this.dataValue);
fetchOptions.method = 'GET';
} else {
let dataAdded = false;
if (!action) {
const dataJson = JSON.stringify(this.dataValue);
if (this._willDataFitInUrl(dataJson, params)) {
params.set('data', dataJson);
fetchOptions.method = 'GET';
dataAdded = true;
}
}

// if GET can't be used, fallback to POST
if (!dataAdded) {
fetchOptions.method = 'POST';
fetchOptions.body = buildFormData(this.dataValue);
fetchOptions.body = JSON.stringify(this.dataValue);
fetchOptions.headers['Content-Type'] = 'application/json';
}

this._onLoadingStart();
Expand Down Expand Up @@ -531,9 +539,11 @@ export default class extends Controller {
})
}

_willDataFitInUrl() {
_willDataFitInUrl(dataJson: string, params: URLSearchParams) {
const urlEncodedJsonData = new URLSearchParams(dataJson).toString();

// if the URL gets remotely close to 2000 chars, it may not fit
return Object.values(this.dataValue).join(',').length < 1500;
return (urlEncodedJsonData + params.toString()).length < 1500;
}

_executeMorphdom(newHtml: string) {
Expand Down
3 changes: 2 additions & 1 deletion src/LiveComponent/assets/test/controller/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ describe('LiveController Action Tests', () => {
await waitFor(() => expect(element).toHaveTextContent('Comment Saved!'));
expect(getByLabelText(element, 'Comments:')).toHaveValue('hi weaver');

expect(postMock.lastOptions().body.get('comments')).toEqual('hi WEAVER');
const bodyData = JSON.parse(postMock.lastOptions().body);
expect(bodyData.comments).toEqual('hi WEAVER');
});

it('Sends action named args', async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/LiveComponent/assets/test/controller/child.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ describe('LiveController parent -> child component tests', () => {
const inputElement = getByLabelText(element, 'Content:');
await userEvent.clear(inputElement);
await userEvent.type(inputElement, 'changed content');
mockRerender({value: 'changed content'}, childTemplate);
mockRerender({value: 'changed content', error: null}, childTemplate);

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

Expand Down
16 changes: 8 additions & 8 deletions src/LiveComponent/assets/test/controller/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ describe('LiveController data-model Tests', () => {
const data = { name: 'Ryan' };
const { element, controller } = await startStimulus(template(data));

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

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

fetchMock.getOnce('end:?name=Jan', template({ name: 'Jan' }));
mockRerender({name: 'Jan'}, template);

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

Expand All @@ -87,11 +89,9 @@ describe('LiveController data-model Tests', () => {
['guy', 150]
];
requests.forEach(([string, delay]) => {
fetchMock.getOnce(
`end:my_component?name=Ryan${string}`,
template({ name: `Ryan${string}_` }),
{ delay }
);
mockRerender({name: `Ryan${string}`}, template, (data: any) => {
data.name = `Ryan${string}_`;
}, { delay });
});

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

mockRerender({name: 'Ryan WEAVER'}, template, (data) => {
mockRerender({name: 'Ryan WEAVER'}, template, (data: any) => {
data.name = 'Ryan Weaver';
});

Expand Down
Loading