Add configs to choose between ecoding parameters in the URL or request body (#1525)

* Allow extensions to set request bodies for GET and DELETE

Even though the semantics of request bodies for GET and DELETE are
undefined, it is legal to set request bodies for them. By default, GET
and DELETE form parameters should be encoded as URLs. If, however, an
extension defines `encodeParameters`, that should override the default
and set the request bodies for GET and DELETE methods, just like it does
for all the other HTTP methods.

* Don't use URL params by default with DELETE

* Re-enable skipped DELETE test

* Remove reference to DELETE in comment

* Add "useUrlParams" to requestConfig

* Add config.methodsThatUseUrlParams

* Add methodsThatUseUrlParams config tests

* Don't switch to body params based on extension
This commit is contained in:
Alexander Petros 2023-07-10 17:52:01 -04:00 committed by GitHub
parent 18aa2470a0
commit febfa1c6a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 84 additions and 16 deletions

View File

@ -72,6 +72,7 @@ return (function () {
defaultFocusScroll: false,
getCacheBusterParam: false,
globalViewTransitions: false,
methodsThatUseUrlParams: ["get"],
},
parseInterval:parseInterval,
_:internalEval,
@ -2971,8 +2972,12 @@ return (function () {
var requestAttrValues = getValuesForElement(elt, 'hx-request');
var eltIsBoosted = getInternalData(elt).boosted;
var useUrlParams = htmx.config.methodsThatUseUrlParams.indexOf(verb) >= 0
var requestConfig = {
boosted: eltIsBoosted,
useUrlParams: useUrlParams,
parameters: filteredParameters,
unfilteredParameters: allParameters,
headers:headers,
@ -2997,6 +3002,7 @@ return (function () {
headers = requestConfig.headers;
filteredParameters = requestConfig.parameters;
errors = requestConfig.errors;
useUrlParams = requestConfig.useUrlParams;
if(errors && errors.length > 0){
triggerEvent(elt, 'htmx:validation:halted', requestConfig)
@ -3008,26 +3014,25 @@ return (function () {
var splitPath = path.split("#");
var pathNoAnchor = splitPath[0];
var anchor = splitPath[1];
var finalPathForGet = null;
if (verb === 'get' || verb === 'delete') {
finalPathForGet = pathNoAnchor;
var finalPath = path
if (useUrlParams) {
finalPath = pathNoAnchor;
var values = Object.keys(filteredParameters).length !== 0;
if (values) {
if (finalPathForGet.indexOf("?") < 0) {
finalPathForGet += "?";
if (finalPath.indexOf("?") < 0) {
finalPath += "?";
} else {
finalPathForGet += "&";
finalPath += "&";
}
finalPathForGet += urlEncode(filteredParameters);
finalPath += urlEncode(filteredParameters);
if (anchor) {
finalPathForGet += "#" + anchor;
finalPath += "#" + anchor;
}
}
xhr.open(verb.toUpperCase(), finalPathForGet, true);
} else {
xhr.open(verb.toUpperCase(), path, true);
}
xhr.open(verb.toUpperCase(), finalPath, true);
xhr.overrideMimeType("text/html");
xhr.withCredentials = requestConfig.withCredentials;
xhr.timeout = requestConfig.timeout;
@ -3048,7 +3053,7 @@ return (function () {
xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted,
pathInfo: {
requestPath: path,
finalRequestPath: finalPathForGet || path,
finalRequestPath: finalPath,
anchor: anchor
}
};
@ -3123,7 +3128,8 @@ return (function () {
});
});
triggerEvent(elt, 'htmx:beforeSend', responseInfo);
xhr.send(verb === 'get' ? null : encodeParamsForBody(xhr, elt, filteredParameters));
var params = useUrlParams ? null : encodeParamsForBody(xhr, elt, filteredParameters)
xhr.send(params);
return promise;
}

View File

@ -177,5 +177,59 @@ describe("Core htmx Parameter Handling", function() {
vals['do'].should.equal('rey');
})
it('it puts GET params in the URL by default', function () {
this.server.respondWith("GET", "/test?i1=value", function (xhr) {
xhr.respond(200, {}, "Clicked!");
});
var form = make('<form hx-trigger="click" hx-get="/test"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('it puts GET params in the body if methodsThatUseUrlParams is empty', function () {
this.server.respondWith("GET", "/test", function (xhr) {
xhr.requestBody.should.equal("i1=value");
xhr.respond(200, {}, "Clicked!");
});
var form = make('<form hx-trigger="click" hx-get="/test"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
try {
htmx.config.methodsThatUseUrlParams = [];
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
} finally {
htmx.config.methodsThatUseUrlParams = ["get"];
}
});
it('it puts DELETE params in the body by default', function () {
this.server.respondWith("DELETE", "/test", function (xhr) {
xhr.requestBody.should.equal("i1=value");
xhr.respond(200, {}, "Clicked!");
});
var form = make('<form hx-trigger="click" hx-delete="/test"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('it puts DELETE params in the URL if methodsThatUseUrlParams contains "delete"', function () {
this.server.respondWith("DELETE", "/test?i1=value", function (xhr) {
xhr.respond(200, {}, "Clicked!");
});
var form = make('<form hx-trigger="click" hx-delete="/test"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
try {
htmx.config.methodsThatUseUrlParams.push("delete")
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
} finally {
htmx.config.methodsThatUseUrlParams = ["get"];
}
});
});

View File

@ -1,4 +1,3 @@
//
describe("json-enc extension", function() {
beforeEach(function () {
this.server = makeServer();
@ -9,6 +8,15 @@ describe("json-enc extension", function() {
clearWorkArea();
});
it('handles basic get properly', function () {
var jsonResponseBody = JSON.stringify({});
this.server.respondWith("GET", "/test", jsonResponseBody);
var div = make('<div hx-get="/test" hx-ext="json-enc">click me</div>');
div.click();
this.server.respond();
this.server.lastRequest.response.should.equal("{}");
})
it('handles basic post properly', function () {
var jsonResponseBody = JSON.stringify({});
this.server.respondWith("POST", "/test", jsonResponseBody);
@ -67,7 +75,6 @@ describe("json-enc extension", function() {
})
it('handles put with form parameters', function () {
this.server.respondWith("PUT", "/test", function (xhr) {
var values = JSON.parse(xhr.requestBody);
values.should.have.keys("username","password");
@ -109,7 +116,7 @@ describe("json-enc extension", function() {
this.server.lastRequest.response.should.equal('{"passwordok":true}');
})
it.skip('handles delete with form parameters', function () {
it('handles delete with form parameters', function () {
this.server.respondWith("DELETE", "/test", function (xhr) {
var values = JSON.parse(xhr.requestBody);

View File

@ -1517,6 +1517,7 @@ listed below:
| `htmx.config.defaultFocusScroll` | if the focused element should be scrolled into view, defaults to false and can be overridden using the [focus-scroll](@/attributes/hx-swap.md#focus-scroll) swap modifier. |
| `htmx.config.getCacheBusterParam` | defaults to false, if set to true htmx will include a cache-busting parameter in `GET` requests to avoid caching partial responses by the browser |
| `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. |
| `htmx.config.methodsThatUseUrlParams` | defaults to `["get"]`, htmx will format requests with this method by encoding their parameters in the URL, not the request body |
</div>