prep release

This commit is contained in:
Carson Gross 2025-12-09 10:20:10 -07:00
parent f0ff590fb4
commit c917b4e880
33 changed files with 1120 additions and 183 deletions

131
dist/htmx.esm.js vendored
View File

@ -171,48 +171,60 @@ var htmx = (() => {
style === 'append' ? 'beforeend' : style;
}
#attributeValue(elt, name, defaultVal, returnElt) {
#findThisElements(elt, attrName) {
let result = [];
this.#attributeValue(elt, attrName, undefined, (val, elt) => {
if (val?.split(/\s*,\s*/).includes('this')) result.push(elt);
});
return result;
}
#attributeValue(elt, name, defaultVal, eltCollector) {
name = this.#prefix(name);
let appendName = name + this.#maybeAdjustMetaCharacter(":append");
let inheritName = name + (this.config.implicitInheritance ? "" : this.#maybeAdjustMetaCharacter(":inherited"));
let inheritAppendName = name + this.#maybeAdjustMetaCharacter(":inherited:append");
if (elt.hasAttribute(name)) {
return returnElt ? elt : elt.getAttribute(name);
let val = elt.getAttribute(name);
return eltCollector ? eltCollector(val, elt) : val;
}
if (elt.hasAttribute(inheritName)) {
return returnElt ? elt : elt.getAttribute(inheritName);
let val = elt.getAttribute(inheritName);
return eltCollector ? eltCollector(val, elt) : val;
}
if (elt.hasAttribute(appendName) || elt.hasAttribute(inheritAppendName)) {
let appendValue = elt.getAttribute(appendName) || elt.getAttribute(inheritAppendName);
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
if (parent) {
let inherited = this.#attributeValue(parent, name, undefined, returnElt);
return returnElt ? inherited : (inherited ? inherited + "," + appendValue : appendValue);
} else {
return returnElt ? elt : appendValue;
if (eltCollector) {
eltCollector(appendValue, elt);
}
if (parent) {
let inherited = this.#attributeValue(parent, name, undefined, eltCollector);
return inherited ? (inherited + "," + appendValue).replace(/[{}]/g, '') : appendValue;
}
return appendValue;
}
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
if (parent) {
let val = this.#attributeValue(parent, name, undefined, returnElt);
if (!returnElt && val && this.config.implicitInheritance) {
let val = this.#attributeValue(parent, name, undefined, eltCollector);
if (!eltCollector && val && this.config.implicitInheritance) {
this.#triggerExtensions(elt, "htmx:after:implicitInheritance", {elt, name, parent})
}
return val;
}
return returnElt ? elt : defaultVal;
return defaultVal;
}
#parseConfig(configString) {
if (configString[0] === '{') return JSON.parse(configString);
let configPattern = /([^\s,]+?)(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
let configPattern = /(?:"([^"]+)"|([^\s,:]+))(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
return [...configString.matchAll(configPattern)].reduce((result, match) => {
let keyPath = match[1].split('.');
let value = (match[2] ?? match[3] ?? match[4] ?? match[5] ?? 'true').trim();
let keyPath = (match[1] ?? match[2]).split('.');
let value = (match[3] ?? match[4] ?? match[5] ?? match[6] ?? 'true').trim();
if (value === 'true') value = true;
else if (value === 'false') value = false;
else if (/^\d+$/.test(value)) value = parseInt(value);
@ -319,7 +331,7 @@ var htmx = (() => {
action: fullAction,
anchor,
method,
headers: this.#determineHeaders(sourceElement),
headers: this.#createCoreHeaders(sourceElement),
abort: ac.abort.bind(ac),
credentials: "same-origin",
signal: ac.signal,
@ -350,7 +362,7 @@ var htmx = (() => {
return `${elt.tagName.toLowerCase()}${elt.id ? '#' + elt.id : ''}`;
}
#determineHeaders(elt) {
#createCoreHeaders(elt) {
let headers = {
"HX-Request": "true",
"HX-Source": this.#buildIdentifier(elt),
@ -360,19 +372,31 @@ var htmx = (() => {
if (this.#isBoosted(elt)) {
headers["HX-Boosted"] = "true"
}
let headersAttribute = this.#attributeValue(elt, "hx-headers");
if (headersAttribute) {
this.#mergeConfig(headersAttribute, headers);
}
return headers;
}
#handleHxHeaders(elt, headers) {
let result = this.#getAttributeObject(elt, "hx-headers");
if (result) {
if (result instanceof Promise) {
return result.then(obj => {
for (let key in obj) {
headers[key] = String(obj[key]);
}
});
} else {
for (let key in result) {
headers[key] = String(result[key]);
}
}
}
}
#resolveTarget(elt, selector) {
if (selector instanceof Element) {
return selector;
} else if (selector != null) {
let thisElt = this.#attributeValue(elt, "hx-target", undefined, true);
return this.#findAllExt(elt, selector, false, thisElt)[0];
return this.#findExt(elt, selector, "hx-target");
} else if (this.#isBoosted(elt)) {
return document.body
} else {
@ -406,6 +430,10 @@ var htmx = (() => {
}
}
// Handle dynamic headers
let headersResult = this.#handleHxHeaders(elt, ctx.request.headers)
if (headersResult) await headersResult // Only await if it returned a promise
// Add HX-Request-Type and HX-Target headers
ctx.request.headers["HX-Request-Type"] = (ctx.target === document.body || ctx.select) ? "full" : "partial";
if (ctx.target) {
@ -1476,7 +1504,7 @@ var htmx = (() => {
}
takeClass(element, className, container = element.parentElement) {
for (let elt of this.findAll(this.#normalizeElement(container), "." + className)) {
for (let elt of this.#findAllExt(this.#normalizeElement(container), "." + className)) {
elt.classList.remove(className);
}
element.classList.add(className);
@ -1657,8 +1685,7 @@ var htmx = (() => {
if (!indicatorsSelector) {
indicatorElements = [elt]
} else {
let thisElt = this.#attributeValue(elt, "hx-indicator", undefined, true);
indicatorElements = this.#findAllExt(elt, indicatorsSelector, false, thisElt);
indicatorElements = this.#findAllExt(elt, indicatorsSelector, "hx-indicator");
}
for (const indicator of indicatorElements) {
indicator._htmxReqCount ||= 0
@ -1684,7 +1711,7 @@ var htmx = (() => {
let disabledSelector = this.#attributeValue(elt, "hx-disable");
let disabledElements = []
if (disabledSelector) {
disabledElements = this.#queryEltAndDescendants(elt, disabledSelector);
disabledElements = this.#findAllExt(elt, disabledSelector, "hx-disable");
for (let indicator of disabledElements) {
indicator._htmxDisableCount ||= 0
indicator._htmxDisableCount++
@ -1760,22 +1787,36 @@ var htmx = (() => {
}
}
#getAttributeObject(elt, attrName) {
let attrValue = this.#attributeValue(elt, attrName);
if (!attrValue) return null;
let javascriptContent = this.#extractJavascriptContent(attrValue);
if (javascriptContent) {
// Wrap in braces if not already wrapped (for htmx 2.x compatibility)
if (javascriptContent.indexOf('{') !== 0) {
javascriptContent = '{' + javascriptContent + '}';
}
// Return promise for async evaluation
return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true);
} else {
// Synchronous path - return the parsed object directly
return this.#parseConfig(attrValue);
}
}
#handleHxVals(elt, body) {
let hxValsValue = this.#attributeValue(elt, "hx-vals");
if (hxValsValue) {
let javascriptContent = this.#extractJavascriptContent(hxValsValue);
if (javascriptContent) {
// Return promise for async evaluation
return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true).then(obj => {
let result = this.#getAttributeObject(elt, "hx-vals");
if (result) {
if (result instanceof Promise) {
return result.then(obj => {
for (let key in obj) {
body.append(key, obj[key])
body.set(key, obj[key])
}
});
} else {
// Synchronous path
let obj = this.#parseConfig(hxValsValue);
for (let key in obj) {
body.append(key, obj[key])
for (let key in result) {
body.set(key, result[key])
}
}
}
@ -1786,11 +1827,11 @@ var htmx = (() => {
return s.startsWith('<') && s.endsWith('/>') ? s.slice(1, -2) : s;
}
#findAllExt(eltOrSelector, maybeSelector, global, thisElt) {
#findAllExt(eltOrSelector, maybeSelector, thisAttr, global) {
let selector = maybeSelector ?? eltOrSelector;
let elt = maybeSelector ? this.#normalizeElement(eltOrSelector) : document;
if (selector.startsWith('global ')) {
return this.#findAllExt(elt, selector.slice(7), true, thisElt);
return this.#findAllExt(elt, selector.slice(7), thisAttr, true);
}
let parts = selector ? selector.replace(/<[^>]+\/>/g, m => m.replace(/,/g, '%2C'))
.split(',').map(p => p.replace(/%2C/g, ',')) : [];
@ -1822,7 +1863,11 @@ var htmx = (() => {
} else if (selector === 'host') {
item = (elt.getRootNode()).host
} else if (selector === 'this') {
item = thisElt || elt
if (thisAttr) {
result.push(...this.#findThisElements(elt, thisAttr));
continue;
}
item = elt
} else {
unprocessedParts.push(selector)
}
@ -1838,7 +1883,7 @@ var htmx = (() => {
result.push(...rootNode.querySelectorAll(standardSelector))
}
return result
return [...new Set(result)]
}
#scanForwardQuery(start, match, global) {
@ -1866,8 +1911,8 @@ var htmx = (() => {
}
}
#findExt(eltOrSelector, selector, thisElt) {
return this.#findAllExt(eltOrSelector, selector)[0]
#findExt(eltOrSelector, selector, thisAttr) {
return this.#findAllExt(eltOrSelector, selector, thisAttr)[0]
}
#extractJavascriptContent(string) {

BIN
dist/htmx.esm.js.br vendored

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

131
dist/htmx.js vendored
View File

@ -171,48 +171,60 @@ var htmx = (() => {
style === 'append' ? 'beforeend' : style;
}
#attributeValue(elt, name, defaultVal, returnElt) {
#findThisElements(elt, attrName) {
let result = [];
this.#attributeValue(elt, attrName, undefined, (val, elt) => {
if (val?.split(/\s*,\s*/).includes('this')) result.push(elt);
});
return result;
}
#attributeValue(elt, name, defaultVal, eltCollector) {
name = this.#prefix(name);
let appendName = name + this.#maybeAdjustMetaCharacter(":append");
let inheritName = name + (this.config.implicitInheritance ? "" : this.#maybeAdjustMetaCharacter(":inherited"));
let inheritAppendName = name + this.#maybeAdjustMetaCharacter(":inherited:append");
if (elt.hasAttribute(name)) {
return returnElt ? elt : elt.getAttribute(name);
let val = elt.getAttribute(name);
return eltCollector ? eltCollector(val, elt) : val;
}
if (elt.hasAttribute(inheritName)) {
return returnElt ? elt : elt.getAttribute(inheritName);
let val = elt.getAttribute(inheritName);
return eltCollector ? eltCollector(val, elt) : val;
}
if (elt.hasAttribute(appendName) || elt.hasAttribute(inheritAppendName)) {
let appendValue = elt.getAttribute(appendName) || elt.getAttribute(inheritAppendName);
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
if (parent) {
let inherited = this.#attributeValue(parent, name, undefined, returnElt);
return returnElt ? inherited : (inherited ? inherited + "," + appendValue : appendValue);
} else {
return returnElt ? elt : appendValue;
if (eltCollector) {
eltCollector(appendValue, elt);
}
if (parent) {
let inherited = this.#attributeValue(parent, name, undefined, eltCollector);
return inherited ? (inherited + "," + appendValue).replace(/[{}]/g, '') : appendValue;
}
return appendValue;
}
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
if (parent) {
let val = this.#attributeValue(parent, name, undefined, returnElt);
if (!returnElt && val && this.config.implicitInheritance) {
let val = this.#attributeValue(parent, name, undefined, eltCollector);
if (!eltCollector && val && this.config.implicitInheritance) {
this.#triggerExtensions(elt, "htmx:after:implicitInheritance", {elt, name, parent})
}
return val;
}
return returnElt ? elt : defaultVal;
return defaultVal;
}
#parseConfig(configString) {
if (configString[0] === '{') return JSON.parse(configString);
let configPattern = /([^\s,]+?)(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
let configPattern = /(?:"([^"]+)"|([^\s,:]+))(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
return [...configString.matchAll(configPattern)].reduce((result, match) => {
let keyPath = match[1].split('.');
let value = (match[2] ?? match[3] ?? match[4] ?? match[5] ?? 'true').trim();
let keyPath = (match[1] ?? match[2]).split('.');
let value = (match[3] ?? match[4] ?? match[5] ?? match[6] ?? 'true').trim();
if (value === 'true') value = true;
else if (value === 'false') value = false;
else if (/^\d+$/.test(value)) value = parseInt(value);
@ -319,7 +331,7 @@ var htmx = (() => {
action: fullAction,
anchor,
method,
headers: this.#determineHeaders(sourceElement),
headers: this.#createCoreHeaders(sourceElement),
abort: ac.abort.bind(ac),
credentials: "same-origin",
signal: ac.signal,
@ -350,7 +362,7 @@ var htmx = (() => {
return `${elt.tagName.toLowerCase()}${elt.id ? '#' + elt.id : ''}`;
}
#determineHeaders(elt) {
#createCoreHeaders(elt) {
let headers = {
"HX-Request": "true",
"HX-Source": this.#buildIdentifier(elt),
@ -360,19 +372,31 @@ var htmx = (() => {
if (this.#isBoosted(elt)) {
headers["HX-Boosted"] = "true"
}
let headersAttribute = this.#attributeValue(elt, "hx-headers");
if (headersAttribute) {
this.#mergeConfig(headersAttribute, headers);
}
return headers;
}
#handleHxHeaders(elt, headers) {
let result = this.#getAttributeObject(elt, "hx-headers");
if (result) {
if (result instanceof Promise) {
return result.then(obj => {
for (let key in obj) {
headers[key] = String(obj[key]);
}
});
} else {
for (let key in result) {
headers[key] = String(result[key]);
}
}
}
}
#resolveTarget(elt, selector) {
if (selector instanceof Element) {
return selector;
} else if (selector != null) {
let thisElt = this.#attributeValue(elt, "hx-target", undefined, true);
return this.#findAllExt(elt, selector, false, thisElt)[0];
return this.#findExt(elt, selector, "hx-target");
} else if (this.#isBoosted(elt)) {
return document.body
} else {
@ -406,6 +430,10 @@ var htmx = (() => {
}
}
// Handle dynamic headers
let headersResult = this.#handleHxHeaders(elt, ctx.request.headers)
if (headersResult) await headersResult // Only await if it returned a promise
// Add HX-Request-Type and HX-Target headers
ctx.request.headers["HX-Request-Type"] = (ctx.target === document.body || ctx.select) ? "full" : "partial";
if (ctx.target) {
@ -1476,7 +1504,7 @@ var htmx = (() => {
}
takeClass(element, className, container = element.parentElement) {
for (let elt of this.findAll(this.#normalizeElement(container), "." + className)) {
for (let elt of this.#findAllExt(this.#normalizeElement(container), "." + className)) {
elt.classList.remove(className);
}
element.classList.add(className);
@ -1657,8 +1685,7 @@ var htmx = (() => {
if (!indicatorsSelector) {
indicatorElements = [elt]
} else {
let thisElt = this.#attributeValue(elt, "hx-indicator", undefined, true);
indicatorElements = this.#findAllExt(elt, indicatorsSelector, false, thisElt);
indicatorElements = this.#findAllExt(elt, indicatorsSelector, "hx-indicator");
}
for (const indicator of indicatorElements) {
indicator._htmxReqCount ||= 0
@ -1684,7 +1711,7 @@ var htmx = (() => {
let disabledSelector = this.#attributeValue(elt, "hx-disable");
let disabledElements = []
if (disabledSelector) {
disabledElements = this.#queryEltAndDescendants(elt, disabledSelector);
disabledElements = this.#findAllExt(elt, disabledSelector, "hx-disable");
for (let indicator of disabledElements) {
indicator._htmxDisableCount ||= 0
indicator._htmxDisableCount++
@ -1760,22 +1787,36 @@ var htmx = (() => {
}
}
#getAttributeObject(elt, attrName) {
let attrValue = this.#attributeValue(elt, attrName);
if (!attrValue) return null;
let javascriptContent = this.#extractJavascriptContent(attrValue);
if (javascriptContent) {
// Wrap in braces if not already wrapped (for htmx 2.x compatibility)
if (javascriptContent.indexOf('{') !== 0) {
javascriptContent = '{' + javascriptContent + '}';
}
// Return promise for async evaluation
return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true);
} else {
// Synchronous path - return the parsed object directly
return this.#parseConfig(attrValue);
}
}
#handleHxVals(elt, body) {
let hxValsValue = this.#attributeValue(elt, "hx-vals");
if (hxValsValue) {
let javascriptContent = this.#extractJavascriptContent(hxValsValue);
if (javascriptContent) {
// Return promise for async evaluation
return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true).then(obj => {
let result = this.#getAttributeObject(elt, "hx-vals");
if (result) {
if (result instanceof Promise) {
return result.then(obj => {
for (let key in obj) {
body.append(key, obj[key])
body.set(key, obj[key])
}
});
} else {
// Synchronous path
let obj = this.#parseConfig(hxValsValue);
for (let key in obj) {
body.append(key, obj[key])
for (let key in result) {
body.set(key, result[key])
}
}
}
@ -1786,11 +1827,11 @@ var htmx = (() => {
return s.startsWith('<') && s.endsWith('/>') ? s.slice(1, -2) : s;
}
#findAllExt(eltOrSelector, maybeSelector, global, thisElt) {
#findAllExt(eltOrSelector, maybeSelector, thisAttr, global) {
let selector = maybeSelector ?? eltOrSelector;
let elt = maybeSelector ? this.#normalizeElement(eltOrSelector) : document;
if (selector.startsWith('global ')) {
return this.#findAllExt(elt, selector.slice(7), true, thisElt);
return this.#findAllExt(elt, selector.slice(7), thisAttr, true);
}
let parts = selector ? selector.replace(/<[^>]+\/>/g, m => m.replace(/,/g, '%2C'))
.split(',').map(p => p.replace(/%2C/g, ',')) : [];
@ -1822,7 +1863,11 @@ var htmx = (() => {
} else if (selector === 'host') {
item = (elt.getRootNode()).host
} else if (selector === 'this') {
item = thisElt || elt
if (thisAttr) {
result.push(...this.#findThisElements(elt, thisAttr));
continue;
}
item = elt
} else {
unprocessedParts.push(selector)
}
@ -1838,7 +1883,7 @@ var htmx = (() => {
result.push(...rootNode.querySelectorAll(standardSelector))
}
return result
return [...new Set(result)]
}
#scanForwardQuery(start, match, global) {
@ -1866,8 +1911,8 @@ var htmx = (() => {
}
}
#findExt(eltOrSelector, selector, thisElt) {
return this.#findAllExt(eltOrSelector, selector)[0]
#findExt(eltOrSelector, selector, thisAttr) {
return this.#findAllExt(eltOrSelector, selector, thisAttr)[0]
}
#extractJavascriptContent(string) {

BIN
dist/htmx.js.br vendored

Binary file not shown.

2
dist/htmx.min.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/htmx.min.js.br vendored

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -171,48 +171,60 @@ var htmx = (() => {
style === 'append' ? 'beforeend' : style;
}
#attributeValue(elt, name, defaultVal, returnElt) {
#findThisElements(elt, attrName) {
let result = [];
this.#attributeValue(elt, attrName, undefined, (val, elt) => {
if (val?.split(/\s*,\s*/).includes('this')) result.push(elt);
});
return result;
}
#attributeValue(elt, name, defaultVal, eltCollector) {
name = this.#prefix(name);
let appendName = name + this.#maybeAdjustMetaCharacter(":append");
let inheritName = name + (this.config.implicitInheritance ? "" : this.#maybeAdjustMetaCharacter(":inherited"));
let inheritAppendName = name + this.#maybeAdjustMetaCharacter(":inherited:append");
if (elt.hasAttribute(name)) {
return returnElt ? elt : elt.getAttribute(name);
let val = elt.getAttribute(name);
return eltCollector ? eltCollector(val, elt) : val;
}
if (elt.hasAttribute(inheritName)) {
return returnElt ? elt : elt.getAttribute(inheritName);
let val = elt.getAttribute(inheritName);
return eltCollector ? eltCollector(val, elt) : val;
}
if (elt.hasAttribute(appendName) || elt.hasAttribute(inheritAppendName)) {
let appendValue = elt.getAttribute(appendName) || elt.getAttribute(inheritAppendName);
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
if (parent) {
let inherited = this.#attributeValue(parent, name, undefined, returnElt);
return returnElt ? inherited : (inherited ? inherited + "," + appendValue : appendValue);
} else {
return returnElt ? elt : appendValue;
if (eltCollector) {
eltCollector(appendValue, elt);
}
if (parent) {
let inherited = this.#attributeValue(parent, name, undefined, eltCollector);
return inherited ? (inherited + "," + appendValue).replace(/[{}]/g, '') : appendValue;
}
return appendValue;
}
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
if (parent) {
let val = this.#attributeValue(parent, name, undefined, returnElt);
if (!returnElt && val && this.config.implicitInheritance) {
let val = this.#attributeValue(parent, name, undefined, eltCollector);
if (!eltCollector && val && this.config.implicitInheritance) {
this.#triggerExtensions(elt, "htmx:after:implicitInheritance", {elt, name, parent})
}
return val;
}
return returnElt ? elt : defaultVal;
return defaultVal;
}
#parseConfig(configString) {
if (configString[0] === '{') return JSON.parse(configString);
let configPattern = /([^\s,]+?)(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
let configPattern = /(?:"([^"]+)"|([^\s,:]+))(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
return [...configString.matchAll(configPattern)].reduce((result, match) => {
let keyPath = match[1].split('.');
let value = (match[2] ?? match[3] ?? match[4] ?? match[5] ?? 'true').trim();
let keyPath = (match[1] ?? match[2]).split('.');
let value = (match[3] ?? match[4] ?? match[5] ?? match[6] ?? 'true').trim();
if (value === 'true') value = true;
else if (value === 'false') value = false;
else if (/^\d+$/.test(value)) value = parseInt(value);
@ -319,7 +331,7 @@ var htmx = (() => {
action: fullAction,
anchor,
method,
headers: this.#determineHeaders(sourceElement),
headers: this.#createCoreHeaders(sourceElement),
abort: ac.abort.bind(ac),
credentials: "same-origin",
signal: ac.signal,
@ -350,7 +362,7 @@ var htmx = (() => {
return `${elt.tagName.toLowerCase()}${elt.id ? '#' + elt.id : ''}`;
}
#determineHeaders(elt) {
#createCoreHeaders(elt) {
let headers = {
"HX-Request": "true",
"HX-Source": this.#buildIdentifier(elt),
@ -360,19 +372,31 @@ var htmx = (() => {
if (this.#isBoosted(elt)) {
headers["HX-Boosted"] = "true"
}
let headersAttribute = this.#attributeValue(elt, "hx-headers");
if (headersAttribute) {
this.#mergeConfig(headersAttribute, headers);
}
return headers;
}
#handleHxHeaders(elt, headers) {
let result = this.#getAttributeObject(elt, "hx-headers");
if (result) {
if (result instanceof Promise) {
return result.then(obj => {
for (let key in obj) {
headers[key] = String(obj[key]);
}
});
} else {
for (let key in result) {
headers[key] = String(result[key]);
}
}
}
}
#resolveTarget(elt, selector) {
if (selector instanceof Element) {
return selector;
} else if (selector != null) {
let thisElt = this.#attributeValue(elt, "hx-target", undefined, true);
return this.#findAllExt(elt, selector, false, thisElt)[0];
return this.#findExt(elt, selector, "hx-target");
} else if (this.#isBoosted(elt)) {
return document.body
} else {
@ -406,6 +430,10 @@ var htmx = (() => {
}
}
// Handle dynamic headers
let headersResult = this.#handleHxHeaders(elt, ctx.request.headers)
if (headersResult) await headersResult // Only await if it returned a promise
// Add HX-Request-Type and HX-Target headers
ctx.request.headers["HX-Request-Type"] = (ctx.target === document.body || ctx.select) ? "full" : "partial";
if (ctx.target) {
@ -1476,7 +1504,7 @@ var htmx = (() => {
}
takeClass(element, className, container = element.parentElement) {
for (let elt of this.findAll(this.#normalizeElement(container), "." + className)) {
for (let elt of this.#findAllExt(this.#normalizeElement(container), "." + className)) {
elt.classList.remove(className);
}
element.classList.add(className);
@ -1657,8 +1685,7 @@ var htmx = (() => {
if (!indicatorsSelector) {
indicatorElements = [elt]
} else {
let thisElt = this.#attributeValue(elt, "hx-indicator", undefined, true);
indicatorElements = this.#findAllExt(elt, indicatorsSelector, false, thisElt);
indicatorElements = this.#findAllExt(elt, indicatorsSelector, "hx-indicator");
}
for (const indicator of indicatorElements) {
indicator._htmxReqCount ||= 0
@ -1684,7 +1711,7 @@ var htmx = (() => {
let disabledSelector = this.#attributeValue(elt, "hx-disable");
let disabledElements = []
if (disabledSelector) {
disabledElements = this.#queryEltAndDescendants(elt, disabledSelector);
disabledElements = this.#findAllExt(elt, disabledSelector, "hx-disable");
for (let indicator of disabledElements) {
indicator._htmxDisableCount ||= 0
indicator._htmxDisableCount++
@ -1760,22 +1787,36 @@ var htmx = (() => {
}
}
#getAttributeObject(elt, attrName) {
let attrValue = this.#attributeValue(elt, attrName);
if (!attrValue) return null;
let javascriptContent = this.#extractJavascriptContent(attrValue);
if (javascriptContent) {
// Wrap in braces if not already wrapped (for htmx 2.x compatibility)
if (javascriptContent.indexOf('{') !== 0) {
javascriptContent = '{' + javascriptContent + '}';
}
// Return promise for async evaluation
return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true);
} else {
// Synchronous path - return the parsed object directly
return this.#parseConfig(attrValue);
}
}
#handleHxVals(elt, body) {
let hxValsValue = this.#attributeValue(elt, "hx-vals");
if (hxValsValue) {
let javascriptContent = this.#extractJavascriptContent(hxValsValue);
if (javascriptContent) {
// Return promise for async evaluation
return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true).then(obj => {
let result = this.#getAttributeObject(elt, "hx-vals");
if (result) {
if (result instanceof Promise) {
return result.then(obj => {
for (let key in obj) {
body.append(key, obj[key])
body.set(key, obj[key])
}
});
} else {
// Synchronous path
let obj = this.#parseConfig(hxValsValue);
for (let key in obj) {
body.append(key, obj[key])
for (let key in result) {
body.set(key, result[key])
}
}
}
@ -1786,11 +1827,11 @@ var htmx = (() => {
return s.startsWith('<') && s.endsWith('/>') ? s.slice(1, -2) : s;
}
#findAllExt(eltOrSelector, maybeSelector, global, thisElt) {
#findAllExt(eltOrSelector, maybeSelector, thisAttr, global) {
let selector = maybeSelector ?? eltOrSelector;
let elt = maybeSelector ? this.#normalizeElement(eltOrSelector) : document;
if (selector.startsWith('global ')) {
return this.#findAllExt(elt, selector.slice(7), true, thisElt);
return this.#findAllExt(elt, selector.slice(7), thisAttr, true);
}
let parts = selector ? selector.replace(/<[^>]+\/>/g, m => m.replace(/,/g, '%2C'))
.split(',').map(p => p.replace(/%2C/g, ',')) : [];
@ -1822,7 +1863,11 @@ var htmx = (() => {
} else if (selector === 'host') {
item = (elt.getRootNode()).host
} else if (selector === 'this') {
item = thisElt || elt
if (thisAttr) {
result.push(...this.#findThisElements(elt, thisAttr));
continue;
}
item = elt
} else {
unprocessedParts.push(selector)
}
@ -1838,7 +1883,7 @@ var htmx = (() => {
result.push(...rootNode.querySelectorAll(standardSelector))
}
return result
return [...new Set(result)]
}
#scanForwardQuery(start, match, global) {
@ -1866,8 +1911,8 @@ var htmx = (() => {
}
}
#findExt(eltOrSelector, selector, thisElt) {
return this.#findAllExt(eltOrSelector, selector)[0]
#findExt(eltOrSelector, selector, thisAttr) {
return this.#findAllExt(eltOrSelector, selector, thisAttr)[0]
}
#extractJavascriptContent(string) {

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -171,48 +171,60 @@ var htmx = (() => {
style === 'append' ? 'beforeend' : style;
}
#attributeValue(elt, name, defaultVal, returnElt) {
#findThisElements(elt, attrName) {
let result = [];
this.#attributeValue(elt, attrName, undefined, (val, elt) => {
if (val?.split(/\s*,\s*/).includes('this')) result.push(elt);
});
return result;
}
#attributeValue(elt, name, defaultVal, eltCollector) {
name = this.#prefix(name);
let appendName = name + this.#maybeAdjustMetaCharacter(":append");
let inheritName = name + (this.config.implicitInheritance ? "" : this.#maybeAdjustMetaCharacter(":inherited"));
let inheritAppendName = name + this.#maybeAdjustMetaCharacter(":inherited:append");
if (elt.hasAttribute(name)) {
return returnElt ? elt : elt.getAttribute(name);
let val = elt.getAttribute(name);
return eltCollector ? eltCollector(val, elt) : val;
}
if (elt.hasAttribute(inheritName)) {
return returnElt ? elt : elt.getAttribute(inheritName);
let val = elt.getAttribute(inheritName);
return eltCollector ? eltCollector(val, elt) : val;
}
if (elt.hasAttribute(appendName) || elt.hasAttribute(inheritAppendName)) {
let appendValue = elt.getAttribute(appendName) || elt.getAttribute(inheritAppendName);
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
if (parent) {
let inherited = this.#attributeValue(parent, name, undefined, returnElt);
return returnElt ? inherited : (inherited ? inherited + "," + appendValue : appendValue);
} else {
return returnElt ? elt : appendValue;
if (eltCollector) {
eltCollector(appendValue, elt);
}
if (parent) {
let inherited = this.#attributeValue(parent, name, undefined, eltCollector);
return inherited ? (inherited + "," + appendValue).replace(/[{}]/g, '') : appendValue;
}
return appendValue;
}
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
if (parent) {
let val = this.#attributeValue(parent, name, undefined, returnElt);
if (!returnElt && val && this.config.implicitInheritance) {
let val = this.#attributeValue(parent, name, undefined, eltCollector);
if (!eltCollector && val && this.config.implicitInheritance) {
this.#triggerExtensions(elt, "htmx:after:implicitInheritance", {elt, name, parent})
}
return val;
}
return returnElt ? elt : defaultVal;
return defaultVal;
}
#parseConfig(configString) {
if (configString[0] === '{') return JSON.parse(configString);
let configPattern = /([^\s,]+?)(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
let configPattern = /(?:"([^"]+)"|([^\s,:]+))(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
return [...configString.matchAll(configPattern)].reduce((result, match) => {
let keyPath = match[1].split('.');
let value = (match[2] ?? match[3] ?? match[4] ?? match[5] ?? 'true').trim();
let keyPath = (match[1] ?? match[2]).split('.');
let value = (match[3] ?? match[4] ?? match[5] ?? match[6] ?? 'true').trim();
if (value === 'true') value = true;
else if (value === 'false') value = false;
else if (/^\d+$/.test(value)) value = parseInt(value);
@ -319,7 +331,7 @@ var htmx = (() => {
action: fullAction,
anchor,
method,
headers: this.#determineHeaders(sourceElement),
headers: this.#createCoreHeaders(sourceElement),
abort: ac.abort.bind(ac),
credentials: "same-origin",
signal: ac.signal,
@ -350,7 +362,7 @@ var htmx = (() => {
return `${elt.tagName.toLowerCase()}${elt.id ? '#' + elt.id : ''}`;
}
#determineHeaders(elt) {
#createCoreHeaders(elt) {
let headers = {
"HX-Request": "true",
"HX-Source": this.#buildIdentifier(elt),
@ -360,19 +372,31 @@ var htmx = (() => {
if (this.#isBoosted(elt)) {
headers["HX-Boosted"] = "true"
}
let headersAttribute = this.#attributeValue(elt, "hx-headers");
if (headersAttribute) {
this.#mergeConfig(headersAttribute, headers);
}
return headers;
}
#handleHxHeaders(elt, headers) {
let result = this.#getAttributeObject(elt, "hx-headers");
if (result) {
if (result instanceof Promise) {
return result.then(obj => {
for (let key in obj) {
headers[key] = String(obj[key]);
}
});
} else {
for (let key in result) {
headers[key] = String(result[key]);
}
}
}
}
#resolveTarget(elt, selector) {
if (selector instanceof Element) {
return selector;
} else if (selector != null) {
let thisElt = this.#attributeValue(elt, "hx-target", undefined, true);
return this.#findAllExt(elt, selector, false, thisElt)[0];
return this.#findExt(elt, selector, "hx-target");
} else if (this.#isBoosted(elt)) {
return document.body
} else {
@ -406,6 +430,10 @@ var htmx = (() => {
}
}
// Handle dynamic headers
let headersResult = this.#handleHxHeaders(elt, ctx.request.headers)
if (headersResult) await headersResult // Only await if it returned a promise
// Add HX-Request-Type and HX-Target headers
ctx.request.headers["HX-Request-Type"] = (ctx.target === document.body || ctx.select) ? "full" : "partial";
if (ctx.target) {
@ -1476,7 +1504,7 @@ var htmx = (() => {
}
takeClass(element, className, container = element.parentElement) {
for (let elt of this.findAll(this.#normalizeElement(container), "." + className)) {
for (let elt of this.#findAllExt(this.#normalizeElement(container), "." + className)) {
elt.classList.remove(className);
}
element.classList.add(className);
@ -1657,8 +1685,7 @@ var htmx = (() => {
if (!indicatorsSelector) {
indicatorElements = [elt]
} else {
let thisElt = this.#attributeValue(elt, "hx-indicator", undefined, true);
indicatorElements = this.#findAllExt(elt, indicatorsSelector, false, thisElt);
indicatorElements = this.#findAllExt(elt, indicatorsSelector, "hx-indicator");
}
for (const indicator of indicatorElements) {
indicator._htmxReqCount ||= 0
@ -1684,7 +1711,7 @@ var htmx = (() => {
let disabledSelector = this.#attributeValue(elt, "hx-disable");
let disabledElements = []
if (disabledSelector) {
disabledElements = this.#queryEltAndDescendants(elt, disabledSelector);
disabledElements = this.#findAllExt(elt, disabledSelector, "hx-disable");
for (let indicator of disabledElements) {
indicator._htmxDisableCount ||= 0
indicator._htmxDisableCount++
@ -1760,22 +1787,36 @@ var htmx = (() => {
}
}
#getAttributeObject(elt, attrName) {
let attrValue = this.#attributeValue(elt, attrName);
if (!attrValue) return null;
let javascriptContent = this.#extractJavascriptContent(attrValue);
if (javascriptContent) {
// Wrap in braces if not already wrapped (for htmx 2.x compatibility)
if (javascriptContent.indexOf('{') !== 0) {
javascriptContent = '{' + javascriptContent + '}';
}
// Return promise for async evaluation
return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true);
} else {
// Synchronous path - return the parsed object directly
return this.#parseConfig(attrValue);
}
}
#handleHxVals(elt, body) {
let hxValsValue = this.#attributeValue(elt, "hx-vals");
if (hxValsValue) {
let javascriptContent = this.#extractJavascriptContent(hxValsValue);
if (javascriptContent) {
// Return promise for async evaluation
return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true).then(obj => {
let result = this.#getAttributeObject(elt, "hx-vals");
if (result) {
if (result instanceof Promise) {
return result.then(obj => {
for (let key in obj) {
body.append(key, obj[key])
body.set(key, obj[key])
}
});
} else {
// Synchronous path
let obj = this.#parseConfig(hxValsValue);
for (let key in obj) {
body.append(key, obj[key])
for (let key in result) {
body.set(key, result[key])
}
}
}
@ -1786,11 +1827,11 @@ var htmx = (() => {
return s.startsWith('<') && s.endsWith('/>') ? s.slice(1, -2) : s;
}
#findAllExt(eltOrSelector, maybeSelector, global, thisElt) {
#findAllExt(eltOrSelector, maybeSelector, thisAttr, global) {
let selector = maybeSelector ?? eltOrSelector;
let elt = maybeSelector ? this.#normalizeElement(eltOrSelector) : document;
if (selector.startsWith('global ')) {
return this.#findAllExt(elt, selector.slice(7), true, thisElt);
return this.#findAllExt(elt, selector.slice(7), thisAttr, true);
}
let parts = selector ? selector.replace(/<[^>]+\/>/g, m => m.replace(/,/g, '%2C'))
.split(',').map(p => p.replace(/%2C/g, ',')) : [];
@ -1822,7 +1863,11 @@ var htmx = (() => {
} else if (selector === 'host') {
item = (elt.getRootNode()).host
} else if (selector === 'this') {
item = thisElt || elt
if (thisAttr) {
result.push(...this.#findThisElements(elt, thisAttr));
continue;
}
item = elt
} else {
unprocessedParts.push(selector)
}
@ -1838,7 +1883,7 @@ var htmx = (() => {
result.push(...rootNode.querySelectorAll(standardSelector))
}
return result
return [...new Set(result)]
}
#scanForwardQuery(start, match, global) {
@ -1866,8 +1911,8 @@ var htmx = (() => {
}
}
#findExt(eltOrSelector, selector, thisElt) {
return this.#findAllExt(eltOrSelector, selector)[0]
#findExt(eltOrSelector, selector, thisAttr) {
return this.#findAllExt(eltOrSelector, selector, thisAttr)[0]
}
#extractJavascriptContent(string) {

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -130,11 +130,17 @@
<!-- ============================================ -->
<script src="./tests/attributes/hx-boost.js"></script>
<script src="./tests/attributes/hx-config.js"></script>
<script src="./tests/attributes/hx-delete.js"></script>
<script src="./tests/attributes/hx-disable.js"></script>
<script src="./tests/attributes/hx-get.js"></script>
<script src="./tests/attributes/hx-headers.js"></script>
<script src="./tests/attributes/hx-include.js"></script>
<script src="./tests/attributes/hx-indicator.js"></script>
<script src="./tests/attributes/hx-on.js"></script>
<script src="./tests/attributes/hx-patch.js"></script>
<script src="./tests/attributes/hx-post.js"></script>
<script src="./tests/attributes/hx-preserve.js"></script>
<script src="./tests/attributes/hx-put.js"></script>
<script src="./tests/attributes/hx-select.js"></script>
<script src="./tests/attributes/hx-select-oob.js"></script>
<script src="./tests/attributes/hx-status.js"></script>

View File

@ -0,0 +1,20 @@
describe('hx-delete attribute', function() {
beforeEach(() => {
setupTest(this.currentTest)
})
afterEach(() => {
cleanupTest(this.currentTest)
})
it('issues a DELETE request', async function() {
mockResponse('DELETE', '/test', 'Deleted!')
let btn = createProcessedHTML('<button hx-delete="/test">Click Me!</button>')
btn.click()
await forRequest()
fetchMock.calls[0].request.method.should.equal('DELETE');
btn.innerHTML.should.equal('Deleted!')
})
})

View File

@ -0,0 +1,149 @@
describe('hx-disable attribute', function() {
beforeEach(() => {
setupTest(this.currentTest)
})
afterEach(() => {
cleanupTest(this.currentTest)
})
it('single element can be disabled w/ hx-disable', async function() {
mockResponse('GET', '/test', 'Clicked!')
let btn = createProcessedHTML('<button hx-get="/test" hx-disable="this">Click Me!</button>')
btn.hasAttribute('disabled').should.equal(false)
btn.click()
btn.hasAttribute('disabled').should.equal(true)
await forRequest()
btn.hasAttribute('disabled').should.equal(false)
})
it('single element can be disabled w/ closest syntax', async function() {
mockResponse('GET', '/test', 'Clicked!')
let fieldset = createProcessedHTML('<fieldset><button id="b1" hx-get="/test" hx-disable="closest fieldset">Click Me!</button></fieldset>')
let btn = find('#b1')
fieldset.hasAttribute('disabled').should.equal(false)
btn.click()
fieldset.hasAttribute('disabled').should.equal(true)
await forRequest()
fieldset.hasAttribute('disabled').should.equal(false)
})
it('multiple requests with same disabled elt are handled properly', async function() {
mockResponse('GET', '/test', 'Clicked!')
createProcessedHTML('<button id="b1" hx-get="/test" hx-disable="#b3">Click Me!</button>' +
'<button id="b2" hx-get="/test" hx-disable="#b3">Click Me!</button>' +
'<button id="b3">Demo</button>')
let b1 = find('#b1')
let b2 = find('#b2')
let b3 = find('#b3')
b3.hasAttribute('disabled').should.equal(false)
b1.click()
b3.hasAttribute('disabled').should.equal(true)
b2.click()
b3.hasAttribute('disabled').should.equal(true)
// Wait for first request to complete
await forRequest()
b3.hasAttribute('disabled').should.equal(true)
// Wait for second request to complete
await forRequest()
b3.hasAttribute('disabled').should.equal(false)
})
it('multiple elts can be disabled', async function() {
mockResponse('GET', '/test', 'Clicked!')
createProcessedHTML('<button id="b1" hx-get="/test" hx-disable="#b2, #b3">Click Me!</button>' +
'<button id="b2">Click Me!</button>' +
'<button id="b3">Demo</button>')
let b1 = find('#b1')
let b2 = find('#b2')
let b3 = find('#b3')
b2.hasAttribute('disabled').should.equal(false)
b3.hasAttribute('disabled').should.equal(false)
b1.click()
b2.hasAttribute('disabled').should.equal(true)
b3.hasAttribute('disabled').should.equal(true)
await forRequest()
b2.hasAttribute('disabled').should.equal(false)
b3.hasAttribute('disabled').should.equal(false)
})
it('load trigger does not prevent disabled element working', async function() {
mockResponse('GET', '/test', 'Loaded!')
createProcessedHTML('<div id="d1" hx-get="/test" hx-disable="#b1" hx-trigger="load">Load Me!</div><button id="b1">Demo</button>')
let div = find('#d1')
let btn = find('#b1')
div.innerHTML.should.equal('Load Me!')
btn.hasAttribute('disabled').should.equal(true)
await forRequest()
div.innerHTML.should.equal('Loaded!')
btn.hasAttribute('disabled').should.equal(false)
})
it('hx-disable supports multiple extended selectors', async function() {
mockResponse('GET', '/test', 'Clicked!')
let form = createProcessedHTML('<form hx-get="/test" hx-disable="find input[type=\'text\'], find button" hx-swap="none"><input id="i1" type="text" placeholder="Type here..."><button id="b2" type="submit">Send</button></form>')
let i1 = find('#i1')
let b2 = find('#b2')
i1.hasAttribute('disabled').should.equal(false)
b2.hasAttribute('disabled').should.equal(false)
b2.click()
i1.hasAttribute('disabled').should.equal(true)
b2.hasAttribute('disabled').should.equal(true)
await forRequest()
i1.hasAttribute('disabled').should.equal(false)
b2.hasAttribute('disabled').should.equal(false)
})
it('closest/find/next/previous handle nothing to find without exception', async function() {
mockResponse('GET', '/test', 'Clicked!')
createProcessedHTML('<button id="btn1" hx-get="/test" hx-disable="closest input">Click Me!</button>' +
'<button id="btn2" hx-get="/test" hx-disable="find input">Click Me!</button>' +
'<button id="btn3" hx-get="/test" hx-disable="next input">Click Me!</button>' +
'<button id="btn4" hx-get="/test" hx-disable="previous input">Click Me!</button>')
let btn1 = find('#btn1')
let btn2 = find('#btn2')
let btn3 = find('#btn3')
let btn4 = find('#btn4')
btn1.click()
btn1.hasAttribute('disabled').should.equal(false)
await forRequest()
btn2.click()
btn2.hasAttribute('disabled').should.equal(false)
await forRequest()
btn3.click()
btn3.hasAttribute('disabled').should.equal(false)
await forRequest()
btn4.click()
btn4.hasAttribute('disabled').should.equal(false)
await forRequest()
})
})

View File

@ -0,0 +1,107 @@
describe('hx-headers attribute', function() {
beforeEach(() => {
setupTest(this.currentTest)
})
afterEach(() => {
cleanupTest(this.currentTest)
})
it('basic hx-headers works', async function() {
mockResponse('POST', '/vars', 'Clicked!')
let div = createProcessedHTML("<div hx-post='/vars' hx-headers='\"i1\":\"test\"'></div>")
div.click()
await forRequest()
fetchMock.calls[0].request.headers.i1.should.equal('test');
div.innerHTML.should.equal('Clicked!')
})
it('basic hx-headers works with braces', async function() {
mockResponse('POST', '/vars', 'Clicked!')
let div = createProcessedHTML('<div hx-post="/vars" hx-headers=\'{"i1":"test"}\'></div>')
div.click()
await forRequest()
fetchMock.calls[0].request.headers.i1.should.equal('test');
div.innerHTML.should.equal('Clicked!')
})
it('multiple hx-headers works', async function() {
mockResponse('POST', '/vars', 'Clicked!')
let div = createProcessedHTML('<div hx-post="/vars" hx-headers=\'{"v1":"test", "v2":"42"}\'></div>')
div.click()
await forRequest()
fetchMock.calls[0].request.headers.v1.should.equal('test');
fetchMock.calls[0].request.headers.v2.should.equal('42');
div.innerHTML.should.equal('Clicked!')
})
it('hx-headers can be inherited from parents', async function() {
mockResponse('POST', '/vars', 'Clicked!')
createProcessedHTML("<div hx-headers:inherited='\"i1\":\"test\"'><div id='d1' hx-post='/vars'></div></div>")
let div = find('#d1')
div.click()
await forRequest()
fetchMock.calls[0].request.headers.i1.should.equal('test');
div.innerHTML.should.equal('Clicked!')
})
it('child hx-headers can override parent', async function() {
mockResponse('POST', '/vars', 'Clicked!')
createProcessedHTML("<div hx-headers:inherited='\"i1\":\"test\"'><div id='d1' hx-headers='\"i1\":\"best\"' hx-post='/vars'></div></div>")
let div = find('#d1')
div.click()
await forRequest()
fetchMock.calls[0].request.headers.i1.should.equal('best');
div.innerHTML.should.equal('Clicked!')
})
it('basic hx-headers javascript: works', async function() {
mockResponse('POST', '/vars', 'Clicked!')
let div = createProcessedHTML('<div hx-post="/vars" hx-headers="javascript:i1:\'test\'"></div>')
div.click()
await forRequest()
fetchMock.calls[0].request.headers.i1.should.equal('test');
div.innerHTML.should.equal('Clicked!')
})
it('hx-headers works with braces and javascript:', async function() {
mockResponse('POST', '/vars', 'Clicked!')
let div = createProcessedHTML('<div hx-post="/vars" hx-headers="javascript:{i1:\'test\'}"></div>')
div.click()
await forRequest()
fetchMock.calls[0].request.headers.i1.should.equal('test');
div.innerHTML.should.equal('Clicked!')
})
it('multiple hx-headers works with javascript', async function() {
mockResponse('POST', '/vars', 'Clicked!')
let div = createProcessedHTML('<div hx-post="/vars" hx-headers="javascript:v1:\'test\', v2:42"></div>')
div.click()
await forRequest()
fetchMock.calls[0].request.headers.v1.should.equal('test');
fetchMock.calls[0].request.headers.v2.should.equal('42');
div.innerHTML.should.equal('Clicked!')
})
it('hx-headers can be inherited from parents with javascript', async function() {
mockResponse('POST', '/vars', 'Clicked!')
createProcessedHTML('<div hx-headers:inherited="javascript:i1:\'test\'"><div id="d1" hx-post="/vars"></div></div>')
let div = find('#d1')
div.click()
await forRequest()
fetchMock.calls[0].request.headers.i1.should.equal('test');
div.innerHTML.should.equal('Clicked!')
})
it('child hx-headers can override parent with javascript', async function() {
mockResponse('POST', '/vars', 'Clicked!')
createProcessedHTML('<div hx-headers:inherited="javascript:i1:\'test\'"><div id="d1" hx-headers="javascript:i1:\'best\'" hx-post="/vars"></div></div>')
let div = find('#d1')
div.click()
await forRequest()
fetchMock.calls[0].request.headers.i1.should.equal('best');
div.innerHTML.should.equal('Clicked!')
})
})

View File

@ -0,0 +1,20 @@
describe('hx-patch attribute', function() {
beforeEach(() => {
setupTest(this.currentTest)
})
afterEach(() => {
cleanupTest(this.currentTest)
})
it('issues a PATCH request', async function() {
mockResponse('PATCH', '/test', 'Patched!')
let btn = createProcessedHTML('<button hx-patch="/test">Click Me!</button>')
btn.click()
await forRequest()
fetchMock.calls[0].request.method.should.equal('PATCH');
btn.innerHTML.should.equal('Patched!')
})
})

View File

@ -0,0 +1,21 @@
describe('hx-post attribute', function() {
beforeEach(() => {
setupTest(this.currentTest)
})
afterEach(() => {
cleanupTest(this.currentTest)
})
it('issues a POST request with proper headers', async function() {
mockResponse('POST', '/test', 'Posted!')
let btn = createProcessedHTML('<button hx-post="/test">Click Me!</button>')
btn.click()
await forRequest()
fetchMock.calls[0].request.method.should.equal('POST');
should.equal(fetchMock.calls[0].request.headers['X-HTTP-Method-Override'], undefined);
btn.innerHTML.should.equal('Posted!')
})
})

View File

@ -0,0 +1,20 @@
describe('hx-put attribute', function() {
beforeEach(() => {
setupTest(this.currentTest)
})
afterEach(() => {
cleanupTest(this.currentTest)
})
it('issues a PUT request', async function() {
mockResponse('PUT', '/test', 'Put!')
let btn = createProcessedHTML('<button hx-put="/test">Click Me!</button>')
btn.click()
await forRequest()
fetchMock.calls[0].request.method.should.equal('PUT');
btn.innerHTML.should.equal('Put!')
})
})

View File

@ -0,0 +1,202 @@
describe('hx-sync attribute', function() {
beforeEach(() => {
setupTest(this.currentTest)
})
afterEach(() => {
cleanupTest(this.currentTest)
})
it('defaults to queue first strategy', async function() {
createProcessedHTML('<div hx-sync:inherited="this">' +
'<button id="b1" hx-get="/test1">Initial</button>' +
'<button id="b2" hx-get="/test2">Initial</button>' +
'<button id="b3" hx-get="/test3">Initial</button></div>')
let b1 = find('#b1')
let b2 = find('#b2')
let b3 = find('#b3')
b1.click()
b2.click()
b3.click()
await forRequest()
b1.innerHTML.should.equal('Click 1')
b2.innerHTML.should.equal('Initial')
b3.innerHTML.should.equal('Initial')
await forRequest()
b1.innerHTML.should.equal('Click 1')
b2.innerHTML.should.equal('Click 2')
b3.innerHTML.should.equal('Initial')
})
// it('can use replace strategy', async function() {
// let count = 0
// mockResponse('GET', '/test', () => 'Click ' + count++)
// createProcessedHTML('<div id="sync-container">' +
// '<button id="b1" hx-get="/test" hx-sync="#sync-container:replace">Initial</button>' +
// '<button id="b2" hx-get="/test" hx-sync="#sync-container:replace">Initial</button></div>')
//
// let b1 = find('#b1')
// let b2 = find('#b2')
// b1.click()
// b2.click()
// await forRequest()
// b1.innerHTML.should.equal('Initial')
// b2.innerHTML.should.equal('Click 0')
// })
//
// it('can use queue all strategy', async function() {
// let count = 0
// mockResponse('GET', '/test', () => 'Click ' + count++)
// createProcessedHTML('<div id="sync-container">' +
// '<button id="b1" hx-get="/test" hx-sync="#sync-container:queue all">Initial</button>' +
// '<button id="b2" hx-get="/test" hx-sync="#sync-container:queue all">Initial</button>' +
// '<button id="b3" hx-get="/test" hx-sync="#sync-container:queue all">Initial</button></div>')
//
// let b1 = find('#b1')
// let b2 = find('#b2')
// let b3 = find('#b3')
//
// b1.click()
// b2.click()
// b3.click()
//
// await forRequest()
// b1.innerHTML.should.equal('Click 0')
// b2.innerHTML.should.equal('Initial')
// b3.innerHTML.should.equal('Initial')
//
// await forRequest()
// b1.innerHTML.should.equal('Click 0')
// b2.innerHTML.should.equal('Click 1')
// b3.innerHTML.should.equal('Initial')
//
// await forRequest()
// b1.innerHTML.should.equal('Click 0')
// b2.innerHTML.should.equal('Click 1')
// b3.innerHTML.should.equal('Click 2')
// })
//
// it('can use queue last strategy', async function() {
// let count = 0
// mockResponse('GET', '/test', () => 'Click ' + count++)
// createProcessedHTML('<div id="sync-container">' +
// '<button id="b1" hx-get="/test" hx-sync="#sync-container:queue last">Initial</button>' +
// '<button id="b2" hx-get="/test" hx-sync="#sync-container:queue last">Initial</button>' +
// '<button id="b3" hx-get="/test" hx-sync="#sync-container:queue last">Initial</button></div>')
//
// let b1 = find('#b1')
// let b2 = find('#b2')
// let b3 = find('#b3')
//
// b1.click()
// b2.click()
// b3.click()
//
// await forRequest()
// b1.innerHTML.should.equal('Click 0')
// b2.innerHTML.should.equal('Initial')
// b3.innerHTML.should.equal('Initial')
//
// await forRequest()
// b1.innerHTML.should.equal('Click 0')
// b2.innerHTML.should.equal('Initial')
// b3.innerHTML.should.equal('Click 1')
// })
//
// it('can use queue first strategy', async function() {
// let count = 0
// mockResponse('GET', '/test', () => 'Click ' + count++)
// createProcessedHTML('<div id="sync-container">' +
// '<button id="b1" hx-get="/test" hx-sync="#sync-container:queue first">Initial</button>' +
// '<button id="b2" hx-get="/test" hx-sync="#sync-container:queue first">Initial</button>' +
// '<button id="b3" hx-get="/test" hx-sync="#sync-container:queue first">Initial</button></div>')
//
// let b1 = find('#b1')
// let b2 = find('#b2')
// let b3 = find('#b3')
//
// b1.click()
// b2.click()
// b3.click()
//
// await forRequest()
// b1.innerHTML.should.equal('Click 0')
// b2.innerHTML.should.equal('Initial')
// b3.innerHTML.should.equal('Initial')
//
// await forRequest()
// b1.innerHTML.should.equal('Click 0')
// b2.innerHTML.should.equal('Click 1')
// b3.innerHTML.should.equal('Initial')
// })
//
// it('can use abort strategy to end existing abortable request', async function() {
// let count = 0
// mockResponse('GET', '/test', () => 'Click ' + count++)
// createProcessedHTML('<div id="sync-container">' +
// '<button id="b1" hx-sync="#sync-container:abort" hx-get="/test">Initial</button>' +
// '<button id="b2" hx-sync="#sync-container:drop" hx-get="/test">Initial</button></div>')
//
// let b1 = find('#b1')
// let b2 = find('#b2')
// b1.click()
// b2.click()
// await forRequest()
// b1.innerHTML.should.equal('Initial')
// b2.innerHTML.should.equal('Click 0')
// })
//
// it('can use abort strategy to drop abortable request when one is in flight', async function() {
// let count = 0
// mockResponse('GET', '/test', () => 'Click ' + count++)
// createProcessedHTML('<div id="sync-container">' +
// '<button id="b1" hx-sync="#sync-container:abort" hx-get="/test">Initial</button>' +
// '<button id="b2" hx-sync="#sync-container:drop" hx-get="/test">Initial</button></div>')
//
// let b1 = find('#b1')
// let b2 = find('#b2')
// b2.click()
// b1.click()
// await forRequest()
// b1.innerHTML.should.equal('Initial')
// b2.innerHTML.should.equal('Click 0')
// })
//
// it('can abort a request programmatically', async function() {
// let count = 0
// mockResponse('GET', '/test', () => 'Click ' + count++)
// createProcessedHTML('<div><button id="b1" hx-get="/test">Initial</button>' +
// '<button id="b2" hx-get="/test">Initial</button></div>')
//
// let b1 = find('#b1')
// let b2 = find('#b2')
// b1.click()
// b2.click()
//
// htmx.trigger(b1, 'htmx:abort')
//
// await forRequest()
// b1.innerHTML.should.equal('Initial')
// b2.innerHTML.should.equal('Click 0')
// })
//
// it('can use drop strategy', async function() {
// let count = 0
// mockResponse('GET', '/test', () => 'Click ' + count++)
// createProcessedHTML('<div id="sync-container">' +
// '<button id="b1" hx-get="/test" hx-sync="#sync-container:drop">Initial</button>' +
// '<button id="b2" hx-get="/test" hx-sync="#sync-container:drop">Initial</button></div>')
//
// let b1 = find('#b1')
// let b2 = find('#b2')
// b1.click()
// b2.click()
// await forRequest()
// b1.innerHTML.should.equal('Click 0')
// b2.innerHTML.should.equal('Initial')
// })
})

View File

@ -8,6 +8,110 @@ describe('hx-vals attribute', function() {
cleanupTest(this.currentTest)
})
// TODO - convert to a direct test
it('basic hx-vals works with HCON', async function() {
mockResponse('POST', '/vars', 'Clicked!')
let div = createProcessedHTML("<div hx-post='/vars' hx-vals='i1:\"test\"'></div>")
div.click()
await forRequest()
fetchMock.calls[0].request.body.get("i1").should.equal('test');
div.innerHTML.should.equal('Clicked!')
})
it('basic hx-vals works without braces', async function() {
mockResponse('POST', '/vars', 'Clicked!')
let div = createProcessedHTML("<div hx-post='/vars' hx-vals='\"i1\":\"test\"'></div>")
div.click()
await forRequest()
fetchMock.calls[0].request.body.get("i1").should.equal('test');
div.innerHTML.should.equal('Clicked!')
})
it('basic hx-vals works with braces', async function() {
mockResponse('POST', '/vars', 'Clicked!')
let div = createProcessedHTML('<div hx-post="/vars" hx-vals=\'{"i1":"test"}\'></div>')
div.click()
await forRequest()
fetchMock.calls[0].request.body.get("i1").should.equal('test');
div.innerHTML.should.equal('Clicked!')
})
it('multiple hx-vals works', async function() {
mockResponse('POST', '/vars', 'Clicked!')
let div = createProcessedHTML('<div hx-post="/vars" hx-vals=\'{"v1":"test", "v2":42}\'></div>')
div.click()
await forRequest()
fetchMock.calls[0].request.body.get("v1").should.equal('test');
fetchMock.calls[0].request.body.get("v2").should.equal('42');
div.innerHTML.should.equal('Clicked!')
})
it('Dynamic hx-vals using spread operator works', async function() {
mockResponse('POST', '/vars', 'Clicked!')
window.foo = function() {
return { v1: 'test', v2: 42 }
}
let div = createProcessedHTML("<div hx-post='/vars' hx-vals='js:{...foo()}'></div>")
div.click()
await forRequest()
fetchMock.calls[0].request.body.get("v1").should.equal('test');
fetchMock.calls[0].request.body.get("v2").should.equal('42');
div.innerHTML.should.equal('Clicked!')
delete window.foo
})
it('hx-vals can be inherited from parents', async function() {
mockResponse('POST', '/vars', 'Clicked!')
createProcessedHTML("<div hx-vals:inherited='\"i1\":\"test\"'><div id='d1' hx-post='/vars'></div></div>")
let div = find('#d1')
div.click()
await forRequest()
fetchMock.calls[0].request.body.get("i1").should.equal('test');
div.innerHTML.should.equal('Clicked!')
})
it('child hx-vals can override parent', async function() {
mockResponse('POST', '/vars', 'Clicked!')
createProcessedHTML("<div hx-vals:inherited='\"i1\":\"test\"'><div id='d1' hx-post='/vars' hx-vals='\"i1\":\"override\"'></div></div>")
let div = find('#d1')
div.click()
await forRequest()
fetchMock.calls[0].request.body.get("i1").should.equal('override');
div.innerHTML.should.equal('Clicked!')
})
it('hx-vals overrides input values', async function() {
mockResponse('POST', '/vals', 'Submitted')
let form = createProcessedHTML('<form hx-post="/vals" hx-vals=\'{"i1":"test"}\'>' +
'<input name="i1" value="original"/>' +
'<button>Submit</button>' +
'</form>')
form.querySelector('button').click()
await forRequest()
fetchMock.calls[0].request.body.get("i1").should.equal('test');
form.innerHTML.should.equal('Submitted')
})
it('computed values using event in js: prefix', async function() {
mockResponse('POST', '/vars', 'Clicked!')
let div = createProcessedHTML('<div id="myDiv" hx-post="/vars" hx-vals=\'js:{i1: event.target.id}\'></div>')
div.click()
await forRequest()
fetchMock.calls[0].request.body.get("i1").should.equal('myDiv');
div.innerHTML.should.equal('Clicked!')
})
it('hx-vals:append merges JSON objects correctly', async function() {
mockResponse('POST', '/test', 'Success')
const div = createProcessedHTML(
'<div hx-vals:inherited=\'{"a":1}\'>' +
' <button hx-post="/test" hx-vals:append=\'{"b":"hi"}\'>Click</button>' +
'</div>'
);
const button = div.querySelector('button');
button.click();
await forRequest()
fetchMock.calls[0].request.body.get('a').should.equal('1');
fetchMock.calls[0].request.body.get('b').should.equal('hi');
});
})

View File

@ -27,7 +27,7 @@ describe('__atributeValue() unit tests', function() {
);
const button = container.querySelector('button');
const result = htmx.__attributeValue(button, 'hx-vals');
assert.equal(result, '{"a":1},{"b":2}');
assert.equal(result, '"a":1,"b":2');
});
it(':inherited still works normally', function () {

View File

@ -130,4 +130,55 @@ describe('__disableElements / __enableElements unit tests', function() {
assert.equal(input._htmxDisableCount, 1)
})
it('resolves this selector for disable', function () {
let container = createProcessedHTML('<button hx-disable="this" hx-get="/test"></button>');
let elements = htmx.__disableElements(container);
assert.isTrue(container.disabled);
assert.equal(elements.length, 1);
assert.equal(elements[0], container);
})
it('resolves this selector with inherited disable', function () {
let container = createProcessedHTML('<button hx-disable:inherited="this"><span hx-get="/test"></span></button>');
let span = container.querySelector('span');
let elements = htmx.__disableElements(span);
assert.isTrue(container.disabled);
})
it('resolves this selector respecting disable override', function () {
let html = '<button hx-disable="this"><span hx-disable=".other"><input hx-get="/test"></span></button>';
let outer = createProcessedHTML(html);
let input = outer.querySelector('input');
let elements = htmx.__disableElements(input);
assert.isFalse(outer.disabled);
assert.equal(elements.length, 0);
})
it('resolves this selector with append for disable', function () {
let html = '<button hx-disable:inherited="this"><input hx-disable:append="this" hx-get="/test"></button>';
let outer = createProcessedHTML(html);
let inner = outer.querySelector('input');
let elements = htmx.__disableElements(inner);
assert.equal(elements.length, 2);
assert.isTrue(inner.disabled);
assert.isTrue(outer.disabled);
})
it('resolves this selector with comma-separated disable values', function () {
let html = '<button hx-disable="this, .other" hx-get="/test"></button>';
let button = createProcessedHTML(html);
let elements = htmx.__disableElements(button);
assert.isTrue(button.disabled);
})
});

View File

@ -130,4 +130,61 @@ describe('__showIndicators / __hideIndicators unit tests', function() {
assert.equal(div._htmxReqCount, 1)
})
it('resolves this selector for indicators', function () {
let container = createProcessedHTML('<div hx-indicator="this"><button hx-get="/test" hx-indicator="this"></button></div>');
let button = container.querySelector('button');
let indicators = htmx.__showIndicators(button);
assert.isTrue(button.classList.contains('htmx-request'));
assert.equal(indicators.length, 1);
assert.equal(indicators[0], button);
})
it('resolves this selector with inherited indicator', function () {
let outer = createProcessedHTML('<div hx-indicator:inherited="this"><button hx-get="/test"></button></div>');
let button = outer.querySelector('button');
let indicators = htmx.__showIndicators(button);
assert.isTrue(outer.classList.contains('htmx-request'));
assert.equal(indicators.length, 1);
assert.equal(indicators[0], outer);
})
it('resolves this selector respecting indicator override', function () {
let html = '<div hx-indicator="this"><button hx-get="/test" hx-indicator=".other" class="other"></button></div>';
let outer = createProcessedHTML(html);
let button = outer.querySelector('button');
let indicators = htmx.__showIndicators(button);
assert.isFalse(outer.classList.contains('htmx-request'));
assert.isTrue(button.classList.contains('htmx-request'));
})
it('resolves this selector with append for indicators', function () {
let html = '<div hx-indicator:inherited="this"><button hx-get="/test" hx-indicator:append="this"></button></div>';
let outer = createProcessedHTML(html);
let button = outer.querySelector('button');
let indicators = htmx.__showIndicators(button);
assert.isTrue(outer.classList.contains('htmx-request'));
assert.isTrue(button.classList.contains('htmx-request'));
assert.equal(indicators.length, 2);
})
it('resolves this selector with comma-separated indicator values', function () {
let html = '<div class="other"><button hx-get="/test" hx-indicator="this, .other"></button></div>';
let container = createProcessedHTML(html);
let button = container.querySelector('button');
let indicators = htmx.__showIndicators(button);
assert.isTrue(button.classList.contains('htmx-request'));
assert.isTrue(container.classList.contains('htmx-request'));
assert.equal(indicators.length, 2);
})
});

View File

@ -53,7 +53,7 @@ describe('htmx.config.implicitInheritance test', function() {
);
const button = container.querySelector('button');
const result = htmx.__attributeValue(button, 'hx-vals');
assert.equal(result, '{"a":1},{"b":2}');
assert.equal(result, '"a":1,"b":2');
});
});