mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-09-27 13:01:03 +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) {
|
||||
done(info.xhr.responseText);
|
||||
}});
|
||||
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 {
|
||||
htmx.logAll();
|
||||
} finally {
|
||||
htmx.config.logger = initialLogger;
|
||||
}
|
||||
it('logAll and logNone works', function () {
|
||||
var initialLogger = htmx.logger
|
||||
htmx.logAll();
|
||||
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) {
|
||||
@ -105,4 +106,4 @@ function getParameters(xhr) {
|
||||
function log(val) {
|
||||
console.log(val);
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
@ -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` 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,
|
||||
|
@ -110,7 +110,7 @@ If you are migrating to htmx from intercooler.js, please see the [migration guid
|
||||
|
||||
### Via A CDN (e.g. unpkg.com)
|
||||
|
||||
The fastest way to get going with htmx is to load it via a CDN. You can simply add this to your head tag
|
||||
The fastest way to get going with htmx is to load it via a CDN. You can simply add this to your head tag
|
||||
and get going:
|
||||
|
||||
```html
|
||||
@ -157,18 +157,18 @@ import 'htmx.org';
|
||||
If you want to use the global `htmx` variable (recommended), you need to inject it to the window scope:
|
||||
|
||||
* Create a custom JS file
|
||||
* Import this file to your `index.js` (below the import from step 2)
|
||||
|
||||
```js
|
||||
* Import this file to your `index.js` (below the import from step 2)
|
||||
|
||||
```js
|
||||
import 'path/to/my_custom.js';
|
||||
```
|
||||
|
||||
|
||||
* Then add this code to the file:
|
||||
|
||||
```js
|
||||
```js
|
||||
window.htmx = require('htmx.org');
|
||||
```
|
||||
|
||||
|
||||
* Finally, rebuild your bundle
|
||||
|
||||
## AJAX
|
||||
@ -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.
|
||||
@ -426,7 +427,7 @@ with any of the following values:
|
||||
#### Morph Swaps {#morphing}
|
||||
|
||||
In addition to the standard swap mechanisms above, htmx also supports _morphing_ swaps, via extensions. Morphing swaps
|
||||
attempt to _merge_ new content into the existing DOM, rather than simply replacing it, and often do a better job
|
||||
attempt to _merge_ new content into the existing DOM, rather than simply replacing it, and often do a better job
|
||||
preserving things like focus, video state, etc. by preserving nodes in-place during the swap operation.
|
||||
|
||||
The following extensions are available for morph-style swaps:
|
||||
@ -435,7 +436,7 @@ The following extensions are available for morph-style swaps:
|
||||
the original DOM morphing library.
|
||||
* [Alpine-morph](@/extensions/alpine-morph.md) - Based on the [alpine morph](https://alpinejs.dev/plugins/morph) plugin, plays
|
||||
well with alpine.js
|
||||
* [Idiomorph](https://github.com/bigskysoftware/idiomorph#htmx) - A newer morphing algorithm developed by us, the creators
|
||||
* [Idiomorph](https://github.com/bigskysoftware/idiomorph#htmx) - A newer morphing algorithm developed by us, the creators
|
||||
of htmx. Idiomorph will be available out of the box in htmx 2.0.
|
||||
|
||||
#### View Transitions {#view-transitions}
|
||||
@ -467,24 +468,24 @@ Consider a race condition between a form submission and an individual input's va
|
||||
|
||||
```html
|
||||
<form hx-post="/store">
|
||||
<input id="title" name="title" type="text"
|
||||
hx-post="/validate"
|
||||
<input id="title" name="title" type="text"
|
||||
hx-post="/validate"
|
||||
hx-trigger="change"
|
||||
>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
Without using `hx-sync`, filling out the input and immediately submitting the form triggers two parallel requests to
|
||||
`/validate` and `/store`.
|
||||
Without using `hx-sync`, filling out the input and immediately submitting the form triggers two parallel requests to
|
||||
`/validate` and `/store`.
|
||||
|
||||
Using `hx-sync="closest form:abort"` on the input will watch for requests on the form and abort the input's request if
|
||||
Using `hx-sync="closest form:abort"` on the input will watch for requests on the form and abort the input's request if
|
||||
a form request is present or starts while the input request is in flight:
|
||||
|
||||
```html
|
||||
<form hx-post="/store">
|
||||
<input id="title" name="title" type="text"
|
||||
hx-post="/validate"
|
||||
<input id="title" name="title" type="text"
|
||||
hx-post="/validate"
|
||||
hx-trigger="change"
|
||||
hx-sync="closest form:abort"
|
||||
>
|
||||
@ -510,7 +511,7 @@ More examples and details can be found on the [`hx-sync` attribute page.](@/attr
|
||||
|
||||
### CSS Transitions {#css_transitions}
|
||||
|
||||
htmx makes it easy to use [CSS Transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions) without
|
||||
htmx makes it easy to use [CSS Transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions) without
|
||||
javascript. Consider this HTML content:
|
||||
|
||||
```html
|
||||
@ -523,7 +524,7 @@ Imagine this content is replaced by htmx via an ajax request with this new conte
|
||||
<div id="div1" class="red">New Content</div>
|
||||
```
|
||||
|
||||
Note two things:
|
||||
Note two things:
|
||||
|
||||
* The div has the *same* id in the original and in the new content
|
||||
* The `red` class has been added to the new content
|
||||
@ -592,8 +593,8 @@ attribute on the elements you wish to be preserved.
|
||||
By default, an element that causes a request will include its value if it has one. If the element is a form it
|
||||
will include the values of all inputs within it.
|
||||
|
||||
As with HTML forms, the `name` attribute of the input is used as the parameter name in the request that htmx sends.
|
||||
|
||||
As with HTML forms, the `name` attribute of the input is used as the parameter name in the request that htmx sends.
|
||||
|
||||
Additionally, if the element causes a non-`GET` request, the values of all the inputs of the nearest enclosing form
|
||||
will be included.
|
||||
|
||||
@ -704,25 +705,25 @@ The anchor tag in this div will issue an AJAX `GET` request to `/blog` and swap
|
||||
### Progressive Enhancement {#progressive_enhancement}
|
||||
|
||||
A feature of `hx-boost` is that it degrades gracefully if javascript is not enabled: the links and forms continue
|
||||
to work, they simply don't use ajax requests. This is known as
|
||||
to work, they simply don't use ajax requests. This is known as
|
||||
[Progressive Enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement), and it allows
|
||||
a wider audience to use your sites functionality.
|
||||
|
||||
Other htmx patterns can be adapted to achieve progressive enhancement as well, but they will require more thought.
|
||||
Other htmx patterns can be adapted to achieve progressive enhancement as well, but they will require more thought.
|
||||
|
||||
Consider the [active search](@/examples/active-search.md) example. As it is written, it will not degrade gracefully:
|
||||
someone who does not have javascript enabled will not be able to use this feature. This is done for simplicity’s sake,
|
||||
to keep the example as brief as possible.
|
||||
|
||||
someone who does not have javascript enabled will not be able to use this feature. This is done for simplicity’s sake,
|
||||
to keep the example as brief as possible.
|
||||
|
||||
However, you could wrap the htmx-enhanced input in a form element:
|
||||
|
||||
```html
|
||||
<form action="/search" method="POST">
|
||||
<input class="form-control" type="search"
|
||||
name="search" placeholder="Begin typing to search users..."
|
||||
hx-post="/search"
|
||||
hx-trigger="keyup changed delay:500ms, search"
|
||||
hx-target="#search-results"
|
||||
<input class="form-control" type="search"
|
||||
name="search" placeholder="Begin typing to search users..."
|
||||
hx-post="/search"
|
||||
hx-trigger="keyup changed delay:500ms, search"
|
||||
hx-target="#search-results"
|
||||
hx-indicator=".htmx-indicator"
|
||||
>
|
||||
</form>
|
||||
@ -733,7 +734,7 @@ clients would be able to hit the enter key and still search. Even better, you c
|
||||
You would then need to update the form with an `hx-post` that mirrored the `action` attribute, or perhaps use `hx-boost`
|
||||
on it.
|
||||
|
||||
You would need to check on the server side for the `HX-Request` header to differentiate between an htmx-driven and a
|
||||
You would need to check on the server side for the `HX-Request` header to differentiate between an htmx-driven and a
|
||||
regular request, to determine exactly what to render to the client.
|
||||
|
||||
Other patterns can be adapted similarly to achieve the progressive enhancement needs of your application.
|
||||
@ -742,8 +743,8 @@ As you can see, this requires more thought and more work. It also rules some fu
|
||||
These tradeoffs must be made by you, the developer, with respect to your projects goals and audience.
|
||||
|
||||
[Accessibility](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/What_is_accessibility) is a concept
|
||||
closely related to progressive enhancement. Using progressive enhancement techniques such as `hx-boost` will make your
|
||||
htmx application more accessible to a wide array of users.
|
||||
closely related to progressive enhancement. Using progressive enhancement techniques such as `hx-boost` will make your
|
||||
htmx application more accessible to a wide array of users.
|
||||
|
||||
htmx-based applications are very similar to normal, non-AJAX driven web applications because htmx is HTML-oriented.
|
||||
|
||||
@ -764,7 +765,7 @@ and [Server Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Serve
|
||||
|
||||
**Note:** In htmx 2.0, these features will be migrated to extensions. These new extensions are already available in
|
||||
htmx 1.7+ and, if you are writing new code, you are encouraged to use the extensions instead. All new feature work for
|
||||
both SSE and web sockets will be done in the extensions.
|
||||
both SSE and web sockets will be done in the extensions.
|
||||
|
||||
Please visit the [SSE extension](@/extensions/server-sent-events.md) and [WebSocket extension](@/extensions/web-sockets.md)
|
||||
pages to learn more about the new extensions.
|
||||
@ -848,10 +849,10 @@ Careful: this element will need to be on all pages or restoring from history won
|
||||
|
||||
### Disabling History Snapshots
|
||||
|
||||
History snapshotting can be disabled for a URL by setting the [hx-history](@/attributes/hx-history.md) attribute to `false`
|
||||
History snapshotting can be disabled for a URL by setting the [hx-history](@/attributes/hx-history.md) attribute to `false`
|
||||
on any element in the current document, or any html fragment loaded into the current document by htmx. This can be used
|
||||
to prevent sensitive data entering the localStorage cache, which can be important for shared-use / public computers.
|
||||
History navigation will work as expected, but on restoration the URL will be requested from the server instead of the
|
||||
to prevent sensitive data entering the localStorage cache, which can be important for shared-use / public computers.
|
||||
History navigation will work as expected, but on restoration the URL will be requested from the server instead of the
|
||||
local history cache.
|
||||
|
||||
## Requests & Responses {#requests}
|
||||
@ -907,7 +908,7 @@ For more on the `HX-Trigger` headers, see [`HX-Trigger` Response Headers](@/head
|
||||
|
||||
Submitting a form via htmx has the benefit, that the [Post/Redirect/Get Pattern](https://en.wikipedia.org/wiki/Post/Redirect/Get) is not needed
|
||||
any more. After successful processing a POST request on the server, you don't need to return a [HTTP 302 (Redirect)](https://en.wikipedia.org/wiki/HTTP_302). You can directly return the new HTML fragment.
|
||||
|
||||
|
||||
### Request Order of Operations {#request-operations}
|
||||
|
||||
The order of operations in a htmx request are:
|
||||
@ -944,7 +945,7 @@ Htmx fires events around validation that can be used to hook in custom validatio
|
||||
* `htmx:validation:halted` - called when a request is not issued due to validation errors. Specific errors may be found
|
||||
in the `event.detail.errors` object
|
||||
|
||||
Non-form elements do not validate before they make requests by default, but you can enable validation by setting
|
||||
Non-form elements do not validate before they make requests by default, but you can enable validation by setting
|
||||
the [`hx-validate`](@/attributes/hx-validate.md) attribute to "true".
|
||||
|
||||
### Validation Example
|
||||
@ -1015,7 +1016,7 @@ document.body.addEventListener('htmx:load', function(evt) {
|
||||
myJavascriptLib.init(evt.detail.elt);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
or, if you would prefer, you can use the following htmx helper:
|
||||
|
||||
```javascript
|
||||
@ -1024,8 +1025,8 @@ htmx.on("htmx:load", function(evt) {
|
||||
});
|
||||
```
|
||||
|
||||
The `htmx:load` event is fired every time an element is loaded into the DOM by htmx, and is effectively the equivalent
|
||||
to the normal `load` event.
|
||||
The `htmx:load` event is fired every time an element is loaded into the DOM by htmx, and is effectively the equivalent
|
||||
to the normal `load` event.
|
||||
|
||||
Some common uses for htmx events are:
|
||||
|
||||
@ -1073,13 +1074,13 @@ document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
||||
} else if(evt.detail.xhr.status === 418){
|
||||
// if the response code 418 (I'm a teapot) is returned, retarget the
|
||||
// content of the response to the element with the id `teapot`
|
||||
evt.detail.shouldSwap = true;
|
||||
evt.detail.shouldSwap = true;
|
||||
evt.detail.target = htmx.find("#teapot");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Here we handle a few [400-level error response codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses)
|
||||
Here we handle a few [400-level error response codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses)
|
||||
that would normally not do a swap in htmx.
|
||||
|
||||
### Event Naming {#event_naming}
|
||||
@ -1108,20 +1109,20 @@ htmx.logger = function(elt, event, data) {
|
||||
|
||||
Declarative and event driven programming with htmx (or any other declartive language) can be a wonderful and highly productive
|
||||
activity, but one disadvantage when compared with imperative approaches is that it can be trickier to debug.
|
||||
|
||||
Figuring out why something *isn't* happening, for example, can be difficult if you don't know the tricks.
|
||||
|
||||
Figuring out why something *isn't* happening, for example, can be difficult if you don't know the tricks.
|
||||
|
||||
Well, here are the tricks:
|
||||
|
||||
|
||||
The first debugging tool you can use is the `htmx.logAll()` method. This will log every event that htmx triggers and
|
||||
will allow you to see exactly what the library is doing.
|
||||
will allow you to see exactly what the library is doing.
|
||||
|
||||
```javascript
|
||||
htmx.logAll();
|
||||
```
|
||||
|
||||
Of course, that won't tell you why htmx *isn't* doing something. You might also not know *what* events a DOM
|
||||
element is firing to use as a trigger. To address this, you can use the
|
||||
element is firing to use as a trigger. To address this, you can use the
|
||||
[`monitorEvents()`](https://developers.google.com/web/updates/2015/05/quickly-monitor-events-from-the-console-panel) method available in the
|
||||
browser console:
|
||||
|
||||
@ -1134,7 +1135,7 @@ to see exactly what is going on with it.
|
||||
|
||||
Note that this *only* works from the console, you cannot embed it in a script tag on your page.
|
||||
|
||||
Finally, push come shove, you might want to just debug `htmx.js` by loading up the unminimized version. It's
|
||||
Finally, push come shove, you might want to just debug `htmx.js` by loading up the unminimized version. It's
|
||||
about 2500 lines of javascript, so not an insurmountable amount of code. You would most likely want to set a break
|
||||
point in the `issueAjaxRequest()` and `handleAjaxResponse()` methods to see what's going on.
|
||||
|
||||
@ -1156,7 +1157,7 @@ Simply add the following script tag to your demo/fiddle/whatever:
|
||||
<script src="https://demo.htmx.org"></script>
|
||||
```
|
||||
|
||||
This helper allows you to add mock responses by adding `template` tags with a `url` attribute to indicate which URL.
|
||||
This helper allows you to add mock responses by adding `template` tags with a `url` attribute to indicate which URL.
|
||||
The response for that url will be the innerHTML of the template, making it easy to construct mock responses. You can
|
||||
add a delay to the response with a `delay` attribute, which should be an integer indicating the number of milliseconds
|
||||
to delay
|
||||
@ -1177,7 +1178,7 @@ Here is an example of the code in action:
|
||||
<!-- post to /foo -->
|
||||
<button hx-post="/foo" hx-target="#result">
|
||||
Count Up
|
||||
</button>
|
||||
</button>
|
||||
<output id="result"></output>
|
||||
|
||||
<!-- respond to /foo with some dynamic content in a template tag -->
|
||||
@ -1210,7 +1211,7 @@ integrating a JavaScript library with htmx via events.
|
||||
Scripting solutions that pair well with htmx include:
|
||||
|
||||
* [VanillaJS](http://vanilla-js.com/) - Simply using the built-in abilities of JavaScript to hook in event handlers to
|
||||
respond to the events htmx emits can work very well for scripting. This is an extremely lightweight and increasingly
|
||||
respond to the events htmx emits can work very well for scripting. This is an extremely lightweight and increasingly
|
||||
popular approach.
|
||||
* [AlpineJS](https://alpinejs.dev/) - Alpine.js provides a rich set of tools for creating sophisticated front end scripts,
|
||||
including reactive programming support, while still remaining extremely lightweight. Alpine encourages the "inline scripting"
|
||||
@ -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>
|
||||
```
|
||||
@ -1278,11 +1279,11 @@ Hyperscript is *not* required when using htmx, anything you can do in hyperscrip
|
||||
well together.
|
||||
|
||||
#### Installing Hyperscript
|
||||
|
||||
|
||||
To use hyperscript in combination with htmx, you need to [install the hyperscript library](https://unpkg.com/browse/hyperscript.org/)
|
||||
either via a CDN or locally. See the [hyperscript website](https://hyperscript.org) for the latest version of the
|
||||
library.
|
||||
|
||||
library.
|
||||
|
||||
When hyperscript is included, it will automatically integrate with htmx and begin processing all hyperscripts embedded
|
||||
in your HTML.
|
||||
|
||||
@ -1349,7 +1350,7 @@ In hyperscript you can implement similar functionality like so:
|
||||
### 3rd Party Javascript {#3rd-party}
|
||||
|
||||
Htmx integrates fairly well with third party libraries. If the library fires events on the DOM, you can use those events to
|
||||
trigger requests from htmx.
|
||||
trigger requests from htmx.
|
||||
|
||||
A good example of this is the [SortableJS demo](@/examples/sortable.md):
|
||||
|
||||
@ -1362,7 +1363,7 @@ A good example of this is the [SortableJS demo](@/examples/sortable.md):
|
||||
</form>
|
||||
```
|
||||
|
||||
With Sortable, as with most javascript libraries, you need to initialize content at some point.
|
||||
With Sortable, as with most javascript libraries, you need to initialize content at some point.
|
||||
|
||||
In jquery you might do this like so:
|
||||
|
||||
@ -1397,10 +1398,10 @@ htmx.onLoad(function(content) {
|
||||
|
||||
This will ensure that as new content is added to the DOM by htmx, sortable elements are properly initialized.
|
||||
|
||||
If javascript adds content to the DOM that has htmx attributes on it, you need to make sure that this content
|
||||
If javascript adds content to the DOM that has htmx attributes on it, you need to make sure that this content
|
||||
is initialized with the `htmx.process()` function.
|
||||
|
||||
For example, if you were to fetch some data and put it into a div using the `fetch` API, and that HTML had
|
||||
For example, if you were to fetch some data and put it into a div using the `fetch` API, and that HTML had
|
||||
htmx attributes in it, you would need to add a call to `htmx.process()` like this:
|
||||
|
||||
```js
|
||||
@ -1410,7 +1411,7 @@ fetch('http://example.com/movies.json')
|
||||
.then(data => { myDiv.innerHTML = data; htmx.process(myDiv); } );
|
||||
```
|
||||
|
||||
Some 3rd party libraries create content from HTML template elements. For instance, Alpine JS uses the `x-if`
|
||||
Some 3rd party libraries create content from HTML template elements. For instance, Alpine JS uses the `x-if`
|
||||
attribute on templates to add content conditionally. Such templates are not initially part of the DOM and,
|
||||
if they contain htmx attributes, will need a call to `htmx.process()` after they are loaded. The following
|
||||
example uses Alpine's `$watch` function to look for a change of value that would trigger conditional content:
|
||||
@ -1436,17 +1437,17 @@ example uses Alpine's `$watch` function to look for a change of value that would
|
||||
htmx works with standard [HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching)
|
||||
mechanisms out of the box.
|
||||
|
||||
If your server adds the
|
||||
[`Last-Modified`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified)
|
||||
HTTP response header to the response for a given URL, the browser will automatically add the
|
||||
[`If-Modified-Since`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since)
|
||||
request HTTP header to the next requests to the same URL. Be mindful that if
|
||||
If your server adds the
|
||||
[`Last-Modified`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified)
|
||||
HTTP response header to the response for a given URL, the browser will automatically add the
|
||||
[`If-Modified-Since`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since)
|
||||
request HTTP header to the next requests to the same URL. Be mindful that if
|
||||
your server can render different content for the same URL depending on some other
|
||||
headers, you need to use the [`Vary`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#vary)
|
||||
response HTTP header. For example, if your server renders the full HTML when the
|
||||
response HTTP header. For example, if your server renders the full HTML when the
|
||||
`HX-Request` header is missing or `false`, and it renders a fragment of that HTML
|
||||
when `HX-Request: true`, you need to add `Vary: HX-Request`. That causes the cache to be
|
||||
keyed based on a composite of the response URL and the `HX-Request` request header —
|
||||
keyed based on a composite of the response URL and the `HX-Request` request header —
|
||||
rather than being based just on the response URL.
|
||||
|
||||
If you are unable (or unwilling) to use the `Vary` header, you can alternatively set the configuration parameter
|
||||
@ -1455,25 +1456,25 @@ in `GET` requests that it makes, which will prevent browsers from caching htmx-b
|
||||
in the same cache slot.
|
||||
|
||||
htmx also works with [`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)
|
||||
as expected. Be mindful that if your server can render different content for the same
|
||||
URL (for example, depending on the value of the `HX-Request` header), the server needs
|
||||
as expected. Be mindful that if your server can render different content for the same
|
||||
URL (for example, depending on the value of the `HX-Request` header), the server needs
|
||||
to generate a different `ETag` for each content.
|
||||
|
||||
## Security
|
||||
|
||||
htmx allows you to define logic directly in your DOM. This has a number of advantages, the
|
||||
largest being [Locality of Behavior](@/essays/locality-of-behaviour.md) making your system
|
||||
largest being [Locality of Behavior](@/essays/locality-of-behaviour.md) making your system
|
||||
more coherent.
|
||||
|
||||
One concern with this approach, however, is security. This is especially the case if you are injecting user-created
|
||||
content into your site without any sort of HTML escaping discipline.
|
||||
content into your site without any sort of HTML escaping discipline.
|
||||
|
||||
You should, of course, escape all 3rd party untrusted content that is injected into your site to prevent, among other issues, [XSS attacks](https://en.wikipedia.org/wiki/Cross-site_scripting). Attributes starting with `hx-` and `data-hx`, as well as inline `<script>` tags should be filtered.
|
||||
|
||||
It is important to understand that htmx does *not* require inline scripts or `eval()` for most of its features. You (or your security team) may use a [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) that intentionally disallows inline scripts and the use of `eval()`. This, however, will have *no effect* on htmx functionality, which will still be able to execute JavaScript code placed in htmx attributes and may be a security concern. With that said, if your site relies on inline scripts that you do wish to allow and have a CSP in place, you may need to define [htmx.config.inlineScriptNonce](#config)--however, HTMX will add this nonce to *all* inline script tags it encounters, meaning a nonce-based CSP will no longer be effective for HTMX-loaded content.
|
||||
|
||||
To address this, if you don't want a particular part of the DOM to allow for htmx functionality, you can place the
|
||||
`hx-disable` or `data-hx-disable` attribute on the enclosing element of that area.
|
||||
`hx-disable` or `data-hx-disable` attribute on the enclosing element of that area.
|
||||
|
||||
This will prevent htmx from executing within that area in the DOM:
|
||||
|
||||
@ -1484,7 +1485,7 @@ This will prevent htmx from executing within that area in the DOM:
|
||||
```
|
||||
|
||||
This approach allows you to enjoy the benefits of [Locality of Behavior](@/essays/locality-of-behaviour.md)
|
||||
while still providing additional safety if your HTML-escaping discipline fails.
|
||||
while still providing additional safety if your HTML-escaping discipline fails.
|
||||
|
||||
## Configuring htmx {#config}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1527,7 +1529,7 @@ You can set them directly in javascript, or you can use a `meta` tag:
|
||||
|
||||
## Conclusion
|
||||
|
||||
And that's it!
|
||||
And that's it!
|
||||
|
||||
Have fun with htmx! You can accomplish [quite a bit](@/examples/_index.md) without writing a lot of code!
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
title = "Animations"
|
||||
template = "demo.html"
|
||||
+++
|
||||
|
||||
|
||||
htmx is designed to allow you to use [CSS transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions)
|
||||
to add smooth animations and transitions to your web page using only CSS and HTML. Below are a few examples of
|
||||
various animation techniques.
|
||||
@ -42,7 +42,7 @@ This div will poll every second and will get replaced with new content which cha
|
||||
Color Swap Demo
|
||||
</div>
|
||||
```
|
||||
|
||||
|
||||
Because the div has a stable id, `color-demo`, htmx will structure the swap such that a CSS transition, defined on the
|
||||
`.smooth` class, applies to the style update from `red` to `blue`, and smoothly transitions between them.
|
||||
|
||||
@ -71,7 +71,7 @@ Because the div has a stable id, `color-demo`, htmx will structure the swap such
|
||||
|
||||
### Smooth Progress Bar
|
||||
|
||||
The [Progress Bar](@/examples/progress-bar.md) demo uses this basic CSS animation technique as well, by updating the `length`
|
||||
The [Progress Bar](@/examples/progress-bar.md) demo uses this basic CSS animation technique as well, by updating the `length`
|
||||
property of a progress bar element, allowing for a smooth animation.
|
||||
|
||||
## Swap Transitions {#swapping}
|
||||
@ -79,7 +79,7 @@ property of a progress bar element, allowing for a smooth animation.
|
||||
### Fade Out On Swap
|
||||
|
||||
If you want to fade out an element that is going to be removed when the request ends, you want to take advantage
|
||||
of the `htmx-swapping` class with some CSS and extend the swap phase to be long enough for your animation to
|
||||
of the `htmx-swapping` class with some CSS and extend the swap phase to be long enough for your animation to
|
||||
complete. This can be done like so:
|
||||
|
||||
```html
|
||||
@ -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!"; });
|
||||
@ -238,14 +240,14 @@ the transition time. This avoids flickering that can happen if the transition i
|
||||
### Using the View Transition API {#view-transitions}
|
||||
|
||||
htmx provides access to the new [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)
|
||||
via the `transition` option of the [`hx-swap`](/attributes/hx-swap) attribute.
|
||||
via the `transition` option of the [`hx-swap`](/attributes/hx-swap) attribute.
|
||||
|
||||
Below is an example of a swap that uses a view transition. The transition is tied to the outer div via a
|
||||
Below is an example of a swap that uses a view transition. The transition is tied to the outer div via a
|
||||
`view-transition-name` property in CSS, and that transition is defined in terms of `::view-transition-old`
|
||||
and `::view-transition-new`, using `@keyframes` to define the animation. (Fuller details on the View Transition
|
||||
API can be found on the [Chrome Developer Page](https://developer.chrome.com/docs/web-platform/view-transitions/) on them.)
|
||||
|
||||
The old content of this transition should slide out to the left and the new content should slide in from the right.
|
||||
The old content of this transition should slide out to the left and the new content should slide in from the right.
|
||||
|
||||
Note that, as of this writing, the visual transition will only occur on Chrome 111+, but more browsers are expected to
|
||||
implement this feature in the near future.
|
||||
|
@ -2,15 +2,15 @@
|
||||
title = "Bulk Update"
|
||||
template = "demo.html"
|
||||
+++
|
||||
|
||||
This demo shows how to implement a common pattern where rows are selected and then bulk updated. This is
|
||||
|
||||
This demo shows how to implement a common pattern where rows are selected and then bulk updated. This is
|
||||
accomplished by putting a form around a table, with checkboxes in the table, and then including the checked
|
||||
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">
|
||||
@ -90,7 +90,7 @@ You can see a working example of this code below.
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
function getIds(params) {
|
||||
if(params['ids']) {
|
||||
if(Array.isArray(params['ids'])) {
|
||||
@ -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,11 +145,11 @@ 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>`
|
||||
}
|
||||
|
||||
|
||||
function displayTable(ids, contacts, action) {
|
||||
var txt = "";
|
||||
for (var i = 0; i < contacts.length; i++) {
|
||||
|
@ -36,7 +36,7 @@ The click to edit pattern provides a way to offer inline editing of all or part
|
||||
</div>
|
||||
<button class="btn">Submit</button>
|
||||
<button class="btn" hx-get="/contact/1">Cancel</button>
|
||||
</form>
|
||||
</form>
|
||||
```
|
||||
|
||||
* The form issues a `PUT` back to `/contacts/1`, following the usual REST-ful pattern.
|
||||
@ -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>`
|
||||
}
|
||||
|
@ -9,13 +9,13 @@ the final row:
|
||||
```html
|
||||
<tr id="replaceMe">
|
||||
<td colspan="3">
|
||||
<button class='btn' hx-get="/contacts/?page=2"
|
||||
hx-target="#replaceMe"
|
||||
<button class='btn' hx-get="/contacts/?page=2"
|
||||
hx-target="#replaceMe"
|
||||
hx-swap="outerHTML">
|
||||
Load More Agents... <img class="htmx-indicator" src="/img/bars.svg">
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
```
|
||||
|
||||
This row contains a button that will replace the entire row with the next page of
|
||||
@ -48,31 +48,31 @@ results (which will contain a button to load the *next* page of results). And s
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
// routes
|
||||
init("/demo", function(request, params){
|
||||
var contacts = dataStore.contactsForPage(1)
|
||||
return tableTemplate(contacts)
|
||||
});
|
||||
|
||||
|
||||
onGet(/\/contacts.*/, function(request, params){
|
||||
var page = parseInt(params['page']);
|
||||
var contacts = dataStore.contactsForPage(page)
|
||||
return rowsTemplate(page, contacts);
|
||||
});
|
||||
|
||||
|
||||
// templates
|
||||
function tableTemplate(contacts) {
|
||||
return `<table><thead><tr><th>Name</th><th>Email</th><th>ID</th></tr></thead><tbody>
|
||||
${rowsTemplate(1, contacts)}
|
||||
</tbody></table>`
|
||||
}
|
||||
|
||||
|
||||
function rowsTemplate(page, contacts) {
|
||||
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;
|
||||
@ -82,8 +82,8 @@ results (which will contain a button to load the *next* page of results). And s
|
||||
return `<tr id="replaceMe">
|
||||
<td colspan="3">
|
||||
<center>
|
||||
<button class='btn' hx-get="/contacts/?page=${page + 1}"
|
||||
hx-target="#replaceMe"
|
||||
<button class='btn' hx-get="/contacts/?page=${page + 1}"
|
||||
hx-target="#replaceMe"
|
||||
hx-swap="outerHTML">
|
||||
Load More Agents... <img class="htmx-indicator" src="/img/bars.svg">
|
||||
</button>
|
||||
|
@ -41,7 +41,7 @@ When a request occurs, it will return a partial to replace the outer div. It mi
|
||||
<input name="email" hx-post="/contact/email" hx-indicator="#ind" value="test@foo.com">
|
||||
<img id="ind" src="/img/bars.svg" class="htmx-indicator"/>
|
||||
<div class='error-message'>That email is already taken. Please enter another email.</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Note that this div is annotated with the `error` class and includes an error message element.
|
||||
@ -92,7 +92,7 @@ Below is a working demo of this example. The only email that will be accepted i
|
||||
onPost("/contact", function(request, params){
|
||||
return formTemplate();
|
||||
});
|
||||
|
||||
|
||||
onPost(/\/contact\/email.*/, function(request, params){
|
||||
var email = params['email'];
|
||||
if(!/\S+@\S+\.\S+/.test(email)) {
|
||||
@ -103,38 +103,38 @@ Below is a working demo of this example. The only email that will be accepted i
|
||||
return emailInputTemplate(email);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// templates
|
||||
function demoTemplate() {
|
||||
|
||||
|
||||
return `<h3>Signup Form</h3><p>Enter an email into the input below and on tab out it will be validated. Only "test@test.com" will pass.</p> ` + formTemplate();
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
|
@ -46,11 +46,11 @@ img {
|
||||
init("/demo", function(request, params){
|
||||
return lazyTemplate();
|
||||
});
|
||||
|
||||
|
||||
onGet("/graph", function(request, params){
|
||||
return "<img alt='Tokyo Climate' src='/img/tokyo.png'>";
|
||||
});
|
||||
|
||||
|
||||
// templates
|
||||
function lazyTemplate(page) {
|
||||
return `<div hx-get="/graph" hx-trigger="load">
|
||||
|
@ -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"
|
||||
hx-trigger="none"
|
||||
hx-swap="outerHTML">
|
||||
<h3>Complete</h3>
|
||||
<div class="progress">
|
||||
<div id="pb" class="progress-bar" style="width:100%"></div>
|
||||
<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-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>
|
||||
</div>
|
||||
</div>
|
||||
<button id="restart-btn" class="btn" hx-post="/start" classes="add show:600ms">
|
||||
Restart Job
|
||||
</button>
|
||||
|
||||
<button id="restart-btn" class="btn" hx-post="/start" classes="add show:600ms">
|
||||
Restart Job
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
@ -125,17 +136,26 @@ This example uses styling cribbed from the bootstrap progress bar:
|
||||
init("/demo", function(request, params){
|
||||
return startButton("Start Progress");
|
||||
});
|
||||
|
||||
|
||||
onPost("/start", function(request, params){
|
||||
var job = jobManager.start();
|
||||
return jobStatusTemplate(job);
|
||||
});
|
||||
|
||||
|
||||
onGet("/job", function(request, params){
|
||||
var job = jobManager.currentProcess();
|
||||
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">
|
||||
@ -145,23 +165,32 @@ This example uses styling cribbed from the bootstrap progress bar:
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
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>`
|
||||
}
|
||||
|
||||
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">
|
||||
<div id="pb" class="progress-bar" style="width:${job.percentComplete}%">
|
||||
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) {
|
||||
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,77 +15,91 @@ 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">
|
||||
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.
|
||||
<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>
|
||||
```
|
||||
|
||||
{{ 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">
|
||||
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.
|
||||
|
||||
<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>`
|
||||
})
|
||||
|
||||
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">
|
||||
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.
|
||||
Vexillologist eu nulla trust fund, street art blue bottle selvage raw denim.
|
||||
Dolore nulla do readymade, est subway tile affogato hammock 8-bit.
|
||||
Godard elit offal pariatur you probably haven't heard of them post-ironic.
|
||||
|
||||
<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.
|
||||
Vexillologist eu nulla trust fund, street art blue bottle selvage raw denim.
|
||||
Dolore nulla do readymade, est subway tile affogato hammock 8-bit.
|
||||
Godard elit offal pariatur you probably haven't heard of them post-ironic.
|
||||
Prism street art cray salvia.
|
||||
</div>`
|
||||
})
|
||||
|
||||
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">
|
||||
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.
|
||||
|
||||
<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.
|
||||
Iceland fanny pack tumeric magna activated charcoal bitters palo santo laboris quis consectetur cupidatat portland aliquip venmo.
|
||||
</div>`
|
||||
})
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ title = "Tabs (Using Hyperscript)"
|
||||
template = "demo.html"
|
||||
+++
|
||||
|
||||
This example shows how to load tab contents using htmx, and to select the "active" tab using Javascript. This reduces some duplication by offloading some of the work of re-rendering the tab HTML from your application server to your clients' browsers.
|
||||
This example shows how to load tab contents using htmx, and to select the "active" tab using Javascript. This reduces some duplication by offloading some of the work of re-rendering the tab HTML from your application server to your clients' browsers.
|
||||
|
||||
You may also consider [a more idiomatic approach](@/examples/tabs-hateoas.md) that follows the principle of [Hypertext As The Engine Of Application State](https://en.wikipedia.org/wiki/HATEOAS).
|
||||
|
||||
@ -12,54 +12,52 @@ 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>
|
||||
onGet("/tab1", function() {
|
||||
return `
|
||||
<p>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.
|
||||
<p>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.</p>
|
||||
`});
|
||||
|
||||
onGet("/tab2", function() {
|
||||
return `
|
||||
<p>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.
|
||||
Vexillologist eu nulla trust fund, street art blue bottle selvage raw denim.
|
||||
Dolore nulla do readymade, est subway tile affogato hammock 8-bit.
|
||||
Godard elit offal pariatur you probably haven't heard of them post-ironic.
|
||||
<p>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.
|
||||
Vexillologist eu nulla trust fund, street art blue bottle selvage raw denim.
|
||||
Dolore nulla do readymade, est subway tile affogato hammock 8-bit.
|
||||
Godard elit offal pariatur you probably haven't heard of them post-ironic.
|
||||
Prism street art cray salvia.</p>
|
||||
`
|
||||
});
|
||||
|
||||
onGet("/tab3", function() {
|
||||
return `
|
||||
<p>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.
|
||||
<p>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.
|
||||
Iceland fanny pack tumeric magna activated charcoal bitters palo santo laboris quis consectetur cupidatat portland aliquip venmo.</p>
|
||||
`
|
||||
});
|
||||
@ -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,9 +180,12 @@ 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>
|
||||
<pre class="language-html"><code class="language-html">${escapeHtml(details.xhr.response)}</code> </pre>
|
||||
</div>
|
||||
</span>`;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user