Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Carson Gross 2023-07-14 13:50:31 -06:00
commit 05a1f8cba7
34 changed files with 3033 additions and 356 deletions

View File

@ -1,22 +0,0 @@
version: 2.1
commands:
orbs:
browser-tools: circleci/browser-tools@1.1.0
jobs:
test:
docker:
- image: cimg/node:16.13.1-browsers
steps:
- browser-tools/install-browser-tools
- checkout
- run: |
node --version
java --version
google-chrome --version
workflows:
tests-containers:
jobs:
- test

19
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Node CI
on:
push:
branches: [ master, dev, htmx-2.0 ]
pull_request:
branches: [ master, dev, htmx-2.0 ]
jobs:
test_suite:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '15.x'
- run: npm ci
- run: npm test

View File

@ -1,6 +1,8 @@
# Changelog # Changelog
## [1.9.3] - 2023-04-28 ## [1.9.3] - 2023-06-??
* Fixed bug w/ WebSocket extension initilization caused by "naked" `hx-trigger` feature
## [1.9.2] - 2023-04-28 ## [1.9.2] - 2023-04-28

2306
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,9 @@
"types": "dist/htmx.d.ts", "types": "dist/htmx.d.ts",
"unpkg": "dist/htmx.min.js", "unpkg": "dist/htmx.min.js",
"web-types": "editors/jetbrains/htmx.web-types.json", "web-types": "editors/jetbrains/htmx.web-types.json",
"engines": {
"node": "15.x"
},
"scripts": { "scripts": {
"test": "mocha-chrome test/index.html", "test": "mocha-chrome test/index.html",
"test-types": "tsc --project ./jsconfig.json", "test-types": "tsc --project ./jsconfig.json",

View File

@ -48,9 +48,12 @@ htmx.defineExtension("preload", {
// in the future // in the future
var hxGet = node.getAttribute("hx-get") || node.getAttribute("data-hx-get") var hxGet = node.getAttribute("hx-get") || node.getAttribute("data-hx-get")
if (hxGet) { if (hxGet) {
htmx.ajax("GET", hxGet, {handler:function(elt, info) { htmx.ajax("GET", hxGet, {
source: node,
handler:function(elt, info) {
done(info.xhr.responseText); done(info.xhr.responseText);
}}); }
});
return; return;
} }

View File

@ -52,7 +52,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
return; return;
// Try to create websockets when elements are processed // Try to create websockets when elements are processed
case "htmx:afterProcessNode": case "htmx:beforeProcessNode":
var parent = evt.target; var parent = evt.target;
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) { forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {

View File

@ -44,6 +44,7 @@ return (function () {
defineExtension : defineExtension, defineExtension : defineExtension,
removeExtension : removeExtension, removeExtension : removeExtension,
logAll : logAll, logAll : logAll,
logNone : logNone,
logger : null, logger : null,
config : { config : {
historyEnabled:true, historyEnabled:true,
@ -71,6 +72,7 @@ return (function () {
defaultFocusScroll: false, defaultFocusScroll: false,
getCacheBusterParam: false, getCacheBusterParam: false,
globalViewTransitions: false, globalViewTransitions: false,
methodsThatUseUrlParams: ["get"],
}, },
parseInterval:parseInterval, parseInterval:parseInterval,
_:internalEval, _:internalEval,
@ -478,6 +480,10 @@ return (function () {
} }
} }
function logNone() {
htmx.logger = null
}
function find(eltOrSelector, selector) { function find(eltOrSelector, selector) {
if (selector) { if (selector) {
return eltOrSelector.querySelector(selector); return eltOrSelector.querySelector(selector);
@ -905,6 +911,17 @@ return (function () {
return hash; return hash;
} }
function deInitOnHandlers(elt) {
var internalData = getInternalData(elt);
if (internalData.onHandlers) {
for (let i = 0; i < internalData.onHandlers.length; i++) {
const handlerInfo = internalData.onHandlers[i];
elt.removeEventListener(handlerInfo.name, handlerInfo.handler);
}
delete internalData.onHandlers
}
}
function deInitNode(element) { function deInitNode(element) {
var internalData = getInternalData(element); var internalData = getInternalData(element);
if (internalData.timeout) { if (internalData.timeout) {
@ -923,12 +940,7 @@ return (function () {
} }
}); });
} }
if (internalData.onHandlers) { deInitOnHandlers(element);
for (let i = 0; i < internalData.onHandlers.length; i++) {
const handlerInfo = internalData.onHandlers[i];
element.removeEventListener(handlerInfo.name, handlerInfo.handler);
}
}
} }
function cleanUpElement(element) { function cleanUpElement(element) {
@ -953,7 +965,7 @@ return (function () {
newElt = eltBeforeNewContent.nextSibling; newElt = eltBeforeNewContent.nextSibling;
} }
getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later
settleInfo.elts = [] // clear existing elements settleInfo.elts = settleInfo.elts.filter(e => e != target);
while(newElt && newElt !== target) { while(newElt && newElt !== target) {
if (newElt.nodeType === Node.ELEMENT_NODE) { if (newElt.nodeType === Node.ELEMENT_NODE) {
settleInfo.elts.push(newElt); settleInfo.elts.push(newElt);
@ -1656,6 +1668,9 @@ return (function () {
var sseEventSource = getInternalData(sseSourceElt).sseEventSource; var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
var sseListener = function (event) { var sseListener = function (event) {
if (maybeCloseSSESource(sseSourceElt)) { if (maybeCloseSSESource(sseSourceElt)) {
return;
}
if (!bodyContains(elt)) {
sseEventSource.removeEventListener(sseEventName, sseListener); sseEventSource.removeEventListener(sseEventName, sseListener);
return; return;
} }
@ -1672,7 +1687,7 @@ return (function () {
var target = getTarget(elt) var target = getTarget(elt)
var settleInfo = makeSettleInfo(elt); var settleInfo = makeSettleInfo(elt);
selectAndSwap(swapSpec.swapStyle, elt, target, response, settleInfo) selectAndSwap(swapSpec.swapStyle, target, elt, response, settleInfo)
settleImmediately(settleInfo.tasks) settleImmediately(settleInfo.tasks)
triggerEvent(elt, "htmx:sseMessage", event) triggerEvent(elt, "htmx:sseMessage", event)
}; };
@ -1826,6 +1841,16 @@ return (function () {
return document.querySelector("[hx-boost], [data-hx-boost]"); return document.querySelector("[hx-boost], [data-hx-boost]");
} }
function findHxOnWildcardElements(elt) {
if (!document.evaluate) return []
let node = null
const elements = []
const iter = document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]', elt)
while (node = iter.iterateNext()) elements.push(node)
return elements
}
function findElementsToProcess(elt) { function findElementsToProcess(elt) {
if (elt.querySelectorAll) { if (elt.querySelectorAll) {
var boostedElts = hasChanceOfBeingBoosted() ? ", a, form" : ""; var boostedElts = hasChanceOfBeingBoosted() ? ", a, form" : "";
@ -1909,6 +1934,22 @@ return (function () {
} }
} }
function processHxOnWildcard(elt) {
// wipe any previous on handlers so that this function takes precedence
deInitOnHandlers(elt)
for (const attr of elt.attributes) {
const { name, value } = attr
if (name.startsWith("hx-on:") || name.startsWith("data-hx-on:")) {
let eventName = name.slice(name.indexOf(":") + 1)
// if the eventName starts with a colon, prepend "htmx" for shorthand support
if (eventName.startsWith(":")) eventName = "htmx" + eventName
addHxOnEventHandler(elt, eventName, value)
}
}
}
function initNode(elt) { function initNode(elt) {
if (elt.closest && elt.closest(htmx.config.disableSelector)) { if (elt.closest && elt.closest(htmx.config.disableSelector)) {
return; return;
@ -1965,6 +2006,9 @@ return (function () {
elt = resolveTarget(elt); elt = resolveTarget(elt);
initNode(elt); initNode(elt);
forEach(findElementsToProcess(elt), function(child) { initNode(child) }); forEach(findElementsToProcess(elt), function(child) { initNode(child) });
// Because it happens second, the new way of adding onHandlers superseeds the old one
// i.e. if there are any hx-on:eventName attributes, the hx-on attribute will be ignored
forEach(findHxOnWildcardElements(elt), processHxOnWildcard);
} }
//==================================================================== //====================================================================
@ -2933,8 +2977,12 @@ return (function () {
var requestAttrValues = getValuesForElement(elt, 'hx-request'); var requestAttrValues = getValuesForElement(elt, 'hx-request');
var eltIsBoosted = getInternalData(elt).boosted; var eltIsBoosted = getInternalData(elt).boosted;
var useUrlParams = htmx.config.methodsThatUseUrlParams.indexOf(verb) >= 0
var requestConfig = { var requestConfig = {
boosted: eltIsBoosted, boosted: eltIsBoosted,
useUrlParams: useUrlParams,
parameters: filteredParameters, parameters: filteredParameters,
unfilteredParameters: allParameters, unfilteredParameters: allParameters,
headers:headers, headers:headers,
@ -2959,6 +3007,7 @@ return (function () {
headers = requestConfig.headers; headers = requestConfig.headers;
filteredParameters = requestConfig.parameters; filteredParameters = requestConfig.parameters;
errors = requestConfig.errors; errors = requestConfig.errors;
useUrlParams = requestConfig.useUrlParams;
if(errors && errors.length > 0){ if(errors && errors.length > 0){
triggerEvent(elt, 'htmx:validation:halted', requestConfig) triggerEvent(elt, 'htmx:validation:halted', requestConfig)
@ -2970,26 +3019,25 @@ return (function () {
var splitPath = path.split("#"); var splitPath = path.split("#");
var pathNoAnchor = splitPath[0]; var pathNoAnchor = splitPath[0];
var anchor = splitPath[1]; var anchor = splitPath[1];
var finalPathForGet = null;
if (verb === 'get') { var finalPath = path
finalPathForGet = pathNoAnchor; if (useUrlParams) {
finalPath = pathNoAnchor;
var values = Object.keys(filteredParameters).length !== 0; var values = Object.keys(filteredParameters).length !== 0;
if (values) { if (values) {
if (finalPathForGet.indexOf("?") < 0) { if (finalPath.indexOf("?") < 0) {
finalPathForGet += "?"; finalPath += "?";
} else { } else {
finalPathForGet += "&"; finalPath += "&";
} }
finalPathForGet += urlEncode(filteredParameters); finalPath += urlEncode(filteredParameters);
if (anchor) { if (anchor) {
finalPathForGet += "#" + anchor; finalPath += "#" + anchor;
} }
} }
xhr.open('GET', finalPathForGet, true);
} else {
xhr.open(verb.toUpperCase(), path, true);
} }
xhr.open(verb.toUpperCase(), finalPath, true);
xhr.overrideMimeType("text/html"); xhr.overrideMimeType("text/html");
xhr.withCredentials = requestConfig.withCredentials; xhr.withCredentials = requestConfig.withCredentials;
xhr.timeout = requestConfig.timeout; xhr.timeout = requestConfig.timeout;
@ -3010,7 +3058,7 @@ return (function () {
xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted, xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted,
pathInfo: { pathInfo: {
requestPath: path, requestPath: path,
finalRequestPath: finalPathForGet || path, finalRequestPath: finalPath,
anchor: anchor anchor: anchor
} }
}; };
@ -3085,7 +3133,8 @@ return (function () {
}); });
}); });
triggerEvent(elt, 'htmx:beforeSend', responseInfo); 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; return promise;
} }

View File

@ -1,6 +1,5 @@
describe("hx-boost attribute", function() { describe("hx-boost attribute", function() {
htmx.logAll();
beforeEach(function () { beforeEach(function () {
this.server = makeServer(); this.server = makeServer();
clearWorkArea(); clearWorkArea();

View File

@ -0,0 +1,133 @@
describe("hx-on:* attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it("can handle basic events w/ no other attributes", function () {
var btn = make("<button hx-on:click='window.foo = true'>Foo</button>");
btn.click();
window.foo.should.equal(true);
delete window.foo;
});
it("can modify a parameter via htmx:configRequest", function () {
this.server.respondWith("POST", "/test", function (xhr) {
var params = parseParams(xhr.requestBody);
xhr.respond(200, {}, params.foo);
});
var btn = make("<button hx-on:htmx:config-request='event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("bar");
});
it("expands :: shorthand into htmx:", function () {
this.server.respondWith("POST", "/test", function (xhr) {
var params = parseParams(xhr.requestBody);
xhr.respond(200, {}, params.foo);
});
var btn = make("<button hx-on::config-request='event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("bar");
});
it("can cancel an event via preventDefault for htmx:config-request", function () {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var btn = make("<button hx-on:htmx:config-request='event.preventDefault()' hx-post='/test' hx-swap='outerHTML'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("Foo");
});
it("can respond to data-hx-on", function () {
this.server.respondWith("POST", "/test", function (xhr) {
var params = parseParams(xhr.requestBody);
xhr.respond(200, {}, params.foo);
});
var btn = make("<button data-hx-on:htmx:config-request='event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("bar");
});
it("has the this symbol set to the element", function () {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "foo");
});
var btn = make("<button hx-on:htmx:config-request='window.elt = this' hx-post='/test'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("foo");
btn.should.equal(window.elt);
delete window.elt;
});
it("can handle multi-line JSON", function () {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "foo");
});
var btn = make("<button hx-on:htmx:config-request='window.elt = {foo: true,\n" +
" bar: false}' hx-post='/test'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("foo");
var obj = {foo: true, bar: false};
obj.should.deep.equal(window.elt);
delete window.elt;
});
it("can handle multiple event handlers in the presence of multi-line JSON", function () {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "foo");
});
var btn = make("<button hx-on:htmx:config-request='window.elt = {foo: true,\n" +
" bar: false}\n'" +
" hx-on:htmx:after-request='window.foo = true'" +
" hx-post='/test'>Foo</button>");
btn.click();
this.server.respond();
btn.innerText.should.equal("foo");
var obj = {foo: true, bar: false};
obj.should.deep.equal(window.elt);
delete window.elt;
window.foo.should.equal(true);
delete window.foo;
});
it("de-initializes hx-on-* content properly", function () {
window.tempCount = 0;
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button id='foo' hx-on:click=\"window.tempCount++;\">increment</button>");
});
var div = make("<div hx-post='/test'>Foo</div>");
// get response
div.click();
this.server.respond();
// click button
byId('foo').click();
window.tempCount.should.equal(1);
// get second response
div.click();
this.server.respond();
// click button again
byId('foo').click();
window.tempCount.should.equal(2);
delete window.tempCount;
});
});

View File

@ -241,13 +241,13 @@ describe("Core htmx API test", function(){
div3.classList.contains("foo").should.equal(true); div3.classList.contains("foo").should.equal(true);
}); });
it('logAll works', function () { it('logAll and logNone works', function () {
var initialLogger = htmx.config.logger var initialLogger = htmx.logger
try {
htmx.logAll(); htmx.logAll();
} finally { htmx.logger.should.not.equal(null);
htmx.config.logger = initialLogger; htmx.logNone();
} should.equal(htmx.logger, null);
htmx.logger = initialLogger;
}); });
it('eval can be suppressed', function () { it('eval can be suppressed', function () {

View File

@ -305,6 +305,25 @@ describe("Core htmx Events", function() {
} }
}); });
it("htmx:afterSettle is called multiple times when doing OOB outerHTML swaps", function () {
var called = 0;
var handler = htmx.on("htmx:afterSettle", function (evt) {
called++;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>\n <div id='t1' hx-swap-oob='true'>t1</div><div id='t2' hx-swap-oob='true'>t2</div>");
});
var div = make("<button id='button' hx-post='/test' hx-target='#t'>Foo</button><div id='t'></div><div id='t1'></div><div id='t2'></div>");
var button = byId("button")
button.click();
this.server.respond();
should.equal(called, 3);
} finally {
htmx.off("htmx:afterSettle", handler);
}
});
it("htmx:afterRequest is called after a successful request", function () { it("htmx:afterRequest is called after a successful request", function () {
var called = false; var called = false;
var handler = htmx.on("htmx:afterRequest", function (evt) { var handler = htmx.on("htmx:afterRequest", function (evt) {

View File

@ -177,5 +177,59 @@ describe("Core htmx Parameter Handling", function() {
vals['do'].should.equal('rey'); 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

@ -130,7 +130,6 @@ describe("Core htmx Regression Tests", function(){
it('a form can reset based on the htmx:afterRequest event', function() { it('a form can reset based on the htmx:afterRequest event', function() {
this.server.respondWith("POST", "/test", "posted"); this.server.respondWith("POST", "/test", "posted");
//htmx.logAll();
var form = make('<div id="d1"></div><form _="on htmx:afterRequest reset() me" hx-post="/test" hx-target="#d1">' + var form = make('<div id="d1"></div><form _="on htmx:afterRequest reset() me" hx-post="/test" hx-target="#d1">' +
' <input type="text" name="input" id="i1"/>' + ' <input type="text" name="input" id="i1"/>' +
@ -174,7 +173,6 @@ describe("Core htmx Regression Tests", function(){
it("supports unset on hx-select", function(){ it("supports unset on hx-select", function(){
this.server.respondWith("GET", "/test", "Foo<span id='example'>Bar</span>"); this.server.respondWith("GET", "/test", "Foo<span id='example'>Bar</span>");
htmx.logAll();
make('<form hx-select="#example">\n' + make('<form hx-select="#example">\n' +
' <button id="b1" hx-select="unset" hx-get="/test">Initial</button>\n' + ' <button id="b1" hx-select="unset" hx-get="/test">Initial</button>\n' +
'</form>') '</form>')

View File

@ -1,4 +1,3 @@
//
describe("json-enc extension", function() { describe("json-enc extension", function() {
beforeEach(function () { beforeEach(function () {
this.server = makeServer(); this.server = makeServer();
@ -9,6 +8,15 @@ describe("json-enc extension", function() {
clearWorkArea(); 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 () { it('handles basic post properly', function () {
var jsonResponseBody = JSON.stringify({}); var jsonResponseBody = JSON.stringify({});
this.server.respondWith("POST", "/test", jsonResponseBody); this.server.respondWith("POST", "/test", jsonResponseBody);
@ -67,7 +75,6 @@ describe("json-enc extension", function() {
}) })
it('handles put with form parameters', function () { it('handles put with form parameters', function () {
this.server.respondWith("PUT", "/test", function (xhr) { this.server.respondWith("PUT", "/test", function (xhr) {
var values = JSON.parse(xhr.requestBody); var values = JSON.parse(xhr.requestBody);
values.should.have.keys("username","password"); values.should.have.keys("username","password");

View File

@ -77,6 +77,17 @@ describe("web-sockets extension", function () {
this.messages.length.should.equal(1); this.messages.length.should.equal(1);
}) })
it('sends data to the server with specific trigger', function () {
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div hx-trigger="click" ws-send id="d1">div1</div></div>');
this.tickMock();
byId("d1").click();
this.tickMock();
this.messages.length.should.equal(1);
})
it('handles message from the server', function () { it('handles message from the server', function () {
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div><div id="d2">div2</div></div>'); var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div><div id="d2">div2</div></div>');
this.tickMock(); this.tickMock();

View File

@ -67,6 +67,7 @@
<script src="attributes/hx-indicator.js"></script> <script src="attributes/hx-indicator.js"></script>
<script src="attributes/hx-disinherit.js"></script> <script src="attributes/hx-disinherit.js"></script>
<script src="attributes/hx-on.js"></script> <script src="attributes/hx-on.js"></script>
<script src="attributes/hx-on-wildcard.js"></script>
<script src="attributes/hx-params.js"></script> <script src="attributes/hx-params.js"></script>
<script src="attributes/hx-patch.js"></script> <script src="attributes/hx-patch.js"></script>
<script src="attributes/hx-post.js"></script> <script src="attributes/hx-post.js"></script>

View File

@ -38,7 +38,8 @@ function getWorkArea() {
} }
function clearWorkArea() { function clearWorkArea() {
getWorkArea().innerHTML = ""; const workArea = getWorkArea();
if (workArea) workArea.innerHTML = "";
} }
function removeWhiteSpace(str) { function removeWhiteSpace(str) {

View File

@ -240,6 +240,16 @@ Log all htmx events, useful for debugging.
htmx.logAll(); htmx.logAll();
``` ```
### Method - `htmx.logNone()` {#logNone}
Log no htmx events, call this to turn off the debugger if you previously enabled it.
##### Example
```js
htmx.logNone();
```
### Property - `htmx.logger` {#logger} ### Property - `htmx.logger` {#logger}
The logger htmx uses to log with The logger htmx uses to log with

View File

@ -7,7 +7,8 @@ added to it for the duration of the request. This can be used to show spinners o
while the request is in flight. while the request is in flight.
The value of this attribute is a CSS query selector of the element or elements to apply the class to, The value of this attribute is a CSS query selector of the element or elements to apply the class to,
or the keyword `closest`, followed by a CSS selector, which will find the closest matching parent (e.g. `closest tr`); or the keyword [`closest`](https://developer.mozilla.org/docs/Web/API/Element/closest), followed by a CSS selector,
which will find the closest ancestor element or itself, that matches the given CSS selector (e.g. `closest tr`);
Here is an example with a spinner adjacent to the button: Here is an example with a spinner adjacent to the button:

View File

@ -6,13 +6,54 @@ The `hx-on` attribute allows you to embed scripts inline to respond to events di
`hx-on` improves upon `onevent` by enabling the handling of any event for enhanced [Locality of Behaviour (LoB)](/essays/locality-of-behaviour/). This also enables you to handle any htmx event. `hx-on` improves upon `onevent` by enabling the handling of any event for enhanced [Locality of Behaviour (LoB)](/essays/locality-of-behaviour/). This also enables you to handle any htmx event.
There are two forms of this attribute, one in which you specify the event as part of the attribute name
after a colon (`hx-on:click`, for example), and one that uses the `hx-on` attribute directly. The
latter form should only be used if IE11 support is required.
### Forms
#### hx-on:* (recommended)
The event name follows a colon `:` in the attribute, and the attribute value is the script to be executed:
```html
<div hx-on:click="alert('Clicked!')">Click</div>
```
All htmx events can be captured, too! Make sure to use the [kebab-case event name](@/docs.md#events),
because DOM attributes do not preserve casing.
To make writing these a little easier, you can use the shorthand double-colon `hx-on::` for htmx
events, and omit the "htmx" part:
```html
<!-- These two are equivalent -->
<button hx-get="/info" hx-on:htmx:before-request="alert('Making a request!')">
Get Info!
</button>
<button hx-get="/info" hx-on::before-request="alert('Making a request!')">
Get Info!
</button>
```
Adding multiple handlers is easy, you just specify additional attributes:
```html
<button hx-get="/info"
hx-on::beforeRequest="alert('Making a request!'")
hx-on::afterRequest="alert('Done making a request!')">
Get Info!
</button>
```
#### hx-on (deprecated, except for IE11 support)
The value is an event name, followed by a colon `:`, followed by the script: The value is an event name, followed by a colon `:`, followed by the script:
```html ```html
<div hx-on="click: alert('Clicked!')">Click</div> <div hx-on="click: alert('Clicked!')">Click</div>
``` ```
All htmx events can be captured, too! And htmx events:
```html ```html
<button hx-get="/info" hx-on="htmx:beforeRequest: alert('Making a request!')"> <button hx-get="/info" hx-on="htmx:beforeRequest: alert('Making a request!')">
@ -20,6 +61,15 @@ All htmx events can be captured, too!
</button> </button>
``` ```
Multiple handlers can be defined by putting them on new lines:
```html
<button hx-get="/info" hx-on="htmx:beforeRequest: alert('Making a request!')
htmx:afterRequest: alert('Done making a request!')">
Get Info!
</button>
```
### Symbols ### Symbols
Like `onevent`, two symbols are made available to event handler scripts: Like `onevent`, two symbols are made available to event handler scripts:
@ -27,19 +77,10 @@ Like `onevent`, two symbols are made available to event handler scripts:
* `this` - The element on which the `hx-on` attribute is defined * `this` - The element on which the `hx-on` attribute is defined
* `event` - The event that triggered the handler * `event` - The event that triggered the handler
### Multiple Handlers
Multiple handlers can be defined by putting them on new lines:
```html
<button hx-get="/info" hx-on="htmx:beforeRequest: alert('Making a request!')
htmx:afterRequest: alert('Done making a request!')">
Get Info!
</button>
```
### Notes ### Notes
* `hx-on` is _not_ inherited, however due to * `hx-on` is _not_ inherited, however due to
[event bubbling](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling_and_capture), [event bubbling](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling_and_capture),
`hx-on` attributes on parent elements will typically be triggered by events on child elements `hx-on` attributes on parent elements will typically be triggered by events on child elements
* `hx-on:*` and `hx-on` cannot be used together on the same element; if `hx-on:*` is present, the value of an `hx-on` attribute
on the same element will be ignored. The two forms can be mixed in the same document, however.

View File

@ -7,7 +7,8 @@ request. The value of this attribute can be:
* A CSS query selector of the element to target. * A CSS query selector of the element to target.
* `this` which indicates that the element that the `hx-target` attribute is on is the target. * `this` which indicates that the element that the `hx-target` attribute is on is the target.
* `closest <CSS selector>` which will find the closest parent ancestor that matches the given CSS selector * `closest <CSS selector>` which will find the [closest](https://developer.mozilla.org/docs/Web/API/Element/closest)
ancestor element or itself, that matches the given CSS selector
(e.g. `closest tr` will target the closest table row to the element). (e.g. `closest tr` will target the closest table row to the element).
* `find <CSS selector>` which will find the first child descendant element that matches the given CSS selector. * `find <CSS selector>` which will find the first child descendant element that matches the given CSS selector.
* `next <CSS selector>` which will scan the DOM forward for the first element that matches the given CSS selector. * `next <CSS selector>` which will scan the DOM forward for the first element that matches the given CSS selector.

View File

@ -58,7 +58,7 @@ is seen again before the delay completes it is ignored, the element will trigger
* The extended CSS selector here allows for the following non-standard CSS values: * The extended CSS selector here allows for the following non-standard CSS values:
* `document` - listen for events on the document * `document` - listen for events on the document
* `window` - listen for events on the window * `window` - listen for events on the window
* `closest <CSS selector>` - finds the closest parent matching the given css selector * `closest <CSS selector>` - finds the [closest](https://developer.mozilla.org/docs/Web/API/Element/closest) ancestor element or itself, matching the given css selector
* `find <CSS selector>` - finds the closest child matching the given css selector * `find <CSS selector>` - finds the closest child matching the given css selector
* `target:<CSS selector>` - allows you to filter via a CSS selector on the target of the event. This can be useful when you want to listen for * `target:<CSS selector>` - allows you to filter via a CSS selector on the target of the event. This can be useful when you want to listen for
triggers from elements that might not be in the DOM at the point of initialization, by, for example, listening on the body, triggers from elements that might not be in the DOM at the point of initialization, by, for example, listening on the body,

View File

@ -392,7 +392,8 @@ input tag.
`hx-target`, and most attributes that take a CSS selector, support an "extended" CSS syntax: `hx-target`, and most attributes that take a CSS selector, support an "extended" CSS syntax:
* You can use the `this` keyword, which indicates that the element that the `hx-target` attribute is on is the target * You can use the `this` keyword, which indicates that the element that the `hx-target` attribute is on is the target
* The `closest <CSS selector>` syntax will find the closest parent ancestor that matches the given CSS selector. * The `closest <CSS selector>` syntax will find the [closest](https://developer.mozilla.org/docs/Web/API/Element/closest)
ancestor element or itself, that matches the given CSS selector.
(e.g. `closest tr` will target the closest table row to the element) (e.g. `closest tr` will target the closest table row to the element)
* The `next <CSS selector>` syntax will find the next element in the DOM matching the given CSS selector. * The `next <CSS selector>` syntax will find the next element in the DOM matching the given CSS selector.
* The `previous <CSS selector>` syntax will find the previous element in the DOM the given CSS selector. * The `previous <CSS selector>` syntax will find the previous element in the DOM the given CSS selector.
@ -1252,7 +1253,7 @@ with an `on*` property, but can be done using the `hx-on` attribute:
```html ```html
<button hx-post="/example" <button hx-post="/example"
hx-on="htmx:beforeRequest: event.detail.parameters.example = 'Hello Scripting!'"> hx-on="htmx:configRequest: event.detail.parameters.example = 'Hello Scripting!'">
Post Me! Post Me!
</button> </button>
``` ```
@ -1516,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.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.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.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> </div>

View File

@ -178,7 +178,7 @@ is a form that on submit will change its look to indicate that a request is bein
transition: opacity 300ms linear; transition: opacity 300ms linear;
} }
</style> </style>
<form hx-post="/name"> <form hx-post="/name" hx-swap="outerHTML">
<label>Name:</label><input name="name"><br/> <label>Name:</label><input name="name"><br/>
<button>Submit</button> <button>Submit</button>
</form> </form>
@ -193,10 +193,12 @@ is a form that on submit will change its look to indicate that a request is bein
} }
</style> </style>
<form hx-post="/name"> <div aria-live="polite">
<form hx-post="/name" hx-swap="outerHTML">
<label>Name:</label><input name="name"><br/> <label>Name:</label><input name="name"><br/>
<button>Submit</button> <button>Submit</button>
</form> </form>
</div>
<script> <script>
onPost("/name", function(){ return "Submitted!"; }); onPost("/name", function(){ return "Submitted!"; });

View File

@ -9,8 +9,8 @@ values in `PUT`'s to two different endpoints: `activate` and `deactivate`:
```html ```html
<div hx-include="#checked-contacts" hx-target="#tbody"> <div hx-include="#checked-contacts" hx-target="#tbody">
<a class="btn" hx-put="/activate">Activate</a> <button class="btn" hx-put="/activate">Activate</button>
<a class="btn" hx-put="/deactivate">Deactivate</a> <button class="btn" hx-put="/deactivate">Deactivate</button>
</div> </div>
<form id="checked-contacts"> <form id="checked-contacts">
@ -126,7 +126,7 @@ You can see a working example of this code below.
// templates // templates
function displayUI(contacts) { function displayUI(contacts) {
return `<h3>Select Rows And Activate Or Deactivate Below<h3> return `<h3>Select Rows And Activate Or Deactivate Below</h3>
<form id="checked-contacts"> <form id="checked-contacts">
<table> <table>
<thead> <thead>
@ -145,8 +145,8 @@ You can see a working example of this code below.
<br/> <br/>
<br/> <br/>
<div hx-include="#checked-contacts" hx-target="#tbody"> <div hx-include="#checked-contacts" hx-target="#tbody">
<a class="btn" hx-put="/activate">Activate</a> <button class="btn" hx-put="/activate">Activate</button>
<a class="btn" hx-put="/deactivate">Deactivate</a> <button class="btn" hx-put="/deactivate">Deactivate</button>
</div>` </div>`
} }

View File

@ -75,18 +75,18 @@ The click to edit pattern provides a way to offer inline editing of all or part
function formTemplate(contact) { function formTemplate(contact) {
return `<form hx-put="/contact/1" hx-target="this" hx-swap="outerHTML"> return `<form hx-put="/contact/1" hx-target="this" hx-swap="outerHTML">
<div> <div>
<label>First Name</label> <label for="firstName">First Name</label>
<input type="text" name="firstName" value="${contact.firstName}"> <input autofocus type="text" id="firstName" name="firstName" value="${contact.firstName}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Last Name</label> <label for="lastName">Last Name</label>
<input type="text" name="lastName" value="${contact.lastName}"> <input type="text" id="lastName" name="lastName" value="${contact.lastName}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Email Address</label> <label for="email">Email Address</label>
<input type="email" name="email" value="${contact.email}"> <input type="email" id="email" name="email" value="${contact.email}">
</div> </div>
<button class="btn">Submit</button> <button class="btn" type="submit">Submit</button>
<button class="btn" hx-get="/contact/1">Cancel</button> <button class="btn" hx-get="/contact/1">Cancel</button>
</form>` </form>`
} }

View File

@ -72,7 +72,7 @@ results (which will contain a button to load the *next* page of results). And s
var txt = ""; var txt = "";
for (var i = 0; i < contacts.length; i++) { for (var i = 0; i < contacts.length; i++) {
var c = contacts[i]; var c = contacts[i];
txt += "<tr><td>" + c.name + "</td><td>" + c.email + "</td><td>" + c.id + "</td></tr>\n"; txt += `<tr><td>${c.name}</td><td>${c.email}</td><td>${c.id}</td></tr>\n`;
} }
txt += loadMoreRow(page); txt += loadMoreRow(page);
return txt; return txt;

View File

@ -113,28 +113,28 @@ Below is a working demo of this example. The only email that will be accepted i
function formTemplate() { function formTemplate() {
return `<form hx-post="/contact"> return `<form hx-post="/contact">
<div hx-target="this" hx-swap="outerHTML"> <div hx-target="this" hx-swap="outerHTML">
<label>Email Address</label> <label for="email">Email Address</label>
<input name="email" hx-post="/contact/email" hx-indicator="#ind"> <input name="email" id="email" hx-post="/contact/email" hx-indicator="#ind">
<img id="ind" src="/img/bars.svg" class="htmx-indicator"/> <img id="ind" src="/img/bars.svg" class="htmx-indicator"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>First Name</label> <label for="firstName">First Name</label>
<input type="text" class="form-control" name="firstName"> <input type="text" class="form-control" name="firstName" id="firstName">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Last Name</label> <label for="lastName">Last Name</label>
<input type="text" class="form-control" name="lastName"> <input type="text" class="form-control" name="lastName" id="lastName">
</div> </div>
<button class="btn btn-default" disabled>Submit</button> <button type='submit' class="btn btn-default" disabled>Submit</button>
</form>`; </form>`;
} }
function emailInputTemplate(val, errorMsg) { function emailInputTemplate(val, errorMsg) {
return `<div hx-target="this" hx-swap="outerHTML" class="${errorMsg ? "error" : "valid"}"> return `<div hx-target="this" hx-swap="outerHTML" class="${errorMsg ? "error" : "valid"}">
<label>Email Address</label> <label>Email Address</label>
<input name="email" hx-post="/contact/email" hx-indicator="#ind" value="${val}"> <input name="email" hx-post="/contact/email" hx-indicator="#ind" value="${val}" aria-invalid="${!!errorMsg}">
<img id="ind" src="/img/bars.svg" class="htmx-indicator"/> <img id="ind" src="/img/bars.svg" class="htmx-indicator"/>
${errorMsg ? ("<div class='error-message'>" + errorMsg + "</div>") : ""} ${errorMsg ? (`<div class='error-message' >${errorMsg}</div>`) : ""}
</div>`; </div>`;
} }
</script> </script>

View File

@ -16,37 +16,48 @@ We start with an initial state with a button that issues a `POST` to `/start` to
</div> </div>
``` ```
This div is then replaced with a new div that reloads itself every 600ms: This div is then replaced with a new div containing status and a progress bar that reloads itself every 600ms:
```html ```html
<div hx-target="this" <div hx-trigger="done" hx-get="/job" hx-swap="outerHTML" hx-target="this">
hx-get="/job" <h3 role="status" id="pblabel" tabindex="-1" autofocus>Running</h3>
hx-trigger="load delay:600ms"
hx-swap="outerHTML"> <div
<h3>Running</h3> hx-get="/job/progress"
<div class="progress"> hx-trigger="every 600ms"
<div id="pb" class="progress-bar" style="width:0%"></div> hx-target="this"
hx-swap="innerHTML">
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-labelledby="pblabel">
<div id="pb" class="progress-bar" style="width:0%">
</div> </div>
</div> </div>
</div>
``` ```
This HTML is rerendered every 600 milliseconds, with the "width" style attribute on the progress bar being updated. This progress bar is updated every 600 milliseconds, with the "width" style attribute and `aria-valuenow` attributed set to current progress value.
Because there is an id on the progress bar div, htmx will smoothly transition between requests by settling the Because there is an id on the progress bar div, htmx will smoothly transition between requests by settling the
style attribute into its new value. This, when coupled with CSS transitions, make the visual transition continuous style attribute into its new value. This, when coupled with CSS transitions, makes the visual transition continuous
rather than jumpy. rather than jumpy.
Finally, when the process is complete, a restart button is added to the UI (we are using the [`class-tools`](@/extensions/class-tools.md) Finally, when the process is complete, a server returns `HX-Trigger: done` header, which triggers an update of the UI to "Complete" state
extension in this example): with a restart button added to the UI (we are using the [`class-tools`](@/extensions/class-tools.md) extension in this example to add fade-in effect on the button):
```html ```html
<div hx-target="this" <div hx-trigger="done" hx-get="/job" hx-swap="outerHTML" hx-target="this">
hx-get="/job" <h3 role="status" id="pblabel" tabindex="-1" autofocus>Complete</h3>
<div
hx-get="/job/progress"
hx-trigger="none" hx-trigger="none"
hx-swap="outerHTML"> hx-target="this"
<h3>Complete</h3> hx-swap="innerHTML">
<div class="progress"> <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="122" aria-labelledby="pblabel">
<div id="pb" class="progress-bar" style="width:100%"></div> <div id="pb" class="progress-bar" style="width:122%">
</div> </div>
</div>
</div>
<button id="restart-btn" class="btn" hx-post="/start" classes="add show:600ms"> <button id="restart-btn" class="btn" hx-post="/start" classes="add show:600ms">
Restart Job Restart Job
</button> </button>
@ -136,6 +147,15 @@ This example uses styling cribbed from the bootstrap progress bar:
return jobStatusTemplate(job); return jobStatusTemplate(job);
}); });
onGet("/job/progress", function(request, params, responseHeaders){
var job = jobManager.currentProcess();
if (job.complete) {
responseHeaders["HX-Trigger"] = "done";
}
return jobProgressTemplate(job);
});
// templates // templates
function startButton(message) { function startButton(message) {
return `<div hx-target="this" hx-swap="outerHTML"> return `<div hx-target="this" hx-swap="outerHTML">
@ -146,22 +166,31 @@ This example uses styling cribbed from the bootstrap progress bar:
</div>`; </div>`;
} }
function jobStatusTemplate(job) { function jobProgressTemplate(job) {
return `<div hx-target="this" return `<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${job.percentComplete}" aria-labelledby="pblabel">
hx-get="/job"
hx-trigger="${job.complete ? 'none' : 'load delay:600ms'}"
hx-swap="outerHTML">
<h3>${job.complete ? "Complete" : "Running"}</h3>
<div class="progress">
<div id="pb" class="progress-bar" style="width:${job.percentComplete}%"> <div id="pb" class="progress-bar" style="width:${job.percentComplete}%">
</div> </div>
</div>`
}
function jobStatusTemplate(job) {
return `<div hx-trigger="done" hx-get="/job" hx-swap="outerHTML" hx-target="this">
<h3 role="status" id="pblabel" tabindex="-1" autofocus>${job.complete ? "Complete" : "Running"}</h3>
<div
hx-get="/job/progress"
hx-trigger="${job.complete ? 'none' : 'every 600ms'}"
hx-target="this"
hx-swap="innerHTML">
${jobProgressTemplate(job)}
</div> </div>
${restartButton(job)}`; ${restartButton(job)}`;
} }
function restartButton(job) { function restartButton(job) {
if(job.complete){ if(job.complete){
return `<button id="restart-btn" class="btn" hx-post="/start" classes="add show:600ms"> return `
<button id="restart-btn" class="btn" hx-post="/start" classes="add show:600ms">
Restart Job Restart Job
</button>` </button>`
} else { } else {

View File

@ -15,13 +15,13 @@ The main page simply includes the following HTML to load the initial tab into th
Subsequent tab pages display all tabs and highlight the selected one accordingly. Subsequent tab pages display all tabs and highlight the selected one accordingly.
```html ```html
<div class="tab-list"> <div class="tab-list" role="tablist">
<a hx-get="/tab1" class="selected">Tab 1</a> <button hx-get="/tab1" class="selected" role="tab" aria-selected="false" aria-controls="tab-content">Tab 1</button>
<a hx-get="/tab2">Tab 2</a> <button hx-get="/tab2" role="tab" aria-selected="false" aria-controls="tab-content">Tab 2</button>
<a hx-get="/tab3">Tab 3</a> <button hx-get="/tab3" role="tab" aria-selected="false" aria-controls="tab-content">Tab 3</button>
</div> </div>
<div class="tab-content"> <div id="tab-content" role="tabpanel" class="tab-content">
Commodo normcore truffaut VHS duis gluten-free keffiyeh iPhone taxidermy godard ramps anim pour-over. Commodo normcore truffaut VHS duis gluten-free keffiyeh iPhone taxidermy godard ramps anim pour-over.
Pitchfork vegan mollit umami quinoa aute aliquip kinfolk eiusmod live-edge cardigan ipsum locavore. Pitchfork vegan mollit umami quinoa aute aliquip kinfolk eiusmod live-edge cardigan ipsum locavore.
Polaroid duis occaecat narwhal small batch food truck. Polaroid duis occaecat narwhal small batch food truck.
@ -33,19 +33,33 @@ Subsequent tab pages display all tabs and highlight the selected one accordingly
{{ demoenv() }} {{ demoenv() }}
<div id="tabs" hx-get="/tab1" hx-trigger="load delay:100ms" hx-target="#tabs" hx-swap="innerHTML"></div> <div id="tabs" hx-target="this" hx-swap="innerHTML">
<div class="tab-list" role="tablist">
<button hx-get="/tab1" class="selected" role="tab" aria-selected="true" aria-controls="tab-content">Tab 1</button>
<button hx-get="/tab2" role="tab" aria-selected="false" aria-controls="tab-content">Tab 2</button>
<button hx-get="/tab3" role="tab" aria-selected="false" aria-controls="tab-content">Tab 3</button>
</div>
<div id="tab-content" role="tabpanel" class="tab-content">
Commodo normcore truffaut VHS duis gluten-free keffiyeh iPhone taxidermy godard ramps anim pour-over.
Pitchfork vegan mollit umami quinoa aute aliquip kinfolk eiusmod live-edge cardigan ipsum locavore.
Polaroid duis occaecat narwhal small batch food truck.
PBR&B venmo shaman small batch you probably haven't heard of them hot chicken readymade.
Enim tousled cliche woke, typewriter single-origin coffee hella culpa.
Art party readymade 90's, asymmetrical hell of fingerstache ipsum.
</div>
</div>
<script> <script>
onGet("/tab1", function() { onGet("/tab1", function() {
return ` return `
<div class="tab-list"> <div class="tab-list" role="tablist">
<a hx-get="/tab1" class="selected">Tab 1</a> <button hx-get="/tab1" class="selected" aria-selected="true" autofocus role="tab" aria-controls="tab-content">Tab 1</button>
<a hx-get="/tab2">Tab 2</a> <button hx-get="/tab2" role="tab" aria-selected="false" aria-controls="tab-content">Tab 2</button>
<a hx-get="/tab3">Tab 3</a> <button hx-get="/tab3" role="tab" aria-selected="false" aria-controls="tab-content">Tab 3</button>
</div> </div>
<div class="tab-content"> <div id="tab-content" role="tabpanel" class="tab-content">
Commodo normcore truffaut VHS duis gluten-free keffiyeh iPhone taxidermy godard ramps anim pour-over. Commodo normcore truffaut VHS duis gluten-free keffiyeh iPhone taxidermy godard ramps anim pour-over.
Pitchfork vegan mollit umami quinoa aute aliquip kinfolk eiusmod live-edge cardigan ipsum locavore. Pitchfork vegan mollit umami quinoa aute aliquip kinfolk eiusmod live-edge cardigan ipsum locavore.
Polaroid duis occaecat narwhal small batch food truck. Polaroid duis occaecat narwhal small batch food truck.
@ -57,13 +71,13 @@ Subsequent tab pages display all tabs and highlight the selected one accordingly
onGet("/tab2", function() { onGet("/tab2", function() {
return ` return `
<div class="tab-list"> <div class="tab-list" role="tablist">
<a hx-get="/tab1">Tab 1</a> <button hx-get="/tab1" role="tab" aria-selected="false" aria-controls="tab-content">Tab 1</button>
<a hx-get="/tab2" class="selected">Tab 2</a> <button hx-get="/tab2" class="selected" aria-selected="true" autofocus role="tab" aria-controls="tab-content">Tab 2</button>
<a hx-get="/tab3">Tab 3</a> <button hx-get="/tab3" role="tab" aria-selected="false" aria-controls="tab-content">Tab 3</button>
</div> </div>
<div class="tab-content"> <div id="tab-content" role="tabpanel" class="tab-content">
Kitsch fanny pack yr, farm-to-table cardigan cillum commodo reprehenderit plaid dolore cronut meditation. Kitsch fanny pack yr, farm-to-table cardigan cillum commodo reprehenderit plaid dolore cronut meditation.
Tattooed polaroid veniam, anim id cornhole hashtag sed forage. Tattooed polaroid veniam, anim id cornhole hashtag sed forage.
Microdosing pug kitsch enim, kombucha pour-over sed irony forage live-edge. Microdosing pug kitsch enim, kombucha pour-over sed irony forage live-edge.
@ -76,13 +90,13 @@ Subsequent tab pages display all tabs and highlight the selected one accordingly
onGet("/tab3", function() { onGet("/tab3", function() {
return ` return `
<div class="tab-list"> <div class="tab-list" role="tablist">
<a hx-get="/tab1">Tab 1</a> <button hx-get="/tab1" role="tab" aria-selected="false" aria-controls="tab-content">Tab 1</button>
<a hx-get="/tab2">Tab 2</a> <button hx-get="/tab2" role="tab" aria-selected="false" aria-controls="tab-content">Tab 2</button>
<a hx-get="/tab3" class="selected">Tab 3</a> <button hx-get="/tab3" class="selected" aria-selected="true" autofocus role="tab" aria-controls="tab-content">Tab 3</button>
</div> </div>
<div class="tab-content"> <div id="tab-content" role="tabpanel" class="tab-content">
Aute chia marfa echo park tote bag hammock mollit artisan listicle direct trade. Aute chia marfa echo park tote bag hammock mollit artisan listicle direct trade.
Raw denim flexitarian eu godard etsy. Raw denim flexitarian eu godard etsy.
Poke tbh la croix put a bird on it fixie polaroid aute cred air plant four loko gastropub swag non brunch. Poke tbh la croix put a bird on it fixie polaroid aute cred air plant four loko gastropub swag non brunch.
@ -101,13 +115,19 @@ Subsequent tab pages display all tabs and highlight the selected one accordingly
border-bottom: solid 3px #eee; border-bottom: solid 3px #eee;
} }
#tabs > .tab-list a { #tabs > .tab-list button {
border: none;
display: inline-block; display: inline-block;
padding: 5px 10px; padding: 5px 10px;
cursor:pointer; cursor:pointer;
background-color: transparent;
} }
#tabs > .tab-list a.selected { #tabs > .tab-list button:hover {
color: var(--midBlue);
}
#tabs > .tab-list button.selected {
background-color: #eee; background-color: #eee;
} }

View File

@ -12,24 +12,24 @@ You may also consider [a more idiomatic approach](@/examples/tabs-hateoas.md) th
The HTML below displays a list of tabs, with added HTMX to dynamically load each tab pane from the server. A simple [hyperscript](https://hyperscript.org) event handler uses the [`take` command](https://hyperscript.org/commands/take/) to switch the selected tab when the content is swapped into the DOM. Alternatively, this could be accomplished with a slightly longer Javascript event handler. The HTML below displays a list of tabs, with added HTMX to dynamically load each tab pane from the server. A simple [hyperscript](https://hyperscript.org) event handler uses the [`take` command](https://hyperscript.org/commands/take/) to switch the selected tab when the content is swapped into the DOM. Alternatively, this could be accomplished with a slightly longer Javascript event handler.
```html ```html
<div id="tabs" hx-target="#tab-contents" _="on htmx:afterOnLoad take .selected for event.target"> <div id="tabs" hx-target="#tab-contents" role="tablist" _="on htmx:afterOnLoad set @aria-selected of <[aria-selected=true]/> to false tell the target take .selected set @aria-selected to true">
<a hx-get="/tab1" class="selected">Tab 1</a> <button role="tab" aria-controls="tab-content" aria-selected="true" hx-get="/tab1" class="selected">Tab 1</button>
<a hx-get="/tab2">Tab 2</a> <button role="tab" aria-controls="tab-content" aria-selected="false" hx-get="/tab2">Tab 2</button>
<a hx-get="/tab3">Tab 3</a> <button role="tab" aria-controls="tab-content" aria-selected="false" hx-get="/tab3">Tab 3</button>
</div> </div>
<div id="tab-contents" hx-get="/tab1" hx-trigger="load"></div> <div id="tab-contents" role="tabpanel" hx-get="/tab1" hx-trigger="load"></div>
``` ```
{{ demoenv() }} {{ demoenv() }}
<div id="tabs" hx-target="#tab-contents" _="on click take .selected for event.target"> <div id="tabs" hx-target="#tab-contents" role="tablist" _="on htmx:afterOnLoad set @aria-selected of <[aria-selected=true]/> to false tell the target take .selected set @aria-selected to true">
<a hx-get="/tab1" class="selected">Tab 1</a> <button role="tab" aria-controls="tab-content" aria-selected="true" hx-get="/tab1" class="selected">Tab 1</button>
<a hx-get="/tab2">Tab 2</a> <button role="tab" aria-controls="tab-content" aria-selected="false" hx-get="/tab2">Tab 2</button>
<a hx-get="/tab3">Tab 3</a> <button role="tab" aria-controls="tab-content" aria-selected="false" hx-get="/tab3">Tab 3</button>
</div> </div>
<div id="tab-contents" hx-get="/tab1" hx-trigger="load"></div> <div id="tab-contents" role="tabpanel" hx-get="/tab1" hx-trigger="load"></div>
<script src="https://unpkg.com/hyperscript.org"></script> <script src="https://unpkg.com/hyperscript.org"></script>
<script> <script>
@ -42,7 +42,6 @@ The HTML below displays a list of tabs, with added HTMX to dynamically load each
Enim tousled cliche woke, typewriter single-origin coffee hella culpa. Enim tousled cliche woke, typewriter single-origin coffee hella culpa.
Art party readymade 90's, asymmetrical hell of fingerstache ipsum.</p> Art party readymade 90's, asymmetrical hell of fingerstache ipsum.</p>
`}); `});
onGet("/tab2", function() { onGet("/tab2", function() {
return ` return `
<p>Kitsch fanny pack yr, farm-to-table cardigan cillum commodo reprehenderit plaid dolore cronut meditation. <p>Kitsch fanny pack yr, farm-to-table cardigan cillum commodo reprehenderit plaid dolore cronut meditation.
@ -54,7 +53,6 @@ The HTML below displays a list of tabs, with added HTMX to dynamically load each
Prism street art cray salvia.</p> Prism street art cray salvia.</p>
` `
}); });
onGet("/tab3", function() { onGet("/tab3", function() {
return ` return `
<p>Aute chia marfa echo park tote bag hammock mollit artisan listicle direct trade. <p>Aute chia marfa echo park tote bag hammock mollit artisan listicle direct trade.
@ -76,13 +74,19 @@ The HTML below displays a list of tabs, with added HTMX to dynamically load each
border-bottom: solid 3px #eee; border-bottom: solid 3px #eee;
} }
#tabs > a { #tabs > button {
border: none;
display: inline-block; display: inline-block;
padding: 5px 10px; padding: 5px 10px;
cursor:pointer; cursor:pointer;
background-color: transparent;
} }
#tabs > a.selected { #tabs > button:hover {
color: var(--midBlue);
}
#tabs > button.selected {
background-color: #eee; background-color: #eee;
} }

View File

@ -59,6 +59,9 @@ function params(request) {
return parseParams(request.requestBody); return parseParams(request.requestBody);
} }
} }
function headers(request) {
return request.getAllResponseHeaders().split("\r\n").filter(h => h.toLowerCase().startsWith("hx-")).map(h => h.split(": ")).reduce((acc, v) => ({ ...acc, [v[0]]: v[1] }), {})
}
//==================================== //====================================
// Routing // Routing
@ -76,29 +79,33 @@ function init(path, response) {
function onGet(path, response) { function onGet(path, response) {
server.respondWith("GET", path, function (request) { server.respondWith("GET", path, function (request) {
let body = response(request, params(request)); let headers = {};
request.respond(200, {}, body); let body = response(request, params(request), headers);
request.respond(200, headers, body);
}); });
} }
function onPut(path, response) { function onPut(path, response) {
server.respondWith("PUT", path, function (request) { server.respondWith("PUT", path, function (request) {
let body = response(request, params(request)); let headers = {};
request.respond(200, {}, body); let body = response(request, params(request), headers);
request.respond(200, headers, body);
}); });
} }
function onPost(path, response) { function onPost(path, response) {
server.respondWith("POST", path, function (request) { server.respondWith("POST", path, function (request) {
let body = response(request, params(request)); let headers = {};
request.respond(200, {}, body); let body = response(request, params(request), headers);
request.respond(200, headers, body);
}); });
} }
function onDelete(path, response) { function onDelete(path, response) {
server.respondWith("DELETE", path, function (request) { server.respondWith("DELETE", path, function (request) {
let body = response(request, params(request)); let headers = {};
request.respond(200, {}, body); let body = response(request, params(request), headers);
request.respond(200, headers, body);
}); });
} }
@ -173,6 +180,9 @@ function demoResponseTemplate(details){
<div> <div>
parameters: ${JSON.stringify(params(details.xhr))} parameters: ${JSON.stringify(params(details.xhr))}
</div> </div>
<div>
headers: ${JSON.stringify(headers(details.xhr))}
</div>
<div> <div>
<b>Response</b> <b>Response</b>
<pre class="language-html"><code class="language-html">${escapeHtml(details.xhr.response)}</code> </pre> <pre class="language-html"><code class="language-html">${escapeHtml(details.xhr.response)}</code> </pre>