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
|
||||
|
||||
## [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
|
||||
|
||||
|
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",
|
||||
"unpkg": "dist/htmx.min.js",
|
||||
"web-types": "editors/jetbrains/htmx.web-types.json",
|
||||
"engines": {
|
||||
"node": "15.x"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha-chrome test/index.html",
|
||||
"test-types": "tsc --project ./jsconfig.json",
|
||||
|
@ -48,9 +48,12 @@ htmx.defineExtension("preload", {
|
||||
// in the future
|
||||
var hxGet = node.getAttribute("hx-get") || node.getAttribute("data-hx-get")
|
||||
if (hxGet) {
|
||||
htmx.ajax("GET", hxGet, {handler:function(elt, info) {
|
||||
htmx.ajax("GET", hxGet, {
|
||||
source: node,
|
||||
handler:function(elt, info) {
|
||||
done(info.xhr.responseText);
|
||||
}});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
return;
|
||||
|
||||
// Try to create websockets when elements are processed
|
||||
case "htmx:afterProcessNode":
|
||||
case "htmx:beforeProcessNode":
|
||||
var parent = evt.target;
|
||||
|
||||
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
|
||||
|
91
src/htmx.js
91
src/htmx.js
@ -44,6 +44,7 @@ return (function () {
|
||||
defineExtension : defineExtension,
|
||||
removeExtension : removeExtension,
|
||||
logAll : logAll,
|
||||
logNone : logNone,
|
||||
logger : null,
|
||||
config : {
|
||||
historyEnabled:true,
|
||||
@ -71,6 +72,7 @@ return (function () {
|
||||
defaultFocusScroll: false,
|
||||
getCacheBusterParam: false,
|
||||
globalViewTransitions: false,
|
||||
methodsThatUseUrlParams: ["get"],
|
||||
},
|
||||
parseInterval:parseInterval,
|
||||
_:internalEval,
|
||||
@ -478,6 +480,10 @@ return (function () {
|
||||
}
|
||||
}
|
||||
|
||||
function logNone() {
|
||||
htmx.logger = null
|
||||
}
|
||||
|
||||
function find(eltOrSelector, selector) {
|
||||
if (selector) {
|
||||
return eltOrSelector.querySelector(selector);
|
||||
@ -905,6 +911,17 @@ return (function () {
|
||||
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) {
|
||||
var internalData = getInternalData(element);
|
||||
if (internalData.timeout) {
|
||||
@ -923,12 +940,7 @@ return (function () {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (internalData.onHandlers) {
|
||||
for (let i = 0; i < internalData.onHandlers.length; i++) {
|
||||
const handlerInfo = internalData.onHandlers[i];
|
||||
element.removeEventListener(handlerInfo.name, handlerInfo.handler);
|
||||
}
|
||||
}
|
||||
deInitOnHandlers(element);
|
||||
}
|
||||
|
||||
function cleanUpElement(element) {
|
||||
@ -953,7 +965,7 @@ return (function () {
|
||||
newElt = eltBeforeNewContent.nextSibling;
|
||||
}
|
||||
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) {
|
||||
if (newElt.nodeType === Node.ELEMENT_NODE) {
|
||||
settleInfo.elts.push(newElt);
|
||||
@ -1656,6 +1668,9 @@ return (function () {
|
||||
var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
|
||||
var sseListener = function (event) {
|
||||
if (maybeCloseSSESource(sseSourceElt)) {
|
||||
return;
|
||||
}
|
||||
if (!bodyContains(elt)) {
|
||||
sseEventSource.removeEventListener(sseEventName, sseListener);
|
||||
return;
|
||||
}
|
||||
@ -1672,7 +1687,7 @@ return (function () {
|
||||
var target = getTarget(elt)
|
||||
var settleInfo = makeSettleInfo(elt);
|
||||
|
||||
selectAndSwap(swapSpec.swapStyle, elt, target, response, settleInfo)
|
||||
selectAndSwap(swapSpec.swapStyle, target, elt, response, settleInfo)
|
||||
settleImmediately(settleInfo.tasks)
|
||||
triggerEvent(elt, "htmx:sseMessage", event)
|
||||
};
|
||||
@ -1826,6 +1841,16 @@ return (function () {
|
||||
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) {
|
||||
if (elt.querySelectorAll) {
|
||||
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) {
|
||||
if (elt.closest && elt.closest(htmx.config.disableSelector)) {
|
||||
return;
|
||||
@ -1965,6 +2006,9 @@ return (function () {
|
||||
elt = resolveTarget(elt);
|
||||
initNode(elt);
|
||||
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 eltIsBoosted = getInternalData(elt).boosted;
|
||||
|
||||
var useUrlParams = htmx.config.methodsThatUseUrlParams.indexOf(verb) >= 0
|
||||
|
||||
var requestConfig = {
|
||||
boosted: eltIsBoosted,
|
||||
useUrlParams: useUrlParams,
|
||||
parameters: filteredParameters,
|
||||
unfilteredParameters: allParameters,
|
||||
headers:headers,
|
||||
@ -2959,6 +3007,7 @@ return (function () {
|
||||
headers = requestConfig.headers;
|
||||
filteredParameters = requestConfig.parameters;
|
||||
errors = requestConfig.errors;
|
||||
useUrlParams = requestConfig.useUrlParams;
|
||||
|
||||
if(errors && errors.length > 0){
|
||||
triggerEvent(elt, 'htmx:validation:halted', requestConfig)
|
||||
@ -2970,26 +3019,25 @@ return (function () {
|
||||
var splitPath = path.split("#");
|
||||
var pathNoAnchor = splitPath[0];
|
||||
var anchor = splitPath[1];
|
||||
var finalPathForGet = null;
|
||||
if (verb === 'get') {
|
||||
finalPathForGet = pathNoAnchor;
|
||||
|
||||
var finalPath = path
|
||||
if (useUrlParams) {
|
||||
finalPath = pathNoAnchor;
|
||||
var values = Object.keys(filteredParameters).length !== 0;
|
||||
if (values) {
|
||||
if (finalPathForGet.indexOf("?") < 0) {
|
||||
finalPathForGet += "?";
|
||||
if (finalPath.indexOf("?") < 0) {
|
||||
finalPath += "?";
|
||||
} else {
|
||||
finalPathForGet += "&";
|
||||
finalPath += "&";
|
||||
}
|
||||
finalPathForGet += urlEncode(filteredParameters);
|
||||
finalPath += urlEncode(filteredParameters);
|
||||
if (anchor) {
|
||||
finalPathForGet += "#" + anchor;
|
||||
finalPath += "#" + anchor;
|
||||
}
|
||||
}
|
||||
xhr.open('GET', finalPathForGet, true);
|
||||
} else {
|
||||
xhr.open(verb.toUpperCase(), path, true);
|
||||
}
|
||||
|
||||
xhr.open(verb.toUpperCase(), finalPath, true);
|
||||
xhr.overrideMimeType("text/html");
|
||||
xhr.withCredentials = requestConfig.withCredentials;
|
||||
xhr.timeout = requestConfig.timeout;
|
||||
@ -3010,7 +3058,7 @@ return (function () {
|
||||
xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted,
|
||||
pathInfo: {
|
||||
requestPath: path,
|
||||
finalRequestPath: finalPathForGet || path,
|
||||
finalRequestPath: finalPath,
|
||||
anchor: anchor
|
||||
}
|
||||
};
|
||||
@ -3085,7 +3133,8 @@ return (function () {
|
||||
});
|
||||
});
|
||||
triggerEvent(elt, 'htmx:beforeSend', responseInfo);
|
||||
xhr.send(verb === 'get' ? null : encodeParamsForBody(xhr, elt, filteredParameters));
|
||||
var params = useUrlParams ? null : encodeParamsForBody(xhr, elt, filteredParameters)
|
||||
xhr.send(params);
|
||||
return promise;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
describe("hx-boost attribute", function() {
|
||||
|
||||
htmx.logAll();
|
||||
beforeEach(function () {
|
||||
this.server = makeServer();
|
||||
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);
|
||||
});
|
||||
|
||||
it('logAll works', function () {
|
||||
var initialLogger = htmx.config.logger
|
||||
try {
|
||||
it('logAll and logNone works', function () {
|
||||
var initialLogger = htmx.logger
|
||||
htmx.logAll();
|
||||
} finally {
|
||||
htmx.config.logger = initialLogger;
|
||||
}
|
||||
htmx.logger.should.not.equal(null);
|
||||
htmx.logNone();
|
||||
should.equal(htmx.logger, null);
|
||||
htmx.logger = initialLogger;
|
||||
});
|
||||
|
||||
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 () {
|
||||
var called = false;
|
||||
var handler = htmx.on("htmx:afterRequest", function (evt) {
|
||||
|
@ -177,5 +177,59 @@ describe("Core htmx Parameter Handling", function() {
|
||||
vals['do'].should.equal('rey');
|
||||
})
|
||||
|
||||
it('it puts GET params in the URL by default', function () {
|
||||
this.server.respondWith("GET", "/test?i1=value", function (xhr) {
|
||||
xhr.respond(200, {}, "Clicked!");
|
||||
});
|
||||
var form = make('<form hx-trigger="click" hx-get="/test"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
|
||||
form.click();
|
||||
this.server.respond();
|
||||
form.innerHTML.should.equal("Clicked!");
|
||||
});
|
||||
|
||||
it('it puts GET params in the body if methodsThatUseUrlParams is empty', function () {
|
||||
this.server.respondWith("GET", "/test", function (xhr) {
|
||||
xhr.requestBody.should.equal("i1=value");
|
||||
xhr.respond(200, {}, "Clicked!");
|
||||
});
|
||||
var form = make('<form hx-trigger="click" hx-get="/test"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
|
||||
|
||||
try {
|
||||
htmx.config.methodsThatUseUrlParams = [];
|
||||
form.click();
|
||||
this.server.respond();
|
||||
form.innerHTML.should.equal("Clicked!");
|
||||
} finally {
|
||||
htmx.config.methodsThatUseUrlParams = ["get"];
|
||||
}
|
||||
});
|
||||
|
||||
it('it puts DELETE params in the body by default', function () {
|
||||
this.server.respondWith("DELETE", "/test", function (xhr) {
|
||||
xhr.requestBody.should.equal("i1=value");
|
||||
xhr.respond(200, {}, "Clicked!");
|
||||
});
|
||||
var form = make('<form hx-trigger="click" hx-delete="/test"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
|
||||
form.click();
|
||||
this.server.respond();
|
||||
form.innerHTML.should.equal("Clicked!");
|
||||
});
|
||||
|
||||
it('it puts DELETE params in the URL if methodsThatUseUrlParams contains "delete"', function () {
|
||||
this.server.respondWith("DELETE", "/test?i1=value", function (xhr) {
|
||||
xhr.respond(200, {}, "Clicked!");
|
||||
});
|
||||
var form = make('<form hx-trigger="click" hx-delete="/test"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
|
||||
|
||||
try {
|
||||
htmx.config.methodsThatUseUrlParams.push("delete")
|
||||
form.click();
|
||||
this.server.respond();
|
||||
form.innerHTML.should.equal("Clicked!");
|
||||
} finally {
|
||||
htmx.config.methodsThatUseUrlParams = ["get"];
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
@ -130,7 +130,6 @@ describe("Core htmx Regression Tests", function(){
|
||||
|
||||
it('a form can reset based on the htmx:afterRequest event', function() {
|
||||
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">' +
|
||||
' <input type="text" name="input" id="i1"/>' +
|
||||
@ -174,7 +173,6 @@ describe("Core htmx Regression Tests", function(){
|
||||
|
||||
it("supports unset on hx-select", function(){
|
||||
this.server.respondWith("GET", "/test", "Foo<span id='example'>Bar</span>");
|
||||
htmx.logAll();
|
||||
make('<form hx-select="#example">\n' +
|
||||
' <button id="b1" hx-select="unset" hx-get="/test">Initial</button>\n' +
|
||||
'</form>')
|
||||
|
@ -1,4 +1,3 @@
|
||||
//
|
||||
describe("json-enc extension", function() {
|
||||
beforeEach(function () {
|
||||
this.server = makeServer();
|
||||
@ -9,6 +8,15 @@ describe("json-enc extension", function() {
|
||||
clearWorkArea();
|
||||
});
|
||||
|
||||
it('handles basic get properly', function () {
|
||||
var jsonResponseBody = JSON.stringify({});
|
||||
this.server.respondWith("GET", "/test", jsonResponseBody);
|
||||
var div = make('<div hx-get="/test" hx-ext="json-enc">click me</div>');
|
||||
div.click();
|
||||
this.server.respond();
|
||||
this.server.lastRequest.response.should.equal("{}");
|
||||
})
|
||||
|
||||
it('handles basic post properly', function () {
|
||||
var jsonResponseBody = JSON.stringify({});
|
||||
this.server.respondWith("POST", "/test", jsonResponseBody);
|
||||
@ -67,7 +75,6 @@ describe("json-enc extension", function() {
|
||||
})
|
||||
|
||||
it('handles put with form parameters', function () {
|
||||
|
||||
this.server.respondWith("PUT", "/test", function (xhr) {
|
||||
var values = JSON.parse(xhr.requestBody);
|
||||
values.should.have.keys("username","password");
|
||||
|
@ -77,6 +77,17 @@ describe("web-sockets extension", function () {
|
||||
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 () {
|
||||
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();
|
||||
|
@ -67,6 +67,7 @@
|
||||
<script src="attributes/hx-indicator.js"></script>
|
||||
<script src="attributes/hx-disinherit.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-patch.js"></script>
|
||||
<script src="attributes/hx-post.js"></script>
|
||||
|
@ -38,7 +38,8 @@ function getWorkArea() {
|
||||
}
|
||||
|
||||
function clearWorkArea() {
|
||||
getWorkArea().innerHTML = "";
|
||||
const workArea = getWorkArea();
|
||||
if (workArea) workArea.innerHTML = "";
|
||||
}
|
||||
|
||||
function removeWhiteSpace(str) {
|
||||
|
@ -240,6 +240,16 @@ Log all htmx events, useful for debugging.
|
||||
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}
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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:
|
||||
|
||||
```html
|
||||
<div hx-on="click: alert('Clicked!')">Click</div>
|
||||
```
|
||||
|
||||
All htmx events can be captured, too!
|
||||
And htmx events:
|
||||
|
||||
```html
|
||||
<button hx-get="/info" hx-on="htmx:beforeRequest: alert('Making a request!')">
|
||||
@ -20,6 +61,15 @@ All htmx events can be captured, too!
|
||||
</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
|
||||
|
||||
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
|
||||
* `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
|
||||
|
||||
* `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),
|
||||
`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.
|
||||
* `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).
|
||||
* `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.
|
||||
|
@ -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:
|
||||
* `document` - listen for events on the document
|
||||
* `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
|
||||
* `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,
|
||||
|
@ -392,7 +392,8 @@ input tag.
|
||||
`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
|
||||
* 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)
|
||||
* 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.
|
||||
@ -1252,7 +1253,7 @@ with an `on*` property, but can be done using the `hx-on` attribute:
|
||||
|
||||
```html
|
||||
<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!
|
||||
</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.getCacheBusterParam` | defaults to false, if set to true htmx will include a cache-busting parameter in `GET` requests to avoid caching partial responses by the browser |
|
||||
| `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. |
|
||||
| `htmx.config.methodsThatUseUrlParams` | defaults to `["get"]`, htmx will format requests with this method by encoding their parameters in the URL, not the request body |
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
</style>
|
||||
<form hx-post="/name">
|
||||
<form hx-post="/name" hx-swap="outerHTML">
|
||||
<label>Name:</label><input name="name"><br/>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
@ -193,10 +193,12 @@ is a form that on submit will change its look to indicate that a request is bein
|
||||
}
|
||||
</style>
|
||||
|
||||
<form hx-post="/name">
|
||||
<div aria-live="polite">
|
||||
<form hx-post="/name" hx-swap="outerHTML">
|
||||
<label>Name:</label><input name="name"><br/>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
onPost("/name", function(){ return "Submitted!"; });
|
||||
|
@ -9,8 +9,8 @@ values in `PUT`'s to two different endpoints: `activate` and `deactivate`:
|
||||
|
||||
```html
|
||||
<div hx-include="#checked-contacts" hx-target="#tbody">
|
||||
<a class="btn" hx-put="/activate">Activate</a>
|
||||
<a class="btn" hx-put="/deactivate">Deactivate</a>
|
||||
<button class="btn" hx-put="/activate">Activate</button>
|
||||
<button class="btn" hx-put="/deactivate">Deactivate</button>
|
||||
</div>
|
||||
|
||||
<form id="checked-contacts">
|
||||
@ -126,7 +126,7 @@ You can see a working example of this code below.
|
||||
|
||||
// templates
|
||||
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">
|
||||
<table>
|
||||
<thead>
|
||||
@ -145,8 +145,8 @@ You can see a working example of this code below.
|
||||
<br/>
|
||||
<br/>
|
||||
<div hx-include="#checked-contacts" hx-target="#tbody">
|
||||
<a class="btn" hx-put="/activate">Activate</a>
|
||||
<a class="btn" hx-put="/deactivate">Deactivate</a>
|
||||
<button class="btn" hx-put="/activate">Activate</button>
|
||||
<button class="btn" hx-put="/deactivate">Deactivate</button>
|
||||
</div>`
|
||||
}
|
||||
|
||||
|
@ -75,18 +75,18 @@ The click to edit pattern provides a way to offer inline editing of all or part
|
||||
function formTemplate(contact) {
|
||||
return `<form hx-put="/contact/1" hx-target="this" hx-swap="outerHTML">
|
||||
<div>
|
||||
<label>First Name</label>
|
||||
<input type="text" name="firstName" value="${contact.firstName}">
|
||||
<label for="firstName">First Name</label>
|
||||
<input autofocus type="text" id="firstName" name="firstName" value="${contact.firstName}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Last Name</label>
|
||||
<input type="text" name="lastName" value="${contact.lastName}">
|
||||
<label for="lastName">Last Name</label>
|
||||
<input type="text" id="lastName" name="lastName" value="${contact.lastName}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email Address</label>
|
||||
<input type="email" name="email" value="${contact.email}">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email" id="email" name="email" value="${contact.email}">
|
||||
</div>
|
||||
<button class="btn">Submit</button>
|
||||
<button class="btn" type="submit">Submit</button>
|
||||
<button class="btn" hx-get="/contact/1">Cancel</button>
|
||||
</form>`
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ results (which will contain a button to load the *next* page of results). And s
|
||||
var txt = "";
|
||||
for (var i = 0; i < contacts.length; 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);
|
||||
return txt;
|
||||
|
@ -113,28 +113,28 @@ Below is a working demo of this example. The only email that will be accepted i
|
||||
function formTemplate() {
|
||||
return `<form hx-post="/contact">
|
||||
<div hx-target="this" hx-swap="outerHTML">
|
||||
<label>Email Address</label>
|
||||
<input name="email" hx-post="/contact/email" hx-indicator="#ind">
|
||||
<label for="email">Email Address</label>
|
||||
<input name="email" id="email" hx-post="/contact/email" hx-indicator="#ind">
|
||||
<img id="ind" src="/img/bars.svg" class="htmx-indicator"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>First Name</label>
|
||||
<input type="text" class="form-control" name="firstName">
|
||||
<label for="firstName">First Name</label>
|
||||
<input type="text" class="form-control" name="firstName" id="firstName">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Last Name</label>
|
||||
<input type="text" class="form-control" name="lastName">
|
||||
<label for="lastName">Last Name</label>
|
||||
<input type="text" class="form-control" name="lastName" id="lastName">
|
||||
</div>
|
||||
<button class="btn btn-default" disabled>Submit</button>
|
||||
<button type='submit' class="btn btn-default" disabled>Submit</button>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
function emailInputTemplate(val, errorMsg) {
|
||||
return `<div hx-target="this" hx-swap="outerHTML" class="${errorMsg ? "error" : "valid"}">
|
||||
<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"/>
|
||||
${errorMsg ? ("<div class='error-message'>" + errorMsg + "</div>") : ""}
|
||||
${errorMsg ? (`<div class='error-message' >${errorMsg}</div>`) : ""}
|
||||
</div>`;
|
||||
}
|
||||
</script>
|
||||
|
@ -16,40 +16,51 @@ We start with an initial state with a button that issues a `POST` to `/start` to
|
||||
</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
|
||||
<div hx-target="this"
|
||||
hx-get="/job"
|
||||
hx-trigger="load delay:600ms"
|
||||
hx-swap="outerHTML">
|
||||
<h3>Running</h3>
|
||||
<div class="progress">
|
||||
<div id="pb" class="progress-bar" style="width:0%"></div>
|
||||
<div hx-trigger="done" hx-get="/job" hx-swap="outerHTML" hx-target="this">
|
||||
<h3 role="status" id="pblabel" tabindex="-1" autofocus>Running</h3>
|
||||
|
||||
<div
|
||||
hx-get="/job/progress"
|
||||
hx-trigger="every 600ms"
|
||||
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>
|
||||
|
||||
```
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
Finally, when the process is complete, a restart button is added to the UI (we are using the [`class-tools`](@/extensions/class-tools.md)
|
||||
extension in this example):
|
||||
Finally, when the process is complete, a server returns `HX-Trigger: done` header, which triggers an update of the UI to "Complete" state
|
||||
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
|
||||
<div hx-target="this"
|
||||
hx-get="/job"
|
||||
<div hx-trigger="done" hx-get="/job" hx-swap="outerHTML" hx-target="this">
|
||||
<h3 role="status" id="pblabel" tabindex="-1" autofocus>Complete</h3>
|
||||
|
||||
<div
|
||||
hx-get="/job/progress"
|
||||
hx-trigger="none"
|
||||
hx-swap="outerHTML">
|
||||
<h3>Complete</h3>
|
||||
<div class="progress">
|
||||
<div id="pb" class="progress-bar" style="width:100%"></div>
|
||||
hx-target="this"
|
||||
hx-swap="innerHTML">
|
||||
<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:122%">
|
||||
</div>
|
||||
<button id="restart-btn" class="btn" hx-post="/start" classes="add show:600ms">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="restart-btn" class="btn" hx-post="/start" classes="add show:600ms">
|
||||
Restart Job
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
@ -136,6 +147,15 @@ This example uses styling cribbed from the bootstrap progress bar:
|
||||
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
|
||||
function startButton(message) {
|
||||
return `<div hx-target="this" hx-swap="outerHTML">
|
||||
@ -146,22 +166,31 @@ This example uses styling cribbed from the bootstrap progress bar:
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function jobStatusTemplate(job) {
|
||||
return `<div hx-target="this"
|
||||
hx-get="/job"
|
||||
hx-trigger="${job.complete ? 'none' : 'load delay:600ms'}"
|
||||
hx-swap="outerHTML">
|
||||
<h3>${job.complete ? "Complete" : "Running"}</h3>
|
||||
<div class="progress">
|
||||
function jobProgressTemplate(job) {
|
||||
return `<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${job.percentComplete}" aria-labelledby="pblabel">
|
||||
<div id="pb" class="progress-bar" style="width:${job.percentComplete}%">
|
||||
</div>
|
||||
</div>
|
||||
${restartButton(job)}`;
|
||||
</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>
|
||||
${restartButton(job)}`;
|
||||
}
|
||||
|
||||
function restartButton(job) {
|
||||
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
|
||||
</button>`
|
||||
} 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.
|
||||
|
||||
```html
|
||||
<div class="tab-list">
|
||||
<a hx-get="/tab1" class="selected">Tab 1</a>
|
||||
<a hx-get="/tab2">Tab 2</a>
|
||||
<a hx-get="/tab3">Tab 3</a>
|
||||
<div class="tab-list" role="tablist">
|
||||
<button hx-get="/tab1" class="selected" role="tab" aria-selected="false" 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 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.
|
||||
Pitchfork vegan mollit umami quinoa aute aliquip kinfolk eiusmod live-edge cardigan ipsum locavore.
|
||||
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() }}
|
||||
|
||||
<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>
|
||||
onGet("/tab1", function() {
|
||||
return `
|
||||
<div class="tab-list">
|
||||
<a hx-get="/tab1" class="selected">Tab 1</a>
|
||||
<a hx-get="/tab2">Tab 2</a>
|
||||
<a hx-get="/tab3">Tab 3</a>
|
||||
<div class="tab-list" role="tablist">
|
||||
<button hx-get="/tab1" class="selected" aria-selected="true" autofocus role="tab" 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 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.
|
||||
Pitchfork vegan mollit umami quinoa aute aliquip kinfolk eiusmod live-edge cardigan ipsum locavore.
|
||||
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() {
|
||||
return `
|
||||
<div class="tab-list">
|
||||
<a hx-get="/tab1">Tab 1</a>
|
||||
<a hx-get="/tab2" class="selected">Tab 2</a>
|
||||
<a hx-get="/tab3">Tab 3</a>
|
||||
<div class="tab-list" role="tablist">
|
||||
<button hx-get="/tab1" role="tab" aria-selected="false" aria-controls="tab-content">Tab 1</button>
|
||||
<button hx-get="/tab2" class="selected" aria-selected="true" autofocus role="tab" 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 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.
|
||||
Tattooed polaroid veniam, anim id cornhole hashtag sed forage.
|
||||
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() {
|
||||
return `
|
||||
<div class="tab-list">
|
||||
<a hx-get="/tab1">Tab 1</a>
|
||||
<a hx-get="/tab2">Tab 2</a>
|
||||
<a hx-get="/tab3" class="selected">Tab 3</a>
|
||||
<div class="tab-list" role="tablist">
|
||||
<button hx-get="/tab1" role="tab" aria-selected="false" 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" class="selected" aria-selected="true" autofocus role="tab" aria-controls="tab-content">Tab 3</button>
|
||||
</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.
|
||||
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.
|
||||
@ -101,13 +115,19 @@ Subsequent tab pages display all tabs and highlight the selected one accordingly
|
||||
border-bottom: solid 3px #eee;
|
||||
}
|
||||
|
||||
#tabs > .tab-list a {
|
||||
#tabs > .tab-list button {
|
||||
border: none;
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
||||
```html
|
||||
<div id="tabs" hx-target="#tab-contents" _="on htmx:afterOnLoad take .selected for event.target">
|
||||
<a hx-get="/tab1" class="selected">Tab 1</a>
|
||||
<a hx-get="/tab2">Tab 2</a>
|
||||
<a hx-get="/tab3">Tab 3</a>
|
||||
<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">
|
||||
<button role="tab" aria-controls="tab-content" aria-selected="true" hx-get="/tab1" class="selected">Tab 1</button>
|
||||
<button role="tab" aria-controls="tab-content" aria-selected="false" hx-get="/tab2">Tab 2</button>
|
||||
<button role="tab" aria-controls="tab-content" aria-selected="false" hx-get="/tab3">Tab 3</button>
|
||||
</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() }}
|
||||
|
||||
<div id="tabs" hx-target="#tab-contents" _="on click take .selected for event.target">
|
||||
<a hx-get="/tab1" class="selected">Tab 1</a>
|
||||
<a hx-get="/tab2">Tab 2</a>
|
||||
<a hx-get="/tab3">Tab 3</a>
|
||||
<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">
|
||||
<button role="tab" aria-controls="tab-content" aria-selected="true" hx-get="/tab1" class="selected">Tab 1</button>
|
||||
<button role="tab" aria-controls="tab-content" aria-selected="false" hx-get="/tab2">Tab 2</button>
|
||||
<button role="tab" aria-controls="tab-content" aria-selected="false" hx-get="/tab3">Tab 3</button>
|
||||
</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>
|
||||
@ -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.
|
||||
Art party readymade 90's, asymmetrical hell of fingerstache ipsum.</p>
|
||||
`});
|
||||
|
||||
onGet("/tab2", function() {
|
||||
return `
|
||||
<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>
|
||||
`
|
||||
});
|
||||
|
||||
onGet("/tab3", function() {
|
||||
return `
|
||||
<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;
|
||||
}
|
||||
|
||||
#tabs > a {
|
||||
#tabs > button {
|
||||
border: none;
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
cursor:pointer;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#tabs > a.selected {
|
||||
#tabs > button:hover {
|
||||
color: var(--midBlue);
|
||||
}
|
||||
|
||||
#tabs > button.selected {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
|
@ -3,13 +3,13 @@
|
||||
//====================================
|
||||
var server = sinon.fakeServer.create();
|
||||
server.fakeHTTPMethods = true;
|
||||
server.getHTTPMethod = function(xhr) {
|
||||
server.getHTTPMethod = function (xhr) {
|
||||
return xhr.requestHeaders['X-HTTP-Method-Override'] || xhr.method;
|
||||
}
|
||||
server.autoRespond = true;
|
||||
server.autoRespondAfter = 80;
|
||||
server.xhr.useFilters = true;
|
||||
server.xhr.addFilter(function (method, url, async, username, password){
|
||||
server.xhr.addFilter(function (method, url, async, username, password) {
|
||||
return url === "/" || url.indexOf("http") === 0;
|
||||
})
|
||||
|
||||
@ -46,10 +46,10 @@ function parseParams(str) {
|
||||
function getQuery(url) {
|
||||
var question = url.indexOf("?");
|
||||
var hash = url.indexOf("#");
|
||||
if(hash==-1 && question==-1) return "";
|
||||
if(hash==-1) hash = url.length;
|
||||
return question==-1 || hash==question+1 ? url.substring(hash) :
|
||||
url.substring(question+1,hash);
|
||||
if (hash == -1 && question == -1) return "";
|
||||
if (hash == -1) hash = url.length;
|
||||
return question == -1 || hash == question + 1 ? url.substring(hash) :
|
||||
url.substring(question + 1, hash);
|
||||
}
|
||||
|
||||
function params(request) {
|
||||
@ -59,6 +59,9 @@ function params(request) {
|
||||
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
|
||||
@ -75,30 +78,34 @@ function init(path, response) {
|
||||
}
|
||||
|
||||
function onGet(path, response) {
|
||||
server.respondWith("GET", path, function(request){
|
||||
let body = response(request, params(request));
|
||||
request.respond(200, {}, body);
|
||||
server.respondWith("GET", path, function (request) {
|
||||
let headers = {};
|
||||
let body = response(request, params(request), headers);
|
||||
request.respond(200, headers, body);
|
||||
});
|
||||
}
|
||||
|
||||
function onPut(path, response) {
|
||||
server.respondWith("PUT", path, function(request){
|
||||
let body = response(request, params(request));
|
||||
request.respond(200, {}, body);
|
||||
server.respondWith("PUT", path, function (request) {
|
||||
let headers = {};
|
||||
let body = response(request, params(request), headers);
|
||||
request.respond(200, headers, body);
|
||||
});
|
||||
}
|
||||
|
||||
function onPost(path, response) {
|
||||
server.respondWith("POST", path, function(request){
|
||||
let body = response(request, params(request));
|
||||
request.respond(200, {}, body);
|
||||
server.respondWith("POST", path, function (request) {
|
||||
let headers = {};
|
||||
let body = response(request, params(request), headers);
|
||||
request.respond(200, headers, body);
|
||||
});
|
||||
}
|
||||
|
||||
function onDelete(path, response) {
|
||||
server.respondWith("DELETE", path, function(request){
|
||||
let body = response(request, params(request));
|
||||
request.respond(200, {}, body);
|
||||
server.respondWith("DELETE", path, function (request) {
|
||||
let headers = {};
|
||||
let body = response(request, params(request), headers);
|
||||
request.respond(200, headers, body);
|
||||
});
|
||||
}
|
||||
|
||||
@ -107,7 +114,7 @@ function onDelete(path, response) {
|
||||
//====================================
|
||||
|
||||
var requestId = 0;
|
||||
htmx.on("htmx:beforeSwap", function(event) {
|
||||
htmx.on("htmx:beforeSwap", function (event) {
|
||||
if (document.getElementById("request-count")) {
|
||||
requestId++;
|
||||
pushActivityChip(`${server.getHTTPMethod(event.detail.xhr)} ${event.detail.xhr.url}`, `req-${requestId}`, demoResponseTemplate(event.detail));
|
||||
@ -128,7 +135,7 @@ function showTimelineEntry(id) {
|
||||
var children = document.getElementById("demo-timeline").children;
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var child = children[i];
|
||||
if (child.id == id + "-link" ) {
|
||||
if (child.id == id + "-link") {
|
||||
child.classList.add('active');
|
||||
} else {
|
||||
child.classList.remove('active');
|
||||
@ -153,19 +160,19 @@ function pushActivityChip(name, id, content) {
|
||||
|
||||
function escapeHtml(string) {
|
||||
var pre = document.createElement('pre');
|
||||
var text = document.createTextNode( string );
|
||||
var text = document.createTextNode(string);
|
||||
pre.appendChild(text);
|
||||
return pre.innerHTML;
|
||||
}
|
||||
|
||||
function demoInitialStateTemplate(html){
|
||||
function demoInitialStateTemplate(html) {
|
||||
return `<span class="activity initial">
|
||||
<b>HTML</b>
|
||||
<pre class="language-html"><code class="language-html">${escapeHtml(html)}</code></pre>
|
||||
</span>`
|
||||
}
|
||||
|
||||
function demoResponseTemplate(details){
|
||||
function demoResponseTemplate(details) {
|
||||
return `<span class="activity response">
|
||||
<div>
|
||||
<b>${server.getHTTPMethod(details.xhr)}</b> ${details.xhr.url}
|
||||
@ -173,6 +180,9 @@ function demoResponseTemplate(details){
|
||||
<div>
|
||||
parameters: ${JSON.stringify(params(details.xhr))}
|
||||
</div>
|
||||
<div>
|
||||
headers: ${JSON.stringify(headers(details.xhr))}
|
||||
</div>
|
||||
<div>
|
||||
<b>Response</b>
|
||||
<pre class="language-html"><code class="language-html">${escapeHtml(details.xhr.response)}</code> </pre>
|
||||
|
Loading…
x
Reference in New Issue
Block a user