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

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

View File

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

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

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

View File

@ -1,6 +1,8 @@
# Changelog
## [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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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;
}

View File

@ -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) {

View File

@ -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;
}

View File

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

View File

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

View File

@ -241,13 +241,13 @@ describe("Core htmx API test", function(){
div3.classList.contains("foo").should.equal(true);
});
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 () {

View File

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

View File

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

View File

@ -130,7 +130,6 @@ describe("Core htmx Regression Tests", function(){
it('a form can reset based on the htmx:afterRequest event', function() {
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>')

View File

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

View File

@ -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();

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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

View File

@ -7,7 +7,8 @@ added to it for the duration of the request. This can be used to show spinners o
while the request is in flight.
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:

View File

@ -6,13 +6,54 @@ The `hx-on` attribute allows you to embed scripts inline to respond to events di
`hx-on` improves upon `onevent` by enabling the handling of any event for enhanced [Locality of Behaviour (LoB)](/essays/locality-of-behaviour/). This also enables you to handle any htmx event.
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.

View File

@ -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.

View File

@ -58,7 +58,7 @@ is seen again before the delay completes it is ignored, the element will trigger
* The extended CSS selector here allows for the following non-standard CSS values:
* `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,

View File

@ -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 simplicitys 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 simplicitys 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 &amp; 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!

View File

@ -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.

View File

@ -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++) {

View File

@ -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>`
}

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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 {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>`;
}