prep 1.9.7 release

This commit is contained in:
Carson Gross 2023-11-02 15:28:53 -06:00
parent 5cd5f927ba
commit 3912e3c2c2
27 changed files with 527 additions and 99 deletions

View File

@ -1,5 +1,16 @@
# Changelog
## [1.9.7] - 2023-11-03
* Fixed a bug where a button associated with a form that is swapped out of the DOM caused errors
* The `hx-target-error` attribute was added to the `response-targets` extension, allowing you to capture all 400 & 500
responses with a single attribute
* `hx-on` now properly supports multiple listeners
* The `hx-confirm` prompt is now passed into custom confirmation handlers
* `next` and `previous` are now valid _extended CSS_ symbols in htmx
* The `htmx:beforeHistoryUpdate` event was added
* Properly ignore the `dialog` formmethod on buttons when resolving the HTTP method to use
## [1.9.6] - 2023-09-22
* IE support has been restored (thank you @telroshan!)

View File

@ -33,7 +33,7 @@ By removing these arbitrary constraints htmx completes HTML as a
## quick start
```html
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
<script src="https://unpkg.com/htmx.org@1.9.7"></script>
<!-- have a button POST a click via AJAX -->
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me

View File

@ -28,15 +28,27 @@ htmx.defineExtension('client-side-templates', {
var handlebarsTemplate = htmx.closest(elt, "[handlebars-template]");
if (handlebarsTemplate) {
var data = JSON.parse(text);
var templateName = handlebarsTemplate.getAttribute('handlebars-template');
return Handlebars.partials[templateName](data);
var templateId = handlebarsTemplate.getAttribute('handlebars-template');
var templateElement = htmx.find('#' + templateId).innerHTML;
var renderTemplate = Handlebars.compile(templateElement);
if (renderTemplate) {
return renderTemplate(data);
} else {
throw "Unknown handlebars template: " + templateId;
}
}
var handlebarsArrayTemplate = htmx.closest(elt, "[handlebars-array-template]");
if (handlebarsArrayTemplate) {
var data = JSON.parse(text);
var templateName = handlebarsArrayTemplate.getAttribute('handlebars-array-template');
return Handlebars.partials[templateName]({"data": data});
var templateId = handlebarsArrayTemplate.getAttribute('handlebars-array-template');
var templateElement = htmx.find('#' + templateId).innerHTML;
var renderTemplate = Handlebars.compile(templateElement);
if (renderTemplate) {
return renderTemplate(data);
} else {
throw "Unknown handlebars template: " + templateId;
}
}
var nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
@ -50,7 +62,7 @@ htmx.defineExtension('client-side-templates', {
return nunjucks.render(templateName, data);
}
}
var xsltTemplate = htmx.closest(elt, "[xslt-template]");
if (xsltTemplate) {
var templateId = xsltTemplate.getAttribute('xslt-template');

View File

@ -38,6 +38,9 @@
'***',
'xxx',
];
if (respCode.startsWith('4') || respCode.startsWith('5')) {
attrPossibilities.push('error');
}
for (var i = 0; i < attrPossibilities.length; i++) {
var attr = attrPrefix + attrPossibilities[i];

2
dist/ext/ws.js vendored
View File

@ -379,7 +379,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
socketWrapper.send(body, elt);
if (api.shouldCancel(evt, elt)) {
if (evt && api.shouldCancel(evt, elt)) {
evt.preventDefault();
}
});

10
dist/htmx.d.ts vendored
View File

@ -385,16 +385,6 @@ export interface HtmxConfig {
disableSelector?: "[hx-disable], [data-hx-disable]" | string;
/** @default "smooth" */
scrollBehavior?: "smooth" | "auto";
/**
* If set to false, disables the interpretation of script tags.
* @default true
*/
allowScriptTags?: boolean;
/**
* If set to true, disables htmx-based requests to non-origin hosts.
* @default false
*/
selfRequestsOnly?: boolean;
}
/**

102
dist/htmx.js vendored
View File

@ -86,7 +86,7 @@ return (function () {
sock.binaryType = htmx.config.wsBinaryType;
return sock;
},
version: "1.9.6"
version: "1.9.7"
};
/** @type {import("./htmx").HtmxInternalApi} */
@ -596,8 +596,12 @@ return (function () {
return [closest(elt, normalizeSelector(selector.substr(8)))];
} else if (selector.indexOf("find ") === 0) {
return [find(elt, normalizeSelector(selector.substr(5)))];
} else if (selector === "next") {
return [elt.nextElementSibling]
} else if (selector.indexOf("next ") === 0) {
return [scanForwardQuery(elt, normalizeSelector(selector.substr(5)))];
} else if (selector === "previous") {
return [elt.previousElementSibling]
} else if (selector.indexOf("previous ") === 0) {
return [scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)))];
} else if (selector === 'document') {
@ -1279,12 +1283,14 @@ return (function () {
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
tokens.shift();
from_arg +=
" " +
consumeUntil(
tokens,
WHITESPACE_OR_COMMA
);
var selector = consumeUntil(
tokens,
WHITESPACE_OR_COMMA
)
// `next` and `previous` allow a selector-less syntax
if (selector.length > 0) {
from_arg += " " + selector;
}
}
triggerSpec.from = from_arg;
} else if (token === "target" && tokens[0] === ":") {
@ -1906,32 +1912,39 @@ return (function () {
}
}
function initButtonTracking(elt) {
// Handle submit buttons/inputs that have the form attribute set
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
var form = resolveTarget("#" + getRawAttribute(elt, "form")) || closest(elt, "form")
if (!form) {
return
// Handle submit buttons/inputs that have the form attribute set
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
function maybeSetLastButtonClicked(evt) {
var elt = closest(evt.target, "button, input[type='submit']");
var internalData = getRelatedFormData(evt)
if (internalData) {
internalData.lastButtonClicked = elt;
}
var maybeSetLastButtonClicked = function (evt) {
var elt = closest(evt.target, "button, input[type='submit']");
if (elt !== null) {
var internalData = getInternalData(form);
internalData.lastButtonClicked = elt;
}
};
};
function maybeUnsetLastButtonClicked(evt){
var internalData = getRelatedFormData(evt)
if (internalData) {
internalData.lastButtonClicked = null;
}
}
function getRelatedFormData(evt) {
var elt = closest(evt.target, "button, input[type='submit']");
if (!elt) {
return;
}
var form = resolveTarget('#' + getRawAttribute(elt, 'form')) || closest(elt, 'form');
if (!form) {
return;
}
return getInternalData(form);
}
function initButtonTracking(elt) {
// need to handle both click and focus in:
// focusin - in case someone tabs in to a button and hits the space bar
// click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724
elt.addEventListener('click', maybeSetLastButtonClicked)
elt.addEventListener('focusin', maybeSetLastButtonClicked)
elt.addEventListener('focusout', function(evt){
var internalData = getInternalData(form);
internalData.lastButtonClicked = null;
})
elt.addEventListener('focusout', maybeUnsetLastButtonClicked)
}
function countCurlies(line) {
@ -1950,7 +1963,9 @@ return (function () {
function addHxOnEventHandler(elt, eventName, code) {
var nodeData = getInternalData(elt);
nodeData.onHandlers = [];
if (!Array.isArray(nodeData.onHandlers)) {
nodeData.onHandlers = [];
}
var func;
var listener = function (e) {
return maybeEval(elt, function() {
@ -2169,6 +2184,12 @@ return (function () {
return;
}
if (htmx.config.historyCacheSize <= 0) {
// make sure that an eventually already existing cache is purged
localStorage.removeItem("htmx-history-cache");
return;
}
url = normalizePath(url);
var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
@ -2434,7 +2455,7 @@ return (function () {
if (shouldInclude(elt)) {
var name = getRawAttribute(elt,"name");
var value = elt.value;
if (elt.multiple) {
if (elt.multiple && elt.tagName === "SELECT") {
value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
}
// include file inputs
@ -2474,6 +2495,9 @@ return (function () {
var formValues = {};
var errors = [];
var internalData = getInternalData(elt);
if (internalData.lastButtonClicked && !bodyContains(internalData.lastButtonClicked)) {
internalData.lastButtonClicked = null
}
// only validate when form is directly submitted and novalidate or formnovalidate are not set
// or if the element has an explicit hx-validate="true" on it
@ -2959,16 +2983,20 @@ return (function () {
var buttonVerb = getRawAttribute(submitter, "formmethod")
if (buttonVerb != null) {
verb = buttonVerb;
// ignore buttons with formmethod="dialog"
if (buttonVerb.toLowerCase() !== "dialog") {
verb = buttonVerb;
}
}
}
var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
// allow event-based confirmation w/ a callback
if (!confirmed) {
var issueRequest = function() {
return issueAjaxRequest(verb, path, elt, event, etc, true);
if (confirmed === undefined) {
var issueRequest = function(skipConfirmation) {
return issueAjaxRequest(verb, path, elt, event, etc, !!skipConfirmation);
}
var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest};
var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest, question: confirmQuestion};
if (triggerEvent(elt, 'htmx:confirm', confirmDetails) === false) {
maybeCall(resolve);
return promise;
@ -3069,8 +3097,7 @@ return (function () {
}
}
var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
if (confirmQuestion) {
if (confirmQuestion && !confirmed) {
if(!confirm(confirmQuestion)) {
maybeCall(resolve);
endRequestLock()
@ -3529,6 +3556,7 @@ return (function () {
// if we need to save history, do so
if (historyUpdate.type) {
triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
if (historyUpdate.type === "push") {
pushUrlIntoHistory(historyUpdate.path);
triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
@ -3538,7 +3566,7 @@ return (function () {
}
}
if (responseInfo.pathInfo.anchor) {
var anchorTarget = find("#" + responseInfo.pathInfo.anchor);
var anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor);
if(anchorTarget) {
anchorTarget.scrollIntoView({block:'start', behavior: "auto"});
}

2
dist/htmx.min.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/htmx.min.js.gz vendored

Binary file not shown.

View File

@ -5,7 +5,7 @@
"AJAX",
"HTML"
],
"version": "1.9.6",
"version": "1.9.7",
"homepage": "https://htmx.org/",
"bugs": {
"url": "https://github.com/bigskysoftware/htmx/issues"

View File

@ -86,7 +86,7 @@ return (function () {
sock.binaryType = htmx.config.wsBinaryType;
return sock;
},
version: "1.9.6"
version: "1.9.7"
};
/** @type {import("./htmx").HtmxInternalApi} */

View File

@ -35,7 +35,7 @@ By removing these arbitrary constraints, htmx completes HTML as a [hypertext](ht
<h2>quick start</h2>
```html
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
<script src="https://unpkg.com/htmx.org@1.9.7"></script>
<!-- have a button POST a click via AJAX -->
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me

View File

@ -114,7 +114,7 @@ The fastest way to get going with htmx is to load it via a CDN. You can simply a
and get going:
```html
<script src="https://unpkg.com/htmx.org@1.9.6" integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni" crossorigin="anonymous"></script>
<script src="https://unpkg.com/htmx.org@1.9.7" integrity="sha384-DkR/zjYGLV0eOpkzbCfUJvsiyyYVP7iL/r5p5Fn+GU0VcrTEFbcjrXHgb2HPI718" crossorigin="anonymous"></script>
```
While the CDN approach is extremely simple, you may want to consider [not using CDNs in production](https://blog.wesleyac.com/posts/why-not-javascript-cdn).

View File

@ -0,0 +1,23 @@
+++
title = "htmx 1.9.7 has been released!"
date = 2023-11-03
[taxonomies]
tag = ["posts", "announcements"]
+++
## htmx 1.9.7 Release
I'm happy to announce the [1.9.7 release](https://unpkg.com/browse/htmx.org@1.9.7/) of htmx.
### Improvements & Bug fixes
* Fixed a bug where a button associated with a form that is swapped out of the DOM caused errors
* The `hx-target-error` attribute was added to the `response-targets` extension, allowing you to capture all 400 & 500
responses with a single attribute
* `hx-on` now properly supports multiple listeners
* The `hx-confirm` prompt is now passed into custom confirmation handlers
* `next` and `previous` are now valid _extended CSS_ symbols in htmx
* The `htmx:beforeHistoryUpdate` event was added
* Properly ignore the `dialog` formmethod on buttons when resolving the HTTP method to use
Thank you to everyone who contributed, and enjoy!

View File

@ -38,6 +38,9 @@
'***',
'xxx',
];
if (respCode.startsWith('4') || respCode.startsWith('5')) {
attrPossibilities.push('error');
}
for (var i = 0; i < attrPossibilities.length; i++) {
var attr = attrPrefix + attrPossibilities[i];

View File

@ -379,7 +379,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
socketWrapper.send(body, elt);
if (api.shouldCancel(evt, elt)) {
if (evt && api.shouldCancel(evt, elt)) {
evt.preventDefault();
}
});

View File

@ -86,7 +86,7 @@ return (function () {
sock.binaryType = htmx.config.wsBinaryType;
return sock;
},
version: "1.9.6"
version: "1.9.7"
};
/** @type {import("./htmx").HtmxInternalApi} */
@ -596,8 +596,12 @@ return (function () {
return [closest(elt, normalizeSelector(selector.substr(8)))];
} else if (selector.indexOf("find ") === 0) {
return [find(elt, normalizeSelector(selector.substr(5)))];
} else if (selector === "next") {
return [elt.nextElementSibling]
} else if (selector.indexOf("next ") === 0) {
return [scanForwardQuery(elt, normalizeSelector(selector.substr(5)))];
} else if (selector === "previous") {
return [elt.previousElementSibling]
} else if (selector.indexOf("previous ") === 0) {
return [scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)))];
} else if (selector === 'document') {
@ -1279,12 +1283,14 @@ return (function () {
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
tokens.shift();
from_arg +=
" " +
consumeUntil(
tokens,
WHITESPACE_OR_COMMA
);
var selector = consumeUntil(
tokens,
WHITESPACE_OR_COMMA
)
// `next` and `previous` allow a selector-less syntax
if (selector.length > 0) {
from_arg += " " + selector;
}
}
triggerSpec.from = from_arg;
} else if (token === "target" && tokens[0] === ":") {
@ -1906,32 +1912,39 @@ return (function () {
}
}
function initButtonTracking(elt) {
// Handle submit buttons/inputs that have the form attribute set
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
var form = resolveTarget("#" + getRawAttribute(elt, "form")) || closest(elt, "form")
if (!form) {
return
// Handle submit buttons/inputs that have the form attribute set
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
function maybeSetLastButtonClicked(evt) {
var elt = closest(evt.target, "button, input[type='submit']");
var internalData = getRelatedFormData(evt)
if (internalData) {
internalData.lastButtonClicked = elt;
}
var maybeSetLastButtonClicked = function (evt) {
var elt = closest(evt.target, "button, input[type='submit']");
if (elt !== null) {
var internalData = getInternalData(form);
internalData.lastButtonClicked = elt;
}
};
};
function maybeUnsetLastButtonClicked(evt){
var internalData = getRelatedFormData(evt)
if (internalData) {
internalData.lastButtonClicked = null;
}
}
function getRelatedFormData(evt) {
var elt = closest(evt.target, "button, input[type='submit']");
if (!elt) {
return;
}
var form = resolveTarget('#' + getRawAttribute(elt, 'form')) || closest(elt, 'form');
if (!form) {
return;
}
return getInternalData(form);
}
function initButtonTracking(elt) {
// need to handle both click and focus in:
// focusin - in case someone tabs in to a button and hits the space bar
// click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724
elt.addEventListener('click', maybeSetLastButtonClicked)
elt.addEventListener('focusin', maybeSetLastButtonClicked)
elt.addEventListener('focusout', function(evt){
var internalData = getInternalData(form);
internalData.lastButtonClicked = null;
})
elt.addEventListener('focusout', maybeUnsetLastButtonClicked)
}
function countCurlies(line) {
@ -1950,7 +1963,9 @@ return (function () {
function addHxOnEventHandler(elt, eventName, code) {
var nodeData = getInternalData(elt);
nodeData.onHandlers = [];
if (!Array.isArray(nodeData.onHandlers)) {
nodeData.onHandlers = [];
}
var func;
var listener = function (e) {
return maybeEval(elt, function() {
@ -2169,6 +2184,12 @@ return (function () {
return;
}
if (htmx.config.historyCacheSize <= 0) {
// make sure that an eventually already existing cache is purged
localStorage.removeItem("htmx-history-cache");
return;
}
url = normalizePath(url);
var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
@ -2434,7 +2455,7 @@ return (function () {
if (shouldInclude(elt)) {
var name = getRawAttribute(elt,"name");
var value = elt.value;
if (elt.multiple) {
if (elt.multiple && elt.tagName === "SELECT") {
value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
}
// include file inputs
@ -2474,6 +2495,9 @@ return (function () {
var formValues = {};
var errors = [];
var internalData = getInternalData(elt);
if (internalData.lastButtonClicked && !bodyContains(internalData.lastButtonClicked)) {
internalData.lastButtonClicked = null
}
// only validate when form is directly submitted and novalidate or formnovalidate are not set
// or if the element has an explicit hx-validate="true" on it
@ -2959,16 +2983,20 @@ return (function () {
var buttonVerb = getRawAttribute(submitter, "formmethod")
if (buttonVerb != null) {
verb = buttonVerb;
// ignore buttons with formmethod="dialog"
if (buttonVerb.toLowerCase() !== "dialog") {
verb = buttonVerb;
}
}
}
var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
// allow event-based confirmation w/ a callback
if (!confirmed) {
var issueRequest = function() {
return issueAjaxRequest(verb, path, elt, event, etc, true);
if (confirmed === undefined) {
var issueRequest = function(skipConfirmation) {
return issueAjaxRequest(verb, path, elt, event, etc, !!skipConfirmation);
}
var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest};
var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest, question: confirmQuestion};
if (triggerEvent(elt, 'htmx:confirm', confirmDetails) === false) {
maybeCall(resolve);
return promise;
@ -3069,8 +3097,7 @@ return (function () {
}
}
var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
if (confirmQuestion) {
if (confirmQuestion && !confirmed) {
if(!confirm(confirmQuestion)) {
maybeCall(resolve);
endRequestLock()
@ -3529,6 +3556,7 @@ return (function () {
// if we need to save history, do so
if (historyUpdate.type) {
triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
if (historyUpdate.type === "push") {
pushUrlIntoHistory(historyUpdate.path);
triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
@ -3538,7 +3566,7 @@ return (function () {
}
}
if (responseInfo.pathInfo.anchor) {
var anchorTarget = find("#" + responseInfo.pathInfo.anchor);
var anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor);
if(anchorTarget) {
anchorTarget.scrollIntoView({block:'start', behavior: "auto"});
}

View File

@ -0,0 +1,126 @@
describe("hx-confirm attribute", function () {
var confirm
beforeEach(function () {
this.server = makeServer();
confirm = sinon.stub(window, "confirm");
clearWorkArea();
});
afterEach(function () {
this.server.restore();
confirm.restore()
clearWorkArea();
});
it('prompts using window.confirm when hx-confirm is set', function () {
this.server.respondWith("GET", "/test", "Clicked!");
confirm.returns(true);
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
btn.click();
confirm.calledOnce.should.equal(true);
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
})
it('stops the request if confirm is cancelled', function () {
this.server.respondWith("GET", "/test", "Clicked!");
confirm.returns(false);
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
btn.click();
confirm.calledOnce.should.equal(true);
this.server.respond();
btn.innerHTML.should.equal("Click Me!");
})
it('uses the value of hx-confirm as the prompt', function () {
this.server.respondWith("GET", "/test", "Clicked!");
confirm.returns(false);
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
btn.click();
confirm.firstCall.args[0].should.equal("Sure?");
this.server.respond();
btn.innerHTML.should.equal("Click Me!");
})
it('should prompt when htmx:confirm handler calls issueRequest', function () {
try {
var btn = make('<button hx-get="/test" hx-confirm="Surely?">Click Me!</button>')
var handler = htmx.on("htmx:confirm", function (evt) {
evt.preventDefault();
evt.detail.issueRequest();
});
btn.click();
confirm.calledOnce.should.equal(true);
} finally {
htmx.off("htmx:confirm", handler);
}
})
it('should include the question in htmx:confirm event', function () {
var stub = sinon.stub();
try {
var btn = make('<button hx-get="/test" hx-confirm="Surely?">Click Me!</button>')
var handler = htmx.on("htmx:confirm", stub);
btn.click();
stub.calledOnce.should.equal(true);
stub.firstCall.args[0].detail.should.have.property("question", "Surely?");
} finally {
htmx.off("htmx:confirm", handler);
}
})
it('should allow skipping built-in window.confirm when using issueRequest', function () {
this.server.respondWith("GET", "/test", "Clicked!");
try {
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
var handler = htmx.on("htmx:confirm", function (evt) {
evt.detail.question.should.equal("Sure?");
evt.preventDefault();
evt.detail.issueRequest(true);
});
btn.click();
confirm.called.should.equal(false);
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
} finally {
htmx.off("htmx:confirm", handler);
}
})
it('should allow skipping built-in window.confirm when using issueRequest', function () {
this.server.respondWith("GET", "/test", "Clicked!");
try {
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
var handler = htmx.on("htmx:confirm", function (evt) {
evt.detail.question.should.equal("Sure?");
evt.preventDefault();
evt.detail.issueRequest(true);
});
btn.click();
confirm.called.should.equal(false);
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
} finally {
htmx.off("htmx:confirm", handler);
}
})
it('should allow htmx:confirm even when no hx-confirm is set', function () {
this.server.respondWith("GET", "/test", "Clicked!");
try {
var btn = make('<button hx-get="/test">Click Me!</button>')
var handler = htmx.on("htmx:confirm", function (evt) {
evt.detail.should.have.property("question", null);
evt.preventDefault();
evt.detail.issueRequest();
});
btn.click();
confirm.called.should.equal(false); // no hx-confirm means no window.confirm
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
} finally {
htmx.off("htmx:confirm", handler);
}
})
});

View File

@ -175,4 +175,22 @@ describe("hx-on:* attribute", function() {
delete window.foo;
delete window.bar;
});
it("cleans up all handlers when the DOM updates", function () {
// setup
window.foo = 0;
window.bar = 0;
var div = make("<div hx-on:increment-foo='window.foo++' hx-on:increment-bar='window.bar++'>Foo</div>");
make("<div>Another Div</div>"); // sole purpose is to update the DOM
// check there is just one handler against each event
htmx.trigger(div, "increment-foo");
htmx.trigger(div, "increment-bar");
window.foo.should.equal(1);
window.bar.should.equal(1);
// teardown
delete window.foo;
delete window.bar;
});
});

View File

@ -171,4 +171,22 @@ describe("hx-on attribute", function() {
delete window.foo;
delete window.bar;
});
it("cleans up all handlers when the DOM updates", function () {
// setup
window.foo = 0;
window.bar = 0;
var div = make("<div hx-on='increment-foo: window.foo++\nincrement-bar: window.bar++'>Foo</div>");
make("<div>Another Div</div>"); // sole purpose is to update the DOM
// check there is just one handler against each event
htmx.trigger(div, "increment-foo");
htmx.trigger(div, "increment-bar");
window.foo.should.equal(1);
window.bar.should.equal(1);
// teardown
delete window.foo;
delete window.bar;
});
});

View File

@ -202,4 +202,43 @@ describe("hx-target attribute", function(){
div3.innerHTML.should.equal("Clicked!");
});
it('targets a `next` element properly without selector', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
make('<div>' +
' <div id="d3"></div>' +
' <button id="b1" hx-target="next" hx-get="/test">Click Me!</button>' +
' <div id="d1"></div>' +
' <div id="d2"></div>' +
'</div>')
var btn = byId("b1")
var div1 = byId("d1")
var div2 = byId("d2")
var div3 = byId("d3")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
div2.innerHTML.should.equal("");
div3.innerHTML.should.equal("");
});
it('targets a `previous` element properly without selector', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
make('<div>' +
' <div id="d3"></div>' +
' <button id="b1" hx-target="previous" hx-get="/test">Click Me!</button>' +
' <div id="d1"></div>' +
' <div id="d2"></div>' +
'</div>')
var btn = byId("b1")
var div1 = byId("d1")
var div2 = byId("d2")
var div3 = byId("d3")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("");
div2.innerHTML.should.equal("");
div3.innerHTML.should.equal("Clicked!");
});
})

View File

@ -506,6 +506,36 @@ describe("hx-trigger attribute", function(){
a1.innerHTML.should.equal("Requests: 1");
});
it('from clause works with next', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
make('<div hx-trigger="click from:next" hx-target="#a1" hx-get="/test"></div><a id="a1">Requests: 0</a>');
var a1 = byId('a1');
a1.innerHTML.should.equal("Requests: 0");
a1.click();
this.server.respond();
a1.innerHTML.should.equal("Requests: 1");
});
it('from clause works with previous', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
make('<a id="a1">Requests: 0</a><div hx-trigger="click from:previous" hx-target="#a1" hx-get="/test"></div>');
var a1 = byId('a1');
a1.innerHTML.should.equal("Requests: 0");
a1.click();
this.server.respond();
a1.innerHTML.should.equal("Requests: 1");
});
it('event listeners on other elements are removed when an element is swapped out', function()
{
var requests = 0;

View File

@ -481,6 +481,33 @@ describe("Core htmx AJAX Tests", function(){
values.should.deep.equal({multiSelect:["m1", "m3", "m7", "m8"]});
});
it('properly handles multiple email input', function()
{
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(204, {}, "");
});
var form = make('<form hx-post="/test" hx-trigger="click">' +
'<input id="multiEmail" name="multiEmail" multiple>'+
'</form>');
form.click();
this.server.respond();
values.should.deep.equal({multiEmail: ''});
byId("multiEmail").value = 'foo@example.com';
form.click();
this.server.respond();
values.should.deep.equal({multiEmail:"foo@example.com"});
byId("multiEmail").value = 'foo@example.com,bar@example.com';
form.click();
this.server.respond();
values.should.deep.equal({multiEmail:"foo@example.com,bar@example.com"});
});
it('properly handles checkbox inputs', function()
{
var values;
@ -1230,4 +1257,59 @@ describe("Core htmx AJAX Tests", function(){
this.server.respond();
values.should.deep.equal({t1: 'textValue', b1: ['inputValue', 'buttonValue'], s1: "selectValue"});
})
it('handles form post with button formmethod dialog properly', function () {
var values;
this.server.respondWith("POST", "/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(200, {}, "");
});
make('<dialog><form hx-post="/test"><button id="submit" formmethod="dialog" name="foo" value="bar">submit</button></form></dialog>');
byId("submit").click();
this.server.respond();
values.should.deep.equal({ foo: 'bar' });
})
it('handles form get with button formmethod dialog properly', function () {
var responded = false;
this.server.respondWith("GET", "/test", function (xhr) {
responded = true;
xhr.respond(200, {}, "");
});
make('<dialog><form hx-get="/test"><button id="submit" formmethod="dialog">submit</button></form></dialog>');
byId("submit").click();
this.server.respond();
responded.should.equal(true);
it("can associate submit buttons from outside a form with the current version of the form after swap", function(){
const template = '<form ' +
'id="hello" ' +
'hx-target="#hello" ' +
'hx-select="#hello" ' +
'hx-swap="outerHTML" ' +
'hx-post="/test">\n' +
'<input id="input" type="text" name="name" />\n' +
'<button name="value" type="submit">Submit</button>\n' +
'</form>\n' +
'<button id="outside" name="outside" form="hello" type="submit">Outside</button>';
var values
this.server.respondWith("/test", function (xhr) {
values = getParameters(xhr);
xhr.respond(200, {}, template);
});
make(template);
const button = byId("outside");
button.focus();
button.click();
this.server.respond();
values.should.deep.equal({name: "", outside: ""});
button.focus();
button.click();
this.server.respond();
values.should.deep.equal({name: "", outside: ""});
})
})

View File

@ -659,7 +659,7 @@ describe("Core htmx Events", function() {
this.server.respond();
div.innerHTML.should.equal("updated");
} finally {
htmx.off("htmx:load", handler);
htmx.off("htmx:confirm", handler);
}
});

View File

@ -172,6 +172,18 @@ describe("web-sockets extension", function () {
this.messages.length.should.equal(1);
})
it('sends data to the server with polling trigger', function () {
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div hx-trigger="every 1s" ws-send id="d1">div1</div></div>');
this.tickMock();
this.clock.tick(2000);
byId("d1").click();
this.tickMock();
this.messages.length.should.equal(2);
})
it('sends expected headers to the server', function () {
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><button hx-trigger="click" hx-target="#target" ws-send id="d1" name="d1-name">div1</button><output id="target"></output></div>');
this.tickMock();

View File

@ -70,6 +70,7 @@
<!-- attribute tests -->
<script src="attributes/hx-boost.js"></script>
<script src="attributes/hx-confirm.js"></script>
<script src="attributes/hx-delete.js"></script>
<script src="attributes/hx-ext.js"></script>
<script src="attributes/hx-get.js"></script>

View File

@ -12,10 +12,14 @@ function make(htmlStr) {
var wa = getWorkArea();
var child = null;
var children = fragment.children || fragment.childNodes; // IE
var appendedChildren = []
while(children.length > 0) {
child = children[0];
wa.appendChild(child);
htmx.process(child);
appendedChildren.push(child)
}
for (var i = 0; i < appendedChildren.length; i++) {
htmx.process(appendedChildren[i]);
}
return child; // return last added element
};