mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-10-02 15:25:26 +00:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
05a1f8cba7
@ -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
19
.github/workflows/ci.yml
vendored
Normal 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
|
@ -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
2306
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
91
src/htmx.js
91
src/htmx.js
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
133
test/attributes/hx-on-wildcard.js
Normal file
133
test/attributes/hx-on-wildcard.js
Normal 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -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 () {
|
||||||
|
@ -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) {
|
||||||
|
@ -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"];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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>')
|
||||||
|
@ -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");
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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!"; });
|
||||||
|
@ -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>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>`
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user