Merge branch 'dev' into json-parse

This commit is contained in:
chg20 2020-06-17 15:28:33 -07:00 committed by GitHub
commit 025b043918
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
119 changed files with 62676 additions and 490 deletions

View File

@ -2,9 +2,9 @@
*high power tools for HTML*
[![Gitter](https://badges.gitter.im/intercooler-js/Lobby.svg)](https://gitter.im/intercooler-js/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Netlify Status](https://api.netlify.com/api/v1/badges/dba3fc85-d9c9-476a-a35a-e52a632cef78/deploy-status)](https://app.netlify.com/sites/htmx/deploys)
[![Circle CI](https://circleci.com/gh/bigskysoftware/htmx.svg?style=svg)]()
[![Gitter](https://img.shields.io/gitter/room/intercooler-js/Lobby.svg)](https://gitter.im/intercooler-js/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Netlify](https://img.shields.io/netlify/dba3fc85-d9c9-476a-a35a-e52a632cef78)](https://app.netlify.com/sites/htmx/deploys)
[![Circle CI](https://circleci.com/gh/bigskysoftware/htmx.svg?style=shield)](https://app.circleci.com/pipelines/github/bigskysoftware/htmx)
## introduction
@ -53,4 +53,4 @@ keep the core htmx code tidy
*javascript fatigue:<br/>
longing for a hypertext<br/>
already in hand*
already in hand*

7
dist/ext/ajax-header.js vendored Normal file
View File

@ -0,0 +1,7 @@
htmx.defineExtension('ajax-header', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest';
}
}
});

View File

@ -58,14 +58,24 @@
}
}
function maybeProcessClasses(elt) {
if (elt.getAttribute) {
var classList = elt.getAttribute("classes") || elt.getAttribute("data-classes");
if (classList) {
processClassList(elt, classList);
}
}
}
htmx.defineExtension('class-tools', {
onEvent: function (name, evt) {
if (name === "processedNode.htmx") {
var elt = evt.detail.elt;
if (elt.getAttribute) {
var classList = elt.getAttribute("classes") || elt.getAttribute("data-classes");
if (classList) {
processClassList(elt, classList);
maybeProcessClasses(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[classes], [data-classes]");
for (var i = 0; i < children.length; i++) {
maybeProcessClasses(children[i]);
}
}
}

View File

@ -24,9 +24,14 @@ htmx.defineExtension('client-side-templates', {
if (nunjucksTemplate) {
var data = JSON.parse(text);
var templateName = nunjucksTemplate.getAttribute('nunjucks-template');
return nunjucks.render(templateName, data);
}
var template = htmx.find('#' + templateName);
if (template) {
return nunjucks.renderString(template.innerHTML, data);
} else {
return nunjucks.render(templateName, data);
}
}
return text;
}
});
});

View File

@ -1,6 +1,11 @@
htmx.defineExtension('json-enc', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
evt.detail.headers['Content-Type'] = "application/json";
}
},
encodeParameters : function(xhr, parameters, elt) {
xhr.requestHeaders['Content-Type'] = 'application/json';
xhr.overrideMimeType('text/json');
return (JSON.stringify(parameters));
}

11
dist/ext/method-override.js vendored Normal file
View File

@ -0,0 +1,11 @@
htmx.defineExtension('method-override', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
var method = evt.detail.verb;
if (method !== "get" || method !== "post") {
evt.detail.headers['X-HTTP-Method-Override'] = method.toUpperCase();
evt.detail.verb = "post";
}
}
}
});

20
dist/ext/remove-me.js vendored
View File

@ -1,14 +1,24 @@
(function(){
function maybeRemoveMe(elt) {
var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
if (timing) {
setTimeout(function () {
elt.parentElement.removeChild(elt);
}, htmx.parseInterval(timing));
}
}
htmx.defineExtension('remove-me', {
onEvent: function (name, evt) {
if (name === "processedNode.htmx") {
var elt = evt.detail.elt;
if (elt.getAttribute) {
var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
if (timing) {
setTimeout(function () {
elt.parentElement.removeChild(elt);
}, htmx.parseInterval(timing));
maybeRemoveMe(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[remove-me], [data-remove-me");
for (var i = 0; i < children.length; i++) {
maybeRemoveMe(children[i]);
}
}
}
}

185
dist/htmx.js vendored
View File

@ -12,12 +12,15 @@ return (function () {
'use strict';
var VERBS = ['get', 'post', 'put', 'delete', 'patch'];
var VERB_SELECTOR = VERBS.map(function(verb){
return "[hx-" + verb + "], [data-hx-" + verb + "]"
}).join(", ");
//====================================================================
// Utilities
//====================================================================
function parseInterval(str) {
if (str === "null" || str === "false" || str === "") {
if (str == null || str === "null" || str === "false" || str === "") {
return null;
} else if (str.lastIndexOf("ms") === str.length - 2) {
return parseFloat(str.substr(0, str.length - 2));
@ -33,6 +36,11 @@ return (function () {
}
// resolve with both hx and data-hx prefixes
function hasAttribute(elt, qualifiedName) {
return elt.hasAttribute && (elt.hasAttribute(qualifiedName) ||
elt.hasAttribute("data-" + qualifiedName));
}
function getAttributeValue(elt, qualifiedName) {
return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName);
}
@ -374,20 +382,23 @@ return (function () {
function handleAttributes(parentNode, fragment, settleInfo) {
forEach(fragment.querySelectorAll("[id]"), function (newNode) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]")
if (oldNode && oldNode !== parentNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
if (newNode.id && newNode.id.length > 0) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]");
if (oldNode && oldNode !== parentNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
}
}
});
}
function makeLoadTask(child) {
function makeAjaxLoadTask(child) {
return function () {
processNode(child);
processScripts(child);
triggerEvent(child, 'load.htmx', {});
};
}
@ -398,7 +409,7 @@ return (function () {
var child = fragment.firstChild;
parentNode.insertBefore(child, insertBefore);
if (child.nodeType !== Node.TEXT_NODE) {
settleInfo.tasks.push(makeLoadTask(child));
settleInfo.tasks.push(makeAjaxLoadTask(child));
}
}
}
@ -477,6 +488,8 @@ return (function () {
function swap(swapStyle, elt, target, fragment, settleInfo) {
switch (swapStyle) {
case "none":
return;
case "outerHTML":
swapOuterHTML(target, fragment, settleInfo);
return;
@ -563,6 +576,9 @@ return (function () {
if (token.indexOf("delay:") === 0) {
triggerSpec.delay = parseInterval(token.substr(6));
}
if (token.indexOf("throttle:") === 0) {
triggerSpec.throttle = parseInterval(token.substr(9));
}
}
return triggerSpec;
}).filter(function(x){ return x !== null });
@ -655,13 +671,21 @@ return (function () {
if (elementData.delayed) {
clearTimeout(elementData.delayed);
}
var issueRequest = function(){
issueAjaxRequest(elt, verb, path, evt.target);
if (elementData.throttle) {
return;
}
if (triggerSpec.delay) {
elementData.delayed = setTimeout(issueRequest, triggerSpec.delay);
if (triggerSpec.throttle) {
elementData.throttle = setTimeout(function(){
issueAjaxRequest(elt, verb, path, evt.target);
elementData.throttle = null;
}, triggerSpec.throttle);
} else if (triggerSpec.delay) {
elementData.delayed = setTimeout(function(){
issueAjaxRequest(elt, verb, path, evt.target);
}, triggerSpec.delay);
} else {
issueRequest();
issueAjaxRequest(elt, verb, path, evt.target);
}
}
};
@ -825,8 +849,8 @@ return (function () {
function processVerbs(elt, nodeData, triggerSpecs) {
var explicitAction = false;
forEach(VERBS, function (verb) {
var path = getAttributeValue(elt, 'hx-' + verb);
if (path) {
if (hasAttribute(elt,'hx-' + verb)) {
var path = getAttributeValue(elt, 'hx-' + verb);
explicitAction = true;
nodeData.path = path;
nodeData.verb = verb;
@ -850,17 +874,50 @@ return (function () {
return explicitAction;
}
function processScript(elt) {
if (elt.tagName === "SCRIPT" && elt.type === "text/javascript") {
eval(elt.innerText);
function evalScript(script) {
if (script.type === "text/javascript") {
try {
eval(script.innerText);
} catch (e) {
logError(e);
}
}
}
function processNode(elt) {
function processScripts(elt) {
if (matches(elt, "script")) {
evalScript(elt);
}
forEach(findAll(elt, "script"), function (script) {
evalScript(script);
});
}
function isHyperScriptAvailable() {
return typeof _hyperscript !== "undefined";
}
function findElementsToProcess(elt) {
if (elt.querySelectorAll) {
return elt.querySelectorAll(VERB_SELECTOR + ", a, form, [hx-sse], [data-hx-sse], [hx-ws], [data-hx-ws]");
} else {
return [];
}
}
function processNode(elt, ignoreChildren) {
var nodeData = getInternalData(elt);
if (!nodeData.processed) {
nodeData.processed = true;
if(isHyperScriptAvailable()){
_hyperscript.init(elt);
}
if (elt.value) {
nodeData.lastValue = elt.value;
}
var triggerSpecs = getTriggerSpecs(elt);
var explicitAction = processVerbs(elt, nodeData, triggerSpecs);
@ -878,11 +935,9 @@ return (function () {
processWebSocketInfo(elt, nodeData, wsInfo);
}
triggerEvent(elt, "processedNode.htmx");
processScript(elt);
}
if (elt.children) { // IE
forEach(elt.children, function(child) { processNode(child) });
if (!ignoreChildren) {
forEach(findElementsToProcess(elt), function(child) { processNode(child, true) });
}
}
@ -890,16 +945,6 @@ return (function () {
// Event/Log Support
//====================================================================
function sendError(elt, eventName, detail) {
var errorURL = getClosestAttributeValue(elt, "hx-error-url");
if (errorURL) {
var xhr = new XMLHttpRequest();
xhr.open("POST", errorURL);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.send(JSON.stringify({ "elt": elt.id, "event": eventName, "detail" : detail }));
}
}
function makeEvent(eventName, detail) {
var evt;
if (window.CustomEvent && typeof window.CustomEvent === 'function') {
@ -948,7 +993,7 @@ return (function () {
}
if (detail.error) {
logError(detail.error);
sendError(elt, eventName, detail);
triggerEvent(elt, "error.htmx", {errorInfo:detail})
}
var eventResult = elt.dispatchEvent(event);
withExtensions(elt, function (extension) {
@ -1191,23 +1236,23 @@ return (function () {
function getHeaders(elt, target, prompt, eventTarget) {
var headers = {
"X-HX-Request" : "true",
"X-HX-Trigger" : getRawAttribute(elt, "id"),
"X-HX-Trigger-Name" : getRawAttribute(elt, "name"),
"X-HX-Target" : getAttributeValue(target, "id"),
"X-HX-Current-URL" : getDocument().location.href,
"HX-Request" : "true",
"HX-Trigger" : getRawAttribute(elt, "id"),
"HX-Trigger-Name" : getRawAttribute(elt, "name"),
"HX-Target" : getAttributeValue(target, "id"),
"HX-Current-URL" : getDocument().location.href,
}
if (prompt !== undefined) {
headers["X-HX-Prompt"] = prompt;
headers["HX-Prompt"] = prompt;
}
if (eventTarget) {
headers["X-HX-Event-Target"] = getRawAttribute(eventTarget, "id");
headers["HX-Event-Target"] = getRawAttribute(eventTarget, "id");
}
if (getDocument().activeElement) {
headers["X-HX-Active-Element"] = getRawAttribute(getDocument().activeElement, "id");
headers["X-HX-Active-Element-Name"] = getRawAttribute(getDocument().activeElement, "name");
headers["HX-Active-Element"] = getRawAttribute(getDocument().activeElement, "id");
headers["HX-Active-Element-Name"] = getRawAttribute(getDocument().activeElement, "name");
if (getDocument().activeElement.value) {
headers["X-HX-Active-Element-Value"] = getRawAttribute(getDocument().activeElement, "value");
headers["HX-Active-Element-Value"] = getRawAttribute(getDocument().activeElement, "value");
}
}
return headers;
@ -1319,9 +1364,6 @@ return (function () {
if (verb !== 'get') {
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
if (verb !== 'post') {
headers['X-HTTP-Method-Override'] = verb.toUpperCase();
}
}
// behavior of anchors w/ empty href is to use the current URL
@ -1334,9 +1376,15 @@ return (function () {
unfilteredParameters:rawParameters,
headers:headers,
target:target,
verb:verb
verb:verb,
path:path
};
if(!triggerEvent(elt, 'configRequest.htmx', requestConfig)) return endRequestLock();
// copy out in case the object was overwritten
path = requestConfig.path;
verb = requestConfig.verb;
headers = requestConfig.headers;
filteredParameters = requestConfig.parameters;
var splitPath = path.split("#");
var pathNoAnchor = splitPath[0];
@ -1357,7 +1405,7 @@ return (function () {
}
xhr.open('GET', finalPathForGet, true);
} else {
xhr.open('POST', path, true);
xhr.open(verb.toUpperCase(), path, true);
}
xhr.overrideMimeType("text/html");
@ -1374,8 +1422,8 @@ return (function () {
try {
if (!triggerEvent(elt, 'beforeOnLoad.htmx', eventDetail)) return;
handleTrigger(elt, this.getResponseHeader("X-HX-Trigger"));
var pushedUrl = this.getResponseHeader("X-HX-Push");
handleTrigger(elt, this.getResponseHeader("HX-Trigger"));
var pushedUrl = this.getResponseHeader("HX-Push");
var shouldSaveHistory = shouldPush(elt) || pushedUrl;
@ -1385,7 +1433,7 @@ return (function () {
}
// don't process 'No Content' response
if (this.status !== 204) {
if (!triggerEvent(elt, 'beforeSwap.htmx', eventDetail)) return;
if (!triggerEvent(target, 'beforeSwap.htmx', eventDetail)) return;
var resp = this.response;
withExtensions(elt, function(extension){
@ -1402,15 +1450,32 @@ return (function () {
target.classList.add("htmx-swapping");
var doSwap = function () {
try {
var activeElt = document.activeElement;
var selectionInfo = {
elt: activeElt,
start: activeElt.selectionStart,
end: activeElt.selectionEnd,
};
var settleInfo = makeSettleInfo(target);
selectAndSwap(swapSpec.swapStyle, target, elt, resp, settleInfo);
if (!bodyContains(selectionInfo.elt) && selectionInfo.elt.id) {
var newActiveElt = document.getElementById(selectionInfo.elt.id);
if (selectionInfo.start && newActiveElt.setSelectionRange) {
newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end);
}
newActiveElt.focus();
}
target.classList.remove("htmx-swapping");
forEach(settleInfo.elts, function (elt) {
if (elt.classList) {
elt.classList.add("htmx-settling");
}
triggerEvent(elt, 'afterSwap.htmx', eventDetail);
});
triggerEvent(elt, 'afterSwap.htmx', eventDetail);
if (anchor) {
location.hash = anchor;
}
@ -1422,12 +1487,14 @@ return (function () {
if (elt.classList) {
elt.classList.remove("htmx-settling");
}
triggerEvent(elt, 'afterSettle.htmx', eventDetail);
});
// push URL and save new page
if (shouldSaveHistory) {
pushUrlIntoHistory(pushedUrl || path);
var pathToPush = pushedUrl || finalPathForGet || path;
pushUrlIntoHistory(pathToPush);
triggerEvent(getDocument().body, 'pushedIntoHistory.htmx', {path:pathToPush});
}
triggerEvent(elt, 'afterSettle.htmx', eventDetail);
}
if (swapSpec.settleDelay > 0) {
@ -1558,7 +1625,7 @@ return (function () {
ready(function () {
mergeMetaConfig();
var body = getDocument().body;
processNode(body);
processNode(body, true);
triggerEvent(body, 'load.htmx', {});
window.onpopstate = function () {
restoreHistory();
@ -1603,4 +1670,4 @@ return (function () {
}
}
)()
}));
}));

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.

7
src/ext/ajax-header.js Normal file
View File

@ -0,0 +1,7 @@
htmx.defineExtension('ajax-header', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest';
}
}
});

View File

@ -58,14 +58,24 @@
}
}
function maybeProcessClasses(elt) {
if (elt.getAttribute) {
var classList = elt.getAttribute("classes") || elt.getAttribute("data-classes");
if (classList) {
processClassList(elt, classList);
}
}
}
htmx.defineExtension('class-tools', {
onEvent: function (name, evt) {
if (name === "processedNode.htmx") {
var elt = evt.detail.elt;
if (elt.getAttribute) {
var classList = elt.getAttribute("classes") || elt.getAttribute("data-classes");
if (classList) {
processClassList(elt, classList);
maybeProcessClasses(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[classes], [data-classes]");
for (var i = 0; i < children.length; i++) {
maybeProcessClasses(children[i]);
}
}
}

View File

@ -24,9 +24,14 @@ htmx.defineExtension('client-side-templates', {
if (nunjucksTemplate) {
var data = JSON.parse(text);
var templateName = nunjucksTemplate.getAttribute('nunjucks-template');
return nunjucks.render(templateName, data);
}
var template = htmx.find('#' + templateName);
if (template) {
return nunjucks.renderString(template.innerHTML, data);
} else {
return nunjucks.render(templateName, data);
}
}
return text;
}
});
});

View File

@ -1,7 +1,12 @@
htmx.defineExtension('json-enc', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
evt.detail.headers['Content-Type'] = "application/json";
}
},
encodeParameters : function(xhr, parameters, elt) {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.overrideMimeType('text/json');
return (JSON.stringify(parameters));
}
});
});

View File

@ -0,0 +1,11 @@
htmx.defineExtension('method-override', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
var method = evt.detail.verb;
if (method !== "get" || method !== "post") {
evt.detail.headers['X-HTTP-Method-Override'] = method.toUpperCase();
evt.detail.verb = "post";
}
}
}
});

View File

@ -1,10 +0,0 @@
htmx.defineExtension('rails-method', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
var methodOverride = evt.detail.headers['X-HTTP-Method-Override'];
if (methodOverride) {
evt.detail.parameters['_method'] = methodOverride;
}
}
}
});

View File

@ -1,14 +1,24 @@
(function(){
function maybeRemoveMe(elt) {
var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
if (timing) {
setTimeout(function () {
elt.parentElement.removeChild(elt);
}, htmx.parseInterval(timing));
}
}
htmx.defineExtension('remove-me', {
onEvent: function (name, evt) {
if (name === "processedNode.htmx") {
var elt = evt.detail.elt;
if (elt.getAttribute) {
var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
if (timing) {
setTimeout(function () {
elt.parentElement.removeChild(elt);
}, htmx.parseInterval(timing));
maybeRemoveMe(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[remove-me], [data-remove-me");
for (var i = 0; i < children.length; i++) {
maybeRemoveMe(children[i]);
}
}
}
}

View File

@ -12,12 +12,15 @@ return (function () {
'use strict';
var VERBS = ['get', 'post', 'put', 'delete', 'patch'];
var VERB_SELECTOR = VERBS.map(function(verb){
return "[hx-" + verb + "], [data-hx-" + verb + "]"
}).join(", ");
//====================================================================
// Utilities
//====================================================================
function parseInterval(str) {
if (str === "null" || str === "false" || str === "") {
if (str == null || str === "null" || str === "false" || str === "") {
return null;
} else if (str.lastIndexOf("ms") === str.length - 2) {
return parseFloat(str.substr(0, str.length - 2));
@ -389,20 +392,23 @@ return (function () {
function handleAttributes(parentNode, fragment, settleInfo) {
forEach(fragment.querySelectorAll("[id]"), function (newNode) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]")
if (oldNode && oldNode !== parentNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
if (newNode.id && newNode.id.length > 0) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]");
if (oldNode && oldNode !== parentNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
}
}
});
}
function makeLoadTask(child) {
function makeAjaxLoadTask(child) {
return function () {
processNode(child);
processScripts(child);
triggerEvent(child, 'load.htmx', {});
};
}
@ -413,7 +419,7 @@ return (function () {
var child = fragment.firstChild;
parentNode.insertBefore(child, insertBefore);
if (child.nodeType !== Node.TEXT_NODE) {
settleInfo.tasks.push(makeLoadTask(child));
settleInfo.tasks.push(makeAjaxLoadTask(child));
}
}
}
@ -492,6 +498,8 @@ return (function () {
function swap(swapStyle, elt, target, fragment, settleInfo) {
switch (swapStyle) {
case "none":
return;
case "outerHTML":
swapOuterHTML(target, fragment, settleInfo);
return;
@ -578,6 +586,9 @@ return (function () {
if (token.indexOf("delay:") === 0) {
triggerSpec.delay = parseInterval(token.substr(6));
}
if (token.indexOf("throttle:") === 0) {
triggerSpec.throttle = parseInterval(token.substr(9));
}
}
return triggerSpec;
}).filter(function(x){ return x !== null });
@ -670,13 +681,21 @@ return (function () {
if (elementData.delayed) {
clearTimeout(elementData.delayed);
}
var issueRequest = function(){
issueAjaxRequest(elt, verb, path, evt.target);
if (elementData.throttle) {
return;
}
if (triggerSpec.delay) {
elementData.delayed = setTimeout(issueRequest, triggerSpec.delay);
if (triggerSpec.throttle) {
elementData.throttle = setTimeout(function(){
issueAjaxRequest(elt, verb, path, evt.target);
elementData.throttle = null;
}, triggerSpec.throttle);
} else if (triggerSpec.delay) {
elementData.delayed = setTimeout(function(){
issueAjaxRequest(elt, verb, path, evt.target);
}, triggerSpec.delay);
} else {
issueRequest();
issueAjaxRequest(elt, verb, path, evt.target);
}
}
};
@ -865,21 +884,50 @@ return (function () {
return explicitAction;
}
function processScript(elt) {
if (elt.tagName === "SCRIPT" && elt.type === "text/javascript") {
function evalScript(script) {
if (script.type === "text/javascript") {
try {
eval(elt.innerText);
} catch(e) {
eval(script.innerText);
} catch (e) {
logError(e);
}
}
}
function processNode(elt) {
function processScripts(elt) {
if (matches(elt, "script")) {
evalScript(elt);
}
forEach(findAll(elt, "script"), function (script) {
evalScript(script);
});
}
function isHyperScriptAvailable() {
return typeof _hyperscript !== "undefined";
}
function findElementsToProcess(elt) {
if (elt.querySelectorAll) {
return elt.querySelectorAll(VERB_SELECTOR + ", a, form, [hx-sse], [data-hx-sse], [hx-ws], [data-hx-ws]");
} else {
return [];
}
}
function processNode(elt, ignoreChildren) {
var nodeData = getInternalData(elt);
if (!nodeData.processed) {
nodeData.processed = true;
if(isHyperScriptAvailable()){
_hyperscript.init(elt);
}
if (elt.value) {
nodeData.lastValue = elt.value;
}
var triggerSpecs = getTriggerSpecs(elt);
var explicitAction = processVerbs(elt, nodeData, triggerSpecs);
@ -897,11 +945,9 @@ return (function () {
processWebSocketInfo(elt, nodeData, wsInfo);
}
triggerEvent(elt, "processedNode.htmx");
processScript(elt);
}
if (elt.children) { // IE
forEach(elt.children, function(child) { processNode(child) });
if (!ignoreChildren) {
forEach(findElementsToProcess(elt), function(child) { processNode(child, true) });
}
}
@ -909,16 +955,6 @@ return (function () {
// Event/Log Support
//====================================================================
function sendError(elt, eventName, detail) {
var errorURL = getClosestAttributeValue(elt, "hx-error-url");
if (errorURL) {
var xhr = new XMLHttpRequest();
xhr.open("POST", errorURL);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.send(JSON.stringify({ "elt": elt.id, "event": eventName, "detail" : detail }));
}
}
function makeEvent(eventName, detail) {
var evt;
if (window.CustomEvent && typeof window.CustomEvent === 'function') {
@ -967,7 +1003,7 @@ return (function () {
}
if (detail.error) {
logError(detail.error);
sendError(elt, eventName, detail);
triggerEvent(elt, "error.htmx", {errorInfo:detail})
}
var eventResult = elt.dispatchEvent(event);
withExtensions(elt, function (extension) {
@ -1338,9 +1374,6 @@ return (function () {
if (verb !== 'get') {
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
if (verb !== 'post') {
headers['X-HTTP-Method-Override'] = verb.toUpperCase();
}
}
// behavior of anchors w/ empty href is to use the current URL
@ -1357,7 +1390,11 @@ return (function () {
path:path
};
if(!triggerEvent(elt, 'configRequest.htmx', requestConfig)) return endRequestLock();
// copy out in case the object was overwritten
path = requestConfig.path;
verb = requestConfig.verb;
headers = requestConfig.headers;
filteredParameters = requestConfig.parameters;
var splitPath = path.split("#");
var pathNoAnchor = splitPath[0];
@ -1378,7 +1415,7 @@ return (function () {
}
xhr.open('GET', finalPathForGet, true);
} else {
xhr.open('POST', path, true);
xhr.open(verb.toUpperCase(), path, true);
}
xhr.overrideMimeType("text/html");
@ -1406,7 +1443,7 @@ return (function () {
}
// don't process 'No Content' response
if (this.status !== 204) {
if (!triggerEvent(elt, 'beforeSwap.htmx', eventDetail)) return;
if (!triggerEvent(target, 'beforeSwap.htmx', eventDetail)) return;
var resp = this.response;
withExtensions(elt, function(extension){
@ -1423,15 +1460,32 @@ return (function () {
target.classList.add("htmx-swapping");
var doSwap = function () {
try {
var activeElt = document.activeElement;
var selectionInfo = {
elt: activeElt,
start: activeElt.selectionStart,
end: activeElt.selectionEnd,
};
var settleInfo = makeSettleInfo(target);
selectAndSwap(swapSpec.swapStyle, target, elt, resp, settleInfo);
if (!bodyContains(selectionInfo.elt) && selectionInfo.elt.id) {
var newActiveElt = document.getElementById(selectionInfo.elt.id);
if (selectionInfo.start && newActiveElt.setSelectionRange) {
newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end);
}
newActiveElt.focus();
}
target.classList.remove("htmx-swapping");
forEach(settleInfo.elts, function (elt) {
if (elt.classList) {
elt.classList.add("htmx-settling");
}
triggerEvent(elt, 'afterSwap.htmx', eventDetail);
});
triggerEvent(elt, 'afterSwap.htmx', eventDetail);
if (anchor) {
location.hash = anchor;
}
@ -1443,12 +1497,14 @@ return (function () {
if (elt.classList) {
elt.classList.remove("htmx-settling");
}
triggerEvent(elt, 'afterSettle.htmx', eventDetail);
});
// push URL and save new page
if (shouldSaveHistory) {
pushUrlIntoHistory(pushedUrl || path);
var pathToPush = pushedUrl || finalPathForGet || path;
pushUrlIntoHistory(pathToPush);
triggerEvent(getDocument().body, 'pushedIntoHistory.htmx', {path:pathToPush});
}
triggerEvent(elt, 'afterSettle.htmx', eventDetail);
}
if (swapSpec.settleDelay > 0) {
@ -1579,7 +1635,7 @@ return (function () {
ready(function () {
mergeMetaConfig();
var body = getDocument().body;
processNode(body);
processNode(body, true);
triggerEvent(body, 'load.htmx', {});
window.onpopstate = function () {
restoreHistory();

View File

@ -8,10 +8,9 @@ describe("hx-delete attribute", function(){
clearWorkArea();
});
it('issues a DELETE request with proper headers', function()
it('issues a DELETE request', function()
{
this.server.respondWith("DELETE", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('DELETE');
xhr.respond(200, {}, "Deleted!");
});
@ -21,10 +20,9 @@ describe("hx-delete attribute", function(){
btn.innerHTML.should.equal("Deleted!");
});
it('issues a DELETE request with proper headers w/ data-* prefix', function()
it('issues a DELETE request w/ data-* prefix', function()
{
this.server.respondWith("DELETE", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('DELETE');
xhr.respond(200, {}, "Deleted!");
});

View File

@ -1,32 +0,0 @@
describe("hx-error-url attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('Submits a POST with error content on bad request', function()
{
this.server.respondWith("POST", "/error", function(xhr){
should.equal(JSON.parse(xhr.requestBody).detail.xhr.status, 404);
});
var btn = make('<button hx-error-url="/error" hx-get="/bad">Click Me!</button>')
btn.click();
this.server.respond();
this.server.respond();
});
it('Submits a POST with error content on bad request w/ data-* prefix', function()
{
this.server.respondWith("POST", "/error", function(xhr){
should.equal(JSON.parse(xhr.requestBody).detail.xhr.status, 404);
});
var btn = make('<button data-hx-error-url="/error" hx-get="/bad">Click Me!</button>')
btn.click();
this.server.respond();
this.server.respond();
});
})

View File

@ -8,10 +8,9 @@ describe("hx-patch attribute", function(){
clearWorkArea();
});
it('issues a PATCH request with proper headers', function()
it('issues a PATCH request', function()
{
this.server.respondWith("PATCH", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('PATCH');
xhr.respond(200, {}, "Patched!");
});
@ -21,10 +20,9 @@ describe("hx-patch attribute", function(){
btn.innerHTML.should.equal("Patched!");
});
it('issues a PATCH request with proper headers w/ data-* prefix', function()
it('issues a PATCH request w/ data-* prefix', function()
{
this.server.respondWith("PATCH", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('PATCH');
xhr.respond(200, {}, "Patched!");
});

View File

@ -1,15 +1,16 @@
describe("hx-push-url attribute", function() {
var KUTTY_HISTORY_CACHE = "htmx-history-cache";
var HTMX_HISTORY_CACHE_NAME = "htmx-history-cache";
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
localStorage.removeItem(KUTTY_HISTORY_CACHE);
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
afterEach(function () {
this.server.restore();
clearWorkArea();
localStorage.removeItem(KUTTY_HISTORY_CACHE);
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
it("navigation should push an element into the cache ", function () {
@ -19,7 +20,7 @@ describe("hx-push-url attribute", function() {
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(1);
});
@ -38,7 +39,7 @@ describe("hx-push-url attribute", function() {
this.server.respond();
workArea.textContent.should.equal("test2")
var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(2);
htmx._('restoreHistory')("/test1")
@ -58,7 +59,7 @@ describe("hx-push-url attribute", function() {
byId("d1").click();
this.server.respond();
}
var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(10); // should only be 10 elements
});
@ -77,49 +78,15 @@ describe("hx-push-url attribute", function() {
this.server.respond();
workArea.textContent.should.equal("test2")
var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(2);
localStorage.removeItem(KUTTY_HISTORY_CACHE); // clear cache
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME); // clear cache
htmx._('restoreHistory')("/test1")
this.server.respond();
getWorkArea().textContent.should.equal("test1")
});
function stringRepeat(str, num) {
num = Number(num);
var result = '';
while (true) {
if (num & 1) { // (1)
result += str;
}
num >>>= 1; // (2)
if (num <= 0) break;
str += str;
}
return result;
}
it("implementation details should be fast", function(){
// create an entry with a large content string (256k) and see how fast we can write and read it
// to local storage as a single entry
var entry = {url: stringRepeat("x", 32), content:stringRepeat("x", 256*1024)}
var array = [];
for (var i = 0; i < 10; i++) {
array.push(entry);
}
var start = performance.now();
var string = JSON.stringify(array);
localStorage.setItem(KUTTY_HISTORY_CACHE, string);
var reReadString = localStorage.getItem(KUTTY_HISTORY_CACHE);
var finalJson = JSON.parse(reReadString);
var end = performance.now();
var timeInMs = end - start;
chai.assert(timeInMs < 300, "Should take less than 300ms on most platforms");
})
it("navigation should push an element into the cache w/ data-* prefix", function () {
this.server.respondWith("GET", "/test", "second");
getWorkArea().innerHTML.should.be.equal("");
@ -127,21 +94,59 @@ describe("hx-push-url attribute", function() {
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(1);
});
it("deals with malformed JSON in history cache when getting", function () {
localStorage.setItem(KUTTY_HISTORY_CACHE, "Invalid JSON");
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, "Invalid JSON");
var history = htmx._('getCachedHistory')('url');
should.equal(history, null);
});
it("deals with malformed JSON in history cache when saving", function () {
localStorage.setItem(KUTTY_HISTORY_CACHE, "Invalid JSON");
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, "Invalid JSON");
htmx._('saveToHistoryCache')('url', 'content', 'title', 'scroll');
var cache = JSON.parse(localStorage.getItem(KUTTY_HISTORY_CACHE));
cache.length.should.equal(1);
});
it("afterSettle.htmx is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("afterSettle.htmx", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("afterSettle.htmx", handler);
}
});
it("should include parameters on a get", function () {
var path = "";
var handler = htmx.on("pushedIntoHistory.htmx", function (evt) {
path = evt.detail.path;
});
try {
this.server.respondWith("GET", /test.*/, function (xhr) {
xhr.respond(200, {}, "second")
});
var form = make('<form hx-trigger="click" hx-push-url="true" hx-get="/test"><input type="hidden" name="foo" value="bar"/>first</form>');
form.click();
this.server.respond();
form.textContent.should.equal("second")
path.should.equal("/test?foo=bar")
} finally {
htmx.off("pushedIntoHistory.htmx", handler);
}
});
});

View File

@ -8,10 +8,9 @@ describe("hx-put attribute", function(){
clearWorkArea();
});
it('issues a PUT request with proper headers', function()
it('issues a PUT request', function()
{
this.server.respondWith("PUT", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('PUT');
xhr.respond(200, {}, "Putted!");
});
@ -21,10 +20,9 @@ describe("hx-put attribute", function(){
btn.innerHTML.should.equal("Putted!");
});
it('issues a PUT request with proper headers', function()
it('issues a PUT request w/ data-* prefix', function()
{
this.server.respondWith("PUT", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('PUT');
xhr.respond(200, {}, "Putted!");
});

View File

@ -246,7 +246,6 @@ describe("hx-swap attribute", function(){
}, 30);
});
it('swap outerHTML properly w/ data-* prefix', function()
{
this.server.respondWith("GET", "/test", '<a id="a1" data-hx-get="/test2">Click Me</a>');
@ -262,5 +261,14 @@ describe("hx-swap attribute", function(){
byId("a1").innerHTML.should.equal('Clicked!');
});
it('swap none works properly', function()
{
this.server.respondWith("GET", "/test", 'Ooops, swapped');
var div = make('<div hx-swap="none" hx-get="/test">Foo</div>')
div.click();
this.server.respond();
div.innerHTML.should.equal('Foo');
});
})

View File

@ -30,17 +30,17 @@ describe("hx-trigger attribute", function(){
var div = make('<div id="d1"></div>');
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
div.innerHTML.should.equal("");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
div.innerHTML.should.equal("");
input.value = "bar";
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 2");
div.innerHTML.should.equal("Requests: 1");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 2");
div.innerHTML.should.equal("Requests: 1");
});
it('once modifier works', function()
@ -113,32 +113,35 @@ describe("hx-trigger attribute", function(){
div.innerHTML.should.equal("Requests: 2");
});
var specExamples = {
"": [{trigger: 'click'}],
"every 1s": [{trigger: 'every', pollInterval: 1000}],
"click": [{trigger: 'click'}],
"customEvent": [{trigger: 'customEvent'}],
"event changed": [{trigger: 'event', changed: true}],
"event once": [{trigger: 'event', once: true}],
"event delay:1s": [{trigger: 'event', delay: 1000}],
"event changed once delay:1s": [{trigger: 'event', changed: true, once: true, delay: 1000}],
"event1,event2": [{trigger: 'event1'}, {trigger: 'event2'}],
"event1, event2": [{trigger: 'event1'}, {trigger: 'event2'}],
"event1 once, event2 changed": [{trigger: 'event1', once: true}, {trigger: 'event2', changed: true}],
"event1,": [{trigger: 'event1'}],
",event1": [{trigger: 'event1'}],
" ": [{trigger: 'click'}],
",": [{trigger: 'click'}]
}
for (var specString in specExamples) {
it("parses " + specString, function()
{
var div = make("<div hx-trigger=" + specString + "></div>");
it("parses spec strings", function()
{
var specExamples = {
"": [{trigger: 'click'}],
"every 1s": [{trigger: 'every', pollInterval: 1000}],
"click": [{trigger: 'click'}],
"customEvent": [{trigger: 'customEvent'}],
"event changed": [{trigger: 'event', changed: true}],
"event once": [{trigger: 'event', once: true}],
"event delay:1s": [{trigger: 'event', delay: 1000}],
"event throttle:1s": [{trigger: 'event', throttle: 1000}],
"event changed once delay:1s": [{trigger: 'event', changed: true, once: true, delay: 1000}],
"event1,event2": [{trigger: 'event1'}, {trigger: 'event2'}],
"event1, event2": [{trigger: 'event1'}, {trigger: 'event2'}],
"event1 once, event2 changed": [{trigger: 'event1', once: true}, {trigger: 'event2', changed: true}],
"event1,": [{trigger: 'event1'}],
",event1": [{trigger: 'event1'}],
" ": [{trigger: 'click'}],
",": [{trigger: 'click'}]
}
for (var specString in specExamples) {
var div = make("<div hx-trigger='" + specString + "'></div>");
var spec = htmx._('getTriggerSpecs')(div);
spec.should.deep.equal(specExamples[specString]);
});
}
spec.should.deep.equal(specExamples[specString], "Found : " + JSON.stringify(spec) + ", expected : " + JSON.stringify(specExamples[specString]) + " for spec: " + specString);
}
});
it('sets default trigger for forms', function()
{

View File

@ -82,5 +82,41 @@ describe("Core htmx Events", function() {
}
});
it("afterSwap.htmx is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("afterSwap.htmx", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("afterSwap.htmx", handler);
}
});
it("afterSettle.htmx is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("afterSettle.htmx", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("afterSettle.htmx", handler);
}
});
});

70
test/core/perf.js Normal file
View File

@ -0,0 +1,70 @@
describe("Core htmx perf Tests", function() {
var HTMX_HISTORY_CACHE_NAME = "htmx-history-cache";
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
afterEach(function () {
this.server.restore();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
function stringRepeat(str, num) {
num = Number(num);
var result = '';
while (true) {
if (num & 1) { // (1)
result += str;
}
num >>>= 1; // (2)
if (num <= 0) break;
str += str;
}
return result;
}
it("DOM processing should be fast", function(){
this.server.respondWith("GET", "/test", "Clicked!");
// create an entry with a large content string (256k) and see how fast we can write and read it
// to local storage as a single entry
var str = stringRepeat("<div>", 30) + stringRepeat("<div><div><span><button hx-get='/test'> Test Get Button </button></span></div></div>\n", 1000) + stringRepeat("</div>", 30);
var start = performance.now();
var stuff = make(str);
var end = performance.now();
var timeInMs = end - start;
// make sure the DOM actually processed
var firstBtn = stuff.querySelector("button");
firstBtn.click();
this.server.respond();
firstBtn.innerHTML.should.equal("Clicked!");
chai.assert(timeInMs < 100, "Should take less than 100ms on most platforms, took: " + timeInMs + "ms");
})
it("history implementation should be fast", function(){
// create an entry with a large content string (256k) and see how fast we can write and read it
// to local storage as a single entry
var entry = {url: stringRepeat("x", 32), content:stringRepeat("x", 256*1024)}
var array = [];
for (var i = 0; i < 10; i++) {
array.push(entry);
}
var start = performance.now();
var string = JSON.stringify(array);
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, string);
var reReadString = localStorage.getItem(HTMX_HISTORY_CACHE_NAME);
var finalJson = JSON.parse(reReadString);
var end = performance.now();
var timeInMs = end - start;
chai.assert(timeInMs < 300, "Should take less than 300ms on most platforms");
})
})

View File

@ -49,4 +49,20 @@ describe("Core htmx Regression Tests", function(){
form.innerHTML.should.equal("variable=")
});
it ('name=id doesnt cause an error', function(){
this.server.respondWith("GET", "/test", "Foo<form><input name=\"id\"/></form>")
var div = make('<div hx-get="/test">Get It</div>')
div.click();
this.server.respond();
div.innerText.should.contain("Foo")
});
it ('empty id doesnt cause an error', function(){
this.server.respondWith("GET", "/test", "Foo\n<div id=''></div>")
var div = make('<div hx-get="/test">Get It</div>')
div.click();
this.server.respond();
div.innerText.should.contain("Foo")
});
})

21
test/ext/ajax-header.js Normal file
View File

@ -0,0 +1,21 @@
describe("ajax-header extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('Sends the X-Requested-With header', function () {
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, xhr.requestHeaders['X-Requested-With'])
});
var btn = make('<button hx-get="/test" hx-ext="ajax-header">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("XMLHttpRequest");
});
});

37
test/ext/hyperscript.js Normal file
View File

@ -0,0 +1,37 @@
describe("hyperscript integration", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('can trigger with a custom event', function () {
this.server.respondWith("GET", "/test", "Custom Event Sent!");
var btn = make('<button _="on click send customEvent" hx-trigger="customEvent" hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Custom Event Sent!");
});
it('can handle htmx driven events', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button _="on afterSettle.htmx add .afterSettle" hx-get="/test">Click Me!</button>')
btn.classList.contains("afterSettle").should.equal(false);
btn.click();
this.server.respond();
btn.classList.contains("afterSettle").should.equal(true);
});
it('can handle htmx error events', function () {
this.server.respondWith("GET", "/test", [404, {}, "Bad request"]);
var div = make('<div id="d1"></div>')
var btn = make('<button _="on error.htmx(errorInfo) put errorInfo.error into #d1.innerHTML" hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
div.innerHTML.should.equal("Response Status Error Code 404 from /test");
});
});

View File

@ -0,0 +1,53 @@
describe("method-override extension", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('issues a DELETE request with proper headers', function()
{
this.server.respondWith("DELETE", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('DELETE');
xhr.method.should.equal("POST")
xhr.respond(200, {}, "Deleted!");
});
var btn = make('<button hx-ext="method-override" hx-delete="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Deleted!");
});
it('issues a PATCH request with proper headers', function()
{
this.server.respondWith("PATCH", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('PATCH');
xhr.method.should.equal("POST")
xhr.respond(200, {}, "Patched!");
});
var btn = make('<button hx-ext="method-override" hx-patch="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Patched!");
});
it('issues a PUT request with proper headers', function()
{
this.server.respondWith("PUT", "/test", function(xhr){
xhr.requestHeaders['X-HTTP-Method-Override'].should.equal('PUT');
xhr.method.should.equal("POST")
xhr.respond(200, {}, "Putted!");
});
var btn = make('<button hx-ext="method-override" hx-put="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Putted!");
});
})

View File

@ -1,61 +0,0 @@
describe("rails-method extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('Does not affect a GET request', function () {
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, xhr.url)
});
var btn = make('<button hx-get="/test" hx-ext="rails-method">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("/test");
});
it('Does not affect a POST request', function () {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, getParameters(xhr)['_method']);
});
var btn = make('<button hx-post="/test" hx-ext="rails-method">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("");
});
it('Adds proper _method param to PUT request', function () {
this.server.respondWith("PUT", "/test", function (xhr) {
xhr.respond(200, {}, getParameters(xhr)['_method']);
});
var btn = make('<button hx-put="/test" hx-ext="rails-method">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("PUT");
});
it('Adds proper _method param to PATCH request', function () {
this.server.respondWith("PATCH", "/test", function (xhr) {
xhr.respond(200, {}, getParameters(xhr)['_method']);
});
var btn = make('<button hx-patch="/test" hx-ext="rails-method">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("PATCH");
});
it('Adds proper _method param to DELETE request', function () {
this.server.respondWith("DELETE", "/test", function (xhr) {
xhr.respond(200, {}, getParameters(xhr)['_method']);
});
var btn = make('<button hx-delete="/test" hx-ext="rails-method">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("DELETE");
});
});

View File

@ -18,35 +18,23 @@ describe("remove-me extension", function(){
}, 40);
});
it('removes classes properly', function(done)
{
var div = make('<div class="foo bar" hx-ext="class-tools" classes="remove bar">Click Me!</div>')
should.equal(div.classList.contains("foo"), true);
should.equal(div.classList.contains("bar"), true);
setTimeout(function(){
should.equal(div.classList.contains("foo"), true);
should.equal(div.classList.contains("bar"), false);
done();
}, 100);
});
it('adds classes properly w/ data-* prefix', function(done)
it('removes properly w/ data-* prefix', function(done)
{
var div = make('<div hx-ext="class-tools" data-classes="add c1">Click Me!</div>')
var div = make('<div hx-ext="remove-me" data-remove-me="20ms">Click Me!</div>')
should.equal(div.classList.length, 0);
setTimeout(function(){
should.equal(div.classList.contains("c1"), true);
should.equal(div.parentElement, null);
done();
}, 100);
});
it('extension can be on parent', function(done)
{
var div = make('<div hx-ext="class-tools"><div id="d1" classes="add c1">Click Me!</div></div>')
var div = make('<div hx-ext="remove-me"><div id="d1" remove-me="20ms">Click Me!</div></div>')
should.equal(div.classList.length, 0);
setTimeout(function(){
should.equal(div.classList.contains("c1"), false);
should.equal(byId("d1").classList.contains("c1"), true);
should.equal(byId("d1"), null);
done();
}, 100);
});

View File

@ -60,11 +60,11 @@
<script src="core/parameters.js"></script>
<script src="core/headers.js"></script>
<script src="core/regressions.js"></script>
<script src="core/perf.js"></script>
<!-- attribute tests -->
<script src="attributes/hx-boost.js"></script>
<script src="attributes/hx-delete.js"></script>
<script src="attributes/hx-error-url.js"></script>
<script src="attributes/hx-ext.js"></script>
<script src="attributes/hx-get.js"></script>
<script src="attributes/hx-include.js"></script>
@ -82,9 +82,16 @@
<script src="attributes/hx-trigger.js"></script>
<script src="attributes/hx-ws.js"></script>
<!-- hyperscript integration -->
<script src="lib/_hyperscript.js"></script>
<script src="ext/hyperscript.js"></script>
<script>
_hyperscript.start();
</script>
<!-- extension tests -->
<script src="../src/ext/rails-method.js"></script>
<script src="ext/rails-method.js"></script>
<script src="../src/ext/method-override.js"></script>
<script src="ext/method-override.js"></script>
<script src="../src/ext/debug.js"></script>
<script src="ext/debug.js"></script>
@ -115,6 +122,8 @@
<script src="../src/ext/include-vals.js"></script>
<script src="ext/include-vals.js"></script>
<script src="../src/ext/ajax-header.js"></script>
<script src="ext/ajax-header.js"></script>
<!-- events last so they don't screw up other tests -->
<script src="core/events.js"></script>

1603
test/lib/_hyperscript.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -29,8 +29,13 @@
//
// make('<div hx-get="/test">dd</div>')
this.server.respondWith("GET", "/test", '<div id="d1" style="color: red; margin: 100px">Foo</div>');
make('<div hx-swap="outerHTML" hx-get="/test" hx-push-url="true" id="d1">Foo</div>');
htmx.logAll();
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(201, {}, '<form><input hx-trigger="keyup delay:1s changed" hx-swap="outerHTML" hx-get="/test" id="i1" value="blahblah"/></form>')
});
make('<form hx-target="this"><input hx-trigger="keyup delay:1s changed" hx-swap="outerHTML" hx-get="/test" id="i1"/></form>');
</script>

View File

@ -5,5 +5,6 @@ module.exports = function(config) {
config.addPassthroughCopy("css");
config.addPassthroughCopy("img");
config.addPassthroughCopy("test");
config.addPassthroughCopy("_redirects");
config.addPlugin(pluginSass, {});
}

1
www/_redirects Normal file
View File

@ -0,0 +1 @@
/locality-of-behaviour /essays/locality-of-behaviour

View File

@ -1,24 +0,0 @@
---
layout: layout.njk
title: </> htmx - hx-error-url
---
## `hx-error-url`
The `hx-error-url` attribute allows you to send client-side errors to a specified URL. It is typically put on the
body tag, so all errors are caught and send to the server.
```html
<body hx-error-url="/errors">\
</body>
```
When a client side error is caught by htmx it will be `POST`-ed to the given URL, with the following JSON format:
```json
{ "elt": elt.id, "event": eventName, "detail" : detail }
```
### Notes
* `hx-error-url` is inherited and can be placed on a parent element

View File

@ -18,7 +18,7 @@ Here is an example:
```html
<div hx-sse="connect /event_stream">
<div hx-get="/chatroom" hx-trigger="chatter">
<div hx-get="/chatroom" hx-trigger="sse:chatter">
...
</div>
</div>

View File

@ -27,6 +27,7 @@ title: </> htmx - high power tools for html
* [animations](#animations)
* [extensions](#extensions)
* [events & logging](#events)
* [hyperscript](#hyperscript)
* [configuring](#config)
</div>
@ -157,8 +158,11 @@ There are few other modifiers you can use for trigger:
* `changed` - only issue a request if the value of the element has changed
* `delay:<time interval>` - wait the given amount of time (e.g. `1s`) before
issuing the request. If the event triggers again, the countdown is reset.
* `throttle:<time interval>` - wait the given amount of time (e.g. `1s`) before
issuing the request. Unlike `delay` if a new event occurs before the time limit is hit the event will be discarded,
so the request will trigger at the end of the time period.
You can use these two attributes to implement a common UX pattern, [Active Search](/examples/active-search):
You can use these attributes to implement many common UX patterns, such as [Active Search](/examples/active-search):
```html
<input type="text" name="q"
@ -302,6 +306,7 @@ with any of the following values:
| `beforebegin` | prepends the content before the target in the targets parent element
| `beforeend` | appends the content after the last child inside the target
| `afterend` | appends the content after the target in the targets parent element
| `none` | does not append content from respons (out of band items will still be processed)
#### <a name="oob_swaps"></a>[Out of Band Swaps](#oob_swaps)
@ -564,8 +569,6 @@ If you set a logger at `htmx.logger`, every event will be logged. This can be v
}
```
Htmx can also send errors to a URL that is specified with the [hx-error-url](/attributes/hx-error-url) attributes. This can be useful for debugging client-side issues.
Htmx includes a helper method:
```javascript
@ -574,6 +577,95 @@ Htmx includes a helper method:
if you want to log everything while developing.
## <a name="hyperscript"></a>[hyperscript](#hyperscript)
**NOTE: hyperscript is in very early alpha**
Hyperscript is a small scripting language designed to be expressive, making it ideal for embedding directly in HTML,
handling custom events, etc. The language is inspired by [HyperTalk](http://hypercard.org/HyperTalk%20Reference%202.4.pdf),
javascript, [gosu](https://gosu-lang.github.io/) and others.
You can explore the language more fully on its main website:
<http://hyperscript.org>
### Events & Hyperscript
Hyperscript was designed to help address features and functionality from intercooler.js that are not implemented in htmx
directly, in a more flexible and open manner. One of its prime features is the ability to respond to arbitrary events
on a DOM element, using the `on` syntax:
```html
<div _="on afterSettle.htmx log 'Settled!'">
...
</div>
```
This will log `Settled!` to the console when the `afterSettle.htmx` event is triggered.
#### intercooler.js features & hyperscript implementations
Below are some examples of intercooler features and the hyperscript equivalent.
##### `ic-remove-after`
Intercooler provided the [`ic-remove-after`](http://intercoolerjs.org/attributes/ic-remove-after.html) attribute
for removing an element after a given amount of time.
In hyperscript this is implemented like so:
```html
<div _="on load wait 5s then remove me">Here is a temporary message!</div>
```
##### `ic-post-errors-to`
Intercooler provided the [`ic-post-errors-to`](http://intercoolerjs.org/attributes/ic-post-errors-to.html) attribute
for posting errors that occured during requests and responses.
In hyperscript similar functionality is implemented like so:
```html
<body _="on error.htmx(errorInfo) ajax POST errorInfo to /errors">
...
</body>
```
##### `ic-switch-class`
Intercooler provided the [`ic-switch-class`](http://intercoolerjs.org/attributes/ic-switch-class.html) attribute, which
let you switch a class between siblings.
In hyperscript you can implement similar functionality like so:
```html
<div hx-target="#content" _="on beforeOnLoad.htmx take .active from .tabs for event.target">
<a class="tabs active" hx-get="/tabl1" >Tab 1</a>
<a class="tabs" hx-get="/tabl2">Tab 2</a>
<a class="tabs" hx-get="/tabl3">Tab 3</a>
</div>
<div id="content">Tab 1 Content</div>
```
##### `X-IC-Redirect`
Intercooler provided more response headers than htmx does: `X-IC-Refresh`, `X-IC-Redirect` etc. Htmx omits these
headers in favor of the general `HX-Trigger`, combined with some client side code.
Let's implement the `X-IC-Redirect` header using the `HX-Trigger` response header and some hyperscript.
First, let's trigger an event with a response header that looks like this:
`HX-Trigger:{"redirect":{"url":"https://htmx.org"}}`
Then we would write the following hyperscript:
```html
<body _="on redirect(url) put url into window.location">
...
</body>
```
## <a name="config"></a>[Configuring htmx](#config)
Htmx allows you to configure a few defaults:

View File

@ -53,8 +53,7 @@ This event is triggered after new content has been [swapped into the DOM](/docs
### <a name="beforeOnLoad.htmx"></a> Event - [`beforeOnLoad.htmx`](#beforeOnLoad.htmx)
This event is triggered before any new content has been [swapped into the DOM](/docs#swapping). If
the event is cancelled, no swap will occur.
This event is triggered before any response processing occurs. If the event is cancelled, no swap will occur.
##### Details
@ -72,6 +71,39 @@ This event is triggered before an AJAX request is issued. If the event is cance
* `detail.xhr` - the `XMLHttpRequest`
* `detail.target` - the target of the request
### <a name="beforeSwap.htmx"></a> Event - [`beforeSwap.htmx`](#beforeSwap.htmx)
This event is triggered before any new content has been [swapped into the DOM](/docs#swapping). If the event is cancelled, no swap will occur.
##### Details
* `detail.elt` - the element that dispatched the request
* `detail.xhr` - the `XMLHttpRequest`
* `detail.target` - the target of the request
### <a name="configRequest.htmx"></a> Event - [`configRequest.htmx`](#configRequest.htmx)
This event is triggered after htmx has collected parameters for inclusion in the request. It can be
used to include or update the parameters that htmx will send:
```javascript
document.body.addEventListener('configRequest.htmx', function(evt) {
evt.detail.parameters['auth_token'] = getAuthToken(); // add a new parameter into the mix
});
```
Note that if an input value appears more than once the value in the `parameters` object will be an array, rather
than a single value.
##### Details
* `detail.parameters` - the parameters that will be submitted in the request
* `detail.unfilteredParameters` - the parameters that were found before filtering by [`hx-select`](/attributes/hx-select)
* `detail.headers` - the request headers
* `detail.elt` - the element that triggered the request
* `detail.target` - the target of the request
* `detail.verb` - the HTTP verb in use
### <a name="historyCacheMiss.htmx"></a> Event - [`historyCacheMiss.htmx`](#historyCacheMiss.htmx)
This event is triggered when a cache miss occurs when restoring history
@ -81,20 +113,20 @@ This event is triggered when a cache miss occurs when restoring history
* `detail.xhr` - the `XMLHttpRequest` that will retrieve the remote content for restoration
* `detail.path` - the path and query of the page being restored
### <a name="historyCacheMissLoad.htmx"></a> Event - [`historyCacheMissLoad.htmx`](#historyCacheMissLoad.htmx)
### <a name="historyCacheMissError.htmx"></a> Event - [`historyCacheMissError.htmx`](#historyCacheMissError.htmx)
This event is triggered when a cache miss occurs and a response has been retrieved succesfully from the server
for the content to restore
This event is triggered when a cache miss occurs and a response has been retrieved from the server
for the content to restore, but the response is an error (e.g. `404`)
##### Details
* `detail.xhr` - the `XMLHttpRequest`
* `detail.path` - the path and query of the page being restored
### <a name="historyCacheMissError.htmx"></a> Event - [`historyCacheMissError.htmx`](#historyCacheMissError.htmx)
### <a name="historyCacheMissLoad.htmx"></a> Event - [`historyCacheMissLoad.htmx`](#historyCacheMissLoad.htmx)
This event is triggered when a cache miss occurs and a response has been retrieved from the server
for the content to restore, but the response is an error (e.g. `404`)
This event is triggered when a cache miss occurs and a response has been retrieved succesfully from the server
for the content to restore
##### Details
@ -209,29 +241,6 @@ This event is triggered when an error occurs during the swap phase
* `detail.elt` - the element that triggered the request
* `detail.target` - the target of the request
### <a name="configRequest.htmx"></a> Event - [`configRequest.htmx`](#configRequest.htmx)
This event is triggered after htmx has collected parameters for inclusion in the request. It can be
used to include or update the parameters that htmx will send:
```javascript
document.body.addEventListener('configRequest.htmx', function(evt) {
evt.detail.parameters['auth_token'] = getAuthToken(); // add a new parameter into the mix
});
```
Note that if an input value appears more than once the value in the `parameters` object will be an array, rather
than a single value.
##### Details
* `detail.parameters` - the parameters that will be submitted in the request
* `detail.unfilteredParameters` - the parameters that were found before filtering by [`hx-select`](/attributes/hx-select)
* `detail.headers` - the request headers
* `detail.elt` - the element that triggered the request
* `detail.target` - the target of the request
* `detail.verb` - the HTTP verb in use
### <a name="targetError.htmx"></a> Event - [`targetError.htmx`](#targetError.htmx)
This event is triggered when a bad selector is used for a [`hx-target`](/attributes/hx-target) attribute (e.g. an

View File

@ -5,17 +5,26 @@ title: </> htmx - high power tools for html
## htmx Extensions
Htmx has an extension mechanism for defining and using extensions to the default behavior in a simple and obvious manner.
Htmx provides an extension mechanism for defining and using extensions within htmx-based applications.
## <a name="using"></a>[Using Extensions](#using)
To use an extension you use the [hx-ext](/attributes/hx-ext) attribute:
Using an extension involves two steps:
* include the extension definition, which will add it to the `htmx` extension registry
* reference the extension via the [hx-ext](/attributes/hx-ext) attribute
Here is an example
```html
<script src="https://unpkg.com/htmx.org@0.0.4/dist/ext/debug.js"></script>
<button hx-post="/example" hx-ext="debug">This Button Uses The Debug Extension</button>
```
Note that the `hx-ext` tag may be placed on parent elements if you want a plugin to apply to an entire swath of the dom,
This loads the debug extension off of the `unpkg` CDN and then adds the debug extension to the given button. (This
will print out extensive logging for the button, for debugging purposes.)
Note that the `hx-ext` tag may be placed on parent elements if you want a plugin to apply to an entire part of the DOM,
and on the `body` tag for it to apply to all htmx requests.
**Tip:** To use multiple extensions on one element, separate them with a comma:
@ -26,26 +35,32 @@ and on the `body` tag for it to apply to all htmx requests.
## <a name="included"></a> [Included Extensions](#included)
The following extensions that are tested and distributed with htmx:
htmx includes a set of extensions out of the box that address common developer needs. These extensions are tested
against `htmx` in each distribution
### <a name='reference'></a> [Included Extensions List](#reference)
<div class="info-table">
| Extension | Description
|-----------|-------------
| [`json-enc`](/extensions/json-enc) | use JSON encoding in the body of requests, rather than the default `x-www-form-urlencoded`
| [`method-override`](/extensions/method-override) | use the `X-HTTP-Method-Override` header for non-`GET` and `POST` requests
| [`morphdom-swap`](/extensions/morphdom-swap) | an extension for using the [morphdom](https://github.com/patrick-steele-idem/morphdom) library as the swapping mechanism in htmx.
| [`client-side-templates`](/extensions/client-side-templates) | support for client side template processing of JSON responses
| [`debug`](/extensions/debug) | an extension for debugging of a particular element using htmx
| [`path-deps`](/extensions/path-deps) | an extension for expressing path-based dependencies [similar to intercoolerjs](http://intercoolerjs.org/docs.html#dependencies)
| [`class-tools`](/extensions/class-tools) | an extension for manipulating timed addition and removal of classes on HTML elements
| [`rails-method`](/extensions/rails-method) | includes the `_method` parameter in requests for rails compatibility
| [`remove-me`](/extensions/remove-me) | allows you to remove an element after a given amount of time
| [`include-vals`](/extensions/include-vals) | allows you to include additional values in a request
| [`ajax-header`](/extensions/ajax-header) | includes the commonly-used `X-Requested-With` header that identifies ajax requests in many backend frameworks
</div>
## <a name="defining"></a>[Defining an Extensions](#defining)
To define an extension you need to call the `htmx.defineExtension()` function:
To define an extension you call the `htmx.defineExtension()` function:
```html
<script>
@ -57,9 +72,11 @@ To define an extension you need to call the `htmx.defineExtension()` function:
</script>
```
Extensions should have names that are dash separated like above and that are reasonably short and descriptive.
Typically, this is done in a stand-alone javascript file, rather than in an inline `script` tag.
Extensions can override the following default extension fields:
Extensions should have names that are dash separated and that are reasonably short and descriptive.
Extensions can override the following default extension points to add or change functionality:
```javascript
{

View File

@ -0,0 +1,22 @@
---
layout: layout.njk
title: </> htmx - high power tools for html
---
## The `ajax-header` Extension
This extension adds the `X-Requested-With` header to requests with the value "XMLHttpRequest".
This header is commonly used by javascript frameworks to differentiate ajax requests from normal http requests.
### Usage
```html
<body hx-ext="ajax-header">
...
</body>
```
### Source
<https://unpkg.com/htmx.org/dist/ext/ajax-header.js>

View File

@ -0,0 +1,24 @@
---
layout: layout.njk
title: </> htmx - high power tools for html
---
## The `method-override` Extension
This extension makes non-`GET` and `POST` requests use a `POST` with the `X-HTTP-Method-Override` header set to the
actual HTTP method. This is necessary when dealing with some firewall or proxy situations.
#### Usage
```html
<body hx-ext="method-override">
<button hx-put="/update">
This request will be made as a POST w/ the X-HTTP-Method-Override Header Set
</button>
</body>
```
#### Source
<https://unpkg.com/htmx.org/dist/ext/method-override.js>

View File

@ -1,20 +0,0 @@
---
layout: layout.njk
title: </> htmx - high power tools for html
---
## The `rails-method` Extension
This extension includes the rails `_method` parameter in non-`GET` or `POST` requests.
### Usage
```html
<body hx-ext="rails-method">
...
</body>
```
### Source
<https://unpkg.com/htmx.org/dist/ext/rails-method.js>

View File

@ -58,14 +58,24 @@
}
}
function maybeProcessClasses(elt) {
if (elt.getAttribute) {
var classList = elt.getAttribute("classes") || elt.getAttribute("data-classes");
if (classList) {
processClassList(elt, classList);
}
}
}
htmx.defineExtension('class-tools', {
onEvent: function (name, evt) {
if (name === "processedNode.htmx") {
var elt = evt.detail.elt;
if (elt.getAttribute) {
var classList = elt.getAttribute("classes") || elt.getAttribute("data-classes");
if (classList) {
processClassList(elt, classList);
maybeProcessClasses(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[classes], [data-classes]");
for (var i = 0; i < children.length; i++) {
maybeProcessClasses(children[i]);
}
}
}

View File

@ -12,12 +12,15 @@ return (function () {
'use strict';
var VERBS = ['get', 'post', 'put', 'delete', 'patch'];
var VERB_SELECTOR = VERBS.map(function(verb){
return "[hx-" + verb + "], [data-hx-" + verb + "]"
}).join(", ");
//====================================================================
// Utilities
//====================================================================
function parseInterval(str) {
if (str === "null" || str === "false" || str === "") {
if (str == null || str === "null" || str === "false" || str === "") {
return null;
} else if (str.lastIndexOf("ms") === str.length - 2) {
return parseFloat(str.substr(0, str.length - 2));
@ -33,6 +36,11 @@ return (function () {
}
// resolve with both hx and data-hx prefixes
function hasAttribute(elt, qualifiedName) {
return elt.hasAttribute && (elt.hasAttribute(qualifiedName) ||
elt.hasAttribute("data-" + qualifiedName));
}
function getAttributeValue(elt, qualifiedName) {
return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName);
}
@ -374,20 +382,23 @@ return (function () {
function handleAttributes(parentNode, fragment, settleInfo) {
forEach(fragment.querySelectorAll("[id]"), function (newNode) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]")
if (oldNode && oldNode !== parentNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
if (newNode.id && newNode.id.length > 0) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]");
if (oldNode && oldNode !== parentNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
}
}
});
}
function makeLoadTask(child) {
function makeAjaxLoadTask(child) {
return function () {
processNode(child);
processScripts(child);
triggerEvent(child, 'load.htmx', {});
};
}
@ -398,7 +409,7 @@ return (function () {
var child = fragment.firstChild;
parentNode.insertBefore(child, insertBefore);
if (child.nodeType !== Node.TEXT_NODE) {
settleInfo.tasks.push(makeLoadTask(child));
settleInfo.tasks.push(makeAjaxLoadTask(child));
}
}
}
@ -477,6 +488,8 @@ return (function () {
function swap(swapStyle, elt, target, fragment, settleInfo) {
switch (swapStyle) {
case "none":
return;
case "outerHTML":
swapOuterHTML(target, fragment, settleInfo);
return;
@ -563,6 +576,9 @@ return (function () {
if (token.indexOf("delay:") === 0) {
triggerSpec.delay = parseInterval(token.substr(6));
}
if (token.indexOf("throttle:") === 0) {
triggerSpec.throttle = parseInterval(token.substr(9));
}
}
return triggerSpec;
}).filter(function(x){ return x !== null });
@ -655,13 +671,21 @@ return (function () {
if (elementData.delayed) {
clearTimeout(elementData.delayed);
}
var issueRequest = function(){
issueAjaxRequest(elt, verb, path, evt.target);
if (elementData.throttle) {
return;
}
if (triggerSpec.delay) {
elementData.delayed = setTimeout(issueRequest, triggerSpec.delay);
if (triggerSpec.throttle) {
elementData.throttle = setTimeout(function(){
issueAjaxRequest(elt, verb, path, evt.target);
elementData.throttle = null;
}, triggerSpec.throttle);
} else if (triggerSpec.delay) {
elementData.delayed = setTimeout(function(){
issueAjaxRequest(elt, verb, path, evt.target);
}, triggerSpec.delay);
} else {
issueRequest();
issueAjaxRequest(elt, verb, path, evt.target);
}
}
};
@ -825,8 +849,8 @@ return (function () {
function processVerbs(elt, nodeData, triggerSpecs) {
var explicitAction = false;
forEach(VERBS, function (verb) {
var path = getAttributeValue(elt, 'hx-' + verb);
if (path) {
if (hasAttribute(elt,'hx-' + verb)) {
var path = getAttributeValue(elt, 'hx-' + verb);
explicitAction = true;
nodeData.path = path;
nodeData.verb = verb;
@ -850,17 +874,50 @@ return (function () {
return explicitAction;
}
function processScript(elt) {
if (elt.tagName === "SCRIPT" && elt.type === "text/javascript") {
eval(elt.innerText);
function evalScript(script) {
if (script.type === "text/javascript") {
try {
eval(script.innerText);
} catch (e) {
logError(e);
}
}
}
function processNode(elt) {
function processScripts(elt) {
if (matches(elt, "script")) {
evalScript(elt);
}
forEach(findAll(elt, "script"), function (script) {
evalScript(script);
});
}
function isHyperScriptAvailable() {
return typeof _hyperscript !== "undefined";
}
function findElementsToProcess(elt) {
if (elt.querySelectorAll) {
return elt.querySelectorAll(VERB_SELECTOR + ", a, form, [hx-sse], [data-hx-sse], [hx-ws], [data-hx-ws]");
} else {
return [];
}
}
function processNode(elt, ignoreChildren) {
var nodeData = getInternalData(elt);
if (!nodeData.processed) {
nodeData.processed = true;
if(isHyperScriptAvailable()){
_hyperscript.init(elt);
}
if (elt.value) {
nodeData.lastValue = elt.value;
}
var triggerSpecs = getTriggerSpecs(elt);
var explicitAction = processVerbs(elt, nodeData, triggerSpecs);
@ -878,11 +935,9 @@ return (function () {
processWebSocketInfo(elt, nodeData, wsInfo);
}
triggerEvent(elt, "processedNode.htmx");
processScript(elt);
}
if (elt.children) { // IE
forEach(elt.children, function(child) { processNode(child) });
if (!ignoreChildren) {
forEach(findElementsToProcess(elt), function(child) { processNode(child, true) });
}
}
@ -890,16 +945,6 @@ return (function () {
// Event/Log Support
//====================================================================
function sendError(elt, eventName, detail) {
var errorURL = getClosestAttributeValue(elt, "hx-error-url");
if (errorURL) {
var xhr = new XMLHttpRequest();
xhr.open("POST", errorURL);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.send(JSON.stringify({ "elt": elt.id, "event": eventName, "detail" : detail }));
}
}
function makeEvent(eventName, detail) {
var evt;
if (window.CustomEvent && typeof window.CustomEvent === 'function') {
@ -948,7 +993,7 @@ return (function () {
}
if (detail.error) {
logError(detail.error);
sendError(elt, eventName, detail);
triggerEvent(elt, "error.htmx", {errorInfo:detail})
}
var eventResult = elt.dispatchEvent(event);
withExtensions(elt, function (extension) {
@ -1319,9 +1364,6 @@ return (function () {
if (verb !== 'get') {
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
if (verb !== 'post') {
headers['X-HTTP-Method-Override'] = verb.toUpperCase();
}
}
// behavior of anchors w/ empty href is to use the current URL
@ -1334,9 +1376,15 @@ return (function () {
unfilteredParameters:rawParameters,
headers:headers,
target:target,
verb:verb
verb:verb,
path:path
};
if(!triggerEvent(elt, 'configRequest.htmx', requestConfig)) return endRequestLock();
// copy out in case the object was overwritten
path = requestConfig.path;
verb = requestConfig.verb;
headers = requestConfig.headers;
filteredParameters = requestConfig.parameters;
var splitPath = path.split("#");
var pathNoAnchor = splitPath[0];
@ -1357,7 +1405,7 @@ return (function () {
}
xhr.open('GET', finalPathForGet, true);
} else {
xhr.open('POST', path, true);
xhr.open(verb.toUpperCase(), path, true);
}
xhr.overrideMimeType("text/html");
@ -1385,7 +1433,7 @@ return (function () {
}
// don't process 'No Content' response
if (this.status !== 204) {
if (!triggerEvent(elt, 'beforeSwap.htmx', eventDetail)) return;
if (!triggerEvent(target, 'beforeSwap.htmx', eventDetail)) return;
var resp = this.response;
withExtensions(elt, function(extension){
@ -1402,15 +1450,32 @@ return (function () {
target.classList.add("htmx-swapping");
var doSwap = function () {
try {
var activeElt = document.activeElement;
var selectionInfo = {
elt: activeElt,
start: activeElt.selectionStart,
end: activeElt.selectionEnd,
};
var settleInfo = makeSettleInfo(target);
selectAndSwap(swapSpec.swapStyle, target, elt, resp, settleInfo);
if (!bodyContains(selectionInfo.elt) && selectionInfo.elt.id) {
var newActiveElt = document.getElementById(selectionInfo.elt.id);
if (selectionInfo.start && newActiveElt.setSelectionRange) {
newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end);
}
newActiveElt.focus();
}
target.classList.remove("htmx-swapping");
forEach(settleInfo.elts, function (elt) {
if (elt.classList) {
elt.classList.add("htmx-settling");
}
triggerEvent(elt, 'afterSwap.htmx', eventDetail);
});
triggerEvent(elt, 'afterSwap.htmx', eventDetail);
if (anchor) {
location.hash = anchor;
}
@ -1422,12 +1487,14 @@ return (function () {
if (elt.classList) {
elt.classList.remove("htmx-settling");
}
triggerEvent(elt, 'afterSettle.htmx', eventDetail);
});
// push URL and save new page
if (shouldSaveHistory) {
pushUrlIntoHistory(pushedUrl || path);
var pathToPush = pushedUrl || finalPathForGet || path;
pushUrlIntoHistory(pathToPush);
triggerEvent(getDocument().body, 'pushedIntoHistory.htmx', {path:pathToPush});
}
triggerEvent(elt, 'afterSettle.htmx', eventDetail);
}
if (swapSpec.settleDelay > 0) {
@ -1558,7 +1625,7 @@ return (function () {
ready(function () {
mergeMetaConfig();
var body = getDocument().body;
processNode(body);
processNode(body, true);
triggerEvent(body, 'load.htmx', {});
window.onpopstate = function () {
restoreHistory();
@ -1603,4 +1670,4 @@ return (function () {
}
}
)()
}));
}));

View File

@ -4,6 +4,15 @@ title: </> htmx - Attributes
---
## Contents
* [Htmx Attribute Reference](#attributes)
* [Htmx CSS Class Reference](#classes)
* [Htmx Request Headers Reference](#request_headers)
* [Htmx Response Headers Reference](#response_headers)
* [Htmx Event Reference](#events)
* [Htmx Extensions Reference](/extensions#reference)
## <a name="attributes"></a> [Attribute Reference](#attributes)
<div class="info-table">
@ -13,7 +22,6 @@ title: </> htmx - Attributes
| [`hx-boost`](/attributes/hx-boost) | progressively enhances anchors and forms to use AJAX requests
| [`hx-confirm`](/attributes/hx-confirm) | shows a confim() dialog before issuing a request
| [`hx-delete`](/attributes/hx-delete) | issues a `DELETE` to the specified URL
| [`hx-error-url`](/attributes/hx-error-url) | a URL to send client-side errors to
| [`hx-ext`](/attributes/hx-ext) | extensions to use for this element
| [`hx-get`](/attributes/hx-get) | issues a `GET` to the specified URL
| [`hx-history-elt`](/attributes/hx-history-elt) | the element to snapshot and restore during history navigation
@ -50,7 +58,7 @@ title: </> htmx - Attributes
## <a name="headers"></a> [HTTP Header Reference](#headers)
### <a name="request_headers"></a> [Request Headers](#request_headers)
### <a name="request_headers"></a> [Request Headers Reference](#request_headers)
<div class="info-table">
@ -70,7 +78,7 @@ title: </> htmx - Attributes
</div>
### <a name="response_headers"></a> [Response Headers](#response_headers)
### <a name="response_headers"></a> [Response Headers Reference](#response_headers)
<div class="info-table">

View File

@ -17,14 +17,24 @@ I'll be setting up a forum and chat room at some point.
[@htmx_org](https://twitter.com/htmx_org)
## Blog & Announcements
<div>
<div class="row">
<div class="1 col">
## Announcements
<ul>
{%- for post in collections.post reversed -%}
<li><a href="{{ post.url }}">{{ post.date | date: "%a, %b %d, %y"}} - {{ post.data.title }}</a>🔥🔥</li>
<li><a href="{{ post.url }}">{{ post.date | date: "%Y-%m-%d"}} - {{ post.data.title }} </a>🔥🔥</li>
{%- endfor -%}
</ul>
</div>
<div class="1 col">
## Essays
* [Locality of Behavior (LoB)](/essays/locality-of-behaviour)
</div>
</div>

10854
www/test/0.0.5/node_modules/chai/chai.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

325
www/test/0.0.5/node_modules/mocha/mocha.css generated vendored Normal file
View File

@ -0,0 +1,325 @@
@charset "utf-8";
body {
margin:0;
}
#mocha {
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: 60px 50px;
}
#mocha ul,
#mocha li {
margin: 0;
padding: 0;
}
#mocha ul {
list-style: none;
}
#mocha h1,
#mocha h2 {
margin: 0;
}
#mocha h1 {
margin-top: 15px;
font-size: 1em;
font-weight: 200;
}
#mocha h1 a {
text-decoration: none;
color: inherit;
}
#mocha h1 a:hover {
text-decoration: underline;
}
#mocha .suite .suite h1 {
margin-top: 0;
font-size: .8em;
}
#mocha .hidden {
display: none;
}
#mocha h2 {
font-size: 12px;
font-weight: normal;
cursor: pointer;
}
#mocha .suite {
margin-left: 15px;
}
#mocha .test {
margin-left: 15px;
overflow: hidden;
}
#mocha .test.pending:hover h2::after {
content: '(pending)';
font-family: arial, sans-serif;
}
#mocha .test.pass.medium .duration {
background: #c09853;
}
#mocha .test.pass.slow .duration {
background: #b94a48;
}
#mocha .test.pass::before {
content: '✓';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #00d6b2;
}
#mocha .test.pass .duration {
font-size: 9px;
margin-left: 5px;
padding: 2px 5px;
color: #fff;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
border-radius: 5px;
}
#mocha .test.pass.fast .duration {
display: none;
}
#mocha .test.pending {
color: #0b97c4;
}
#mocha .test.pending::before {
content: '◦';
color: #0b97c4;
}
#mocha .test.fail {
color: #c00;
}
#mocha .test.fail pre {
color: black;
}
#mocha .test.fail::before {
content: '✖';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #c00;
}
#mocha .test pre.error {
color: #c00;
max-height: 300px;
overflow: auto;
}
#mocha .test .html-error {
overflow: auto;
color: black;
display: block;
float: left;
clear: left;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid #eee;
max-width: 85%; /*(1)*/
max-width: -webkit-calc(100% - 42px);
max-width: -moz-calc(100% - 42px);
max-width: calc(100% - 42px); /*(2)*/
max-height: 300px;
word-wrap: break-word;
border-bottom-color: #ddd;
-webkit-box-shadow: 0 1px 3px #eee;
-moz-box-shadow: 0 1px 3px #eee;
box-shadow: 0 1px 3px #eee;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
#mocha .test .html-error pre.error {
border: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-box-shadow: 0;
-moz-box-shadow: 0;
box-shadow: 0;
padding: 0;
margin: 0;
margin-top: 18px;
max-height: none;
}
/**
* (1): approximate for browsers not supporting calc
* (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border)
* ^^ seriously
*/
#mocha .test pre {
display: block;
float: left;
clear: left;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid #eee;
max-width: 85%; /*(1)*/
max-width: -webkit-calc(100% - 42px);
max-width: -moz-calc(100% - 42px);
max-width: calc(100% - 42px); /*(2)*/
word-wrap: break-word;
border-bottom-color: #ddd;
-webkit-box-shadow: 0 1px 3px #eee;
-moz-box-shadow: 0 1px 3px #eee;
box-shadow: 0 1px 3px #eee;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
#mocha .test h2 {
position: relative;
}
#mocha .test a.replay {
position: absolute;
top: 3px;
right: 0;
text-decoration: none;
vertical-align: middle;
display: block;
width: 15px;
height: 15px;
line-height: 15px;
text-align: center;
background: #eee;
font-size: 15px;
-webkit-border-radius: 15px;
-moz-border-radius: 15px;
border-radius: 15px;
-webkit-transition:opacity 200ms;
-moz-transition:opacity 200ms;
-o-transition:opacity 200ms;
transition: opacity 200ms;
opacity: 0.3;
color: #888;
}
#mocha .test:hover a.replay {
opacity: 1;
}
#mocha-report.pass .test.fail {
display: none;
}
#mocha-report.fail .test.pass {
display: none;
}
#mocha-report.pending .test.pass,
#mocha-report.pending .test.fail {
display: none;
}
#mocha-report.pending .test.pass.pending {
display: block;
}
#mocha-error {
color: #c00;
font-size: 1.5em;
font-weight: 100;
letter-spacing: 1px;
}
#mocha-stats {
position: fixed;
top: 15px;
right: 10px;
font-size: 12px;
margin: 0;
color: #888;
z-index: 1;
}
#mocha-stats .progress {
float: right;
padding-top: 0;
/**
* Set safe initial values, so mochas .progress does not inherit these
* properties from Bootstrap .progress (which causes .progress height to
* equal line height set in Bootstrap).
*/
height: auto;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
background-color: initial;
}
#mocha-stats em {
color: black;
}
#mocha-stats a {
text-decoration: none;
color: inherit;
}
#mocha-stats a:hover {
border-bottom: 1px solid #eee;
}
#mocha-stats li {
display: inline-block;
margin: 0 5px;
list-style: none;
padding-top: 11px;
}
#mocha-stats canvas {
width: 40px;
height: 40px;
}
#mocha code .comment { color: #ddd; }
#mocha code .init { color: #2f6fad; }
#mocha code .string { color: #5890ad; }
#mocha code .keyword { color: #8a6343; }
#mocha code .number { color: #2f6fad; }
@media screen and (max-device-width: 480px) {
#mocha {
margin: 60px 0px;
}
#mocha #stats {
position: absolute;
}
}

18178
www/test/0.0.5/node_modules/mocha/mocha.js generated vendored Normal file

File diff suppressed because one or more lines are too long

16430
www/test/0.0.5/node_modules/sinon/pkg/sinon.js generated vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
htmx.defineExtension('ajax-header', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest';
}
}
});

View File

@ -0,0 +1,84 @@
(function(){
function splitOnWhitespace(trigger) {
return trigger.split(/\s+/);
}
function parseClassOperation(trimmedValue) {
var split = splitOnWhitespace(trimmedValue);
if (split.length > 1) {
var operation = split[0];
var classDef = split[1].trim();
var cssClass;
var delay;
if (classDef.indexOf(":") > 0) {
var splitCssClass = classDef.split(':');
cssClass = splitCssClass[0];
delay = htmx.parseInterval(splitCssClass[1]);
} else {
cssClass = classDef;
delay = 100;
}
return {
operation:operation,
cssClass:cssClass,
delay:delay
}
} else {
return null;
}
}
function processClassList(elt, classList) {
var runs = classList.split("&");
for (var i = 0; i < runs.length; i++) {
var run = runs[i];
var currentRunTime = 0;
var classOperations = run.split(",");
for (var j = 0; j < classOperations.length; j++) {
var value = classOperations[j];
var trimmedValue = value.trim();
var classOperation = parseClassOperation(trimmedValue);
if (classOperation) {
if (classOperation.operation === "toggle") {
setTimeout(function () {
setInterval(function () {
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
}, classOperation.delay);
}, currentRunTime);
currentRunTime = currentRunTime + classOperation.delay;
} else {
currentRunTime = currentRunTime + classOperation.delay;
setTimeout(function () {
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
}, currentRunTime);
}
}
}
}
}
function maybeProcessClasses(elt) {
if (elt.getAttribute) {
var classList = elt.getAttribute("classes") || elt.getAttribute("data-classes");
if (classList) {
processClassList(elt, classList);
}
}
}
htmx.defineExtension('class-tools', {
onEvent: function (name, evt) {
if (name === "processedNode.htmx") {
var elt = evt.detail.elt;
maybeProcessClasses(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[classes], [data-classes]");
for (var i = 0; i < children.length; i++) {
maybeProcessClasses(children[i]);
}
}
}
}
});
})();

View File

@ -0,0 +1,37 @@
htmx.defineExtension('client-side-templates', {
transformResponse : function(text, xhr, elt) {
var mustacheTemplate = htmx.closest(elt, "[mustache-template]");
if (mustacheTemplate) {
var data = JSON.parse(text);
var templateId = mustacheTemplate.getAttribute('mustache-template');
var template = htmx.find("#" + templateId);
if (template) {
return Mustache.render(template.innerHTML, data);
} else {
throw "Unknown mustache template: " + templateId;
}
}
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 nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
if (nunjucksTemplate) {
var data = JSON.parse(text);
var templateName = nunjucksTemplate.getAttribute('nunjucks-template');
var template = htmx.find('#' + templateName);
if (template) {
return nunjucks.renderString(template.innerHTML, data);
} else {
return nunjucks.render(templateName, data);
}
}
return text;
}
});

View File

@ -0,0 +1,11 @@
htmx.defineExtension('debug', {
onEvent: function (name, evt) {
if (console.debug) {
console.debug(name, evt);
} else if (console) {
console.log("DEBUG:", name, evt);
} else {
throw "NO CONSOLE SUPPORTED"
}
}
});

View File

@ -0,0 +1,24 @@
(function(){
function mergeObjects(obj1, obj2) {
for (var key in obj2) {
if (obj2.hasOwnProperty(key)) {
obj1[key] = obj2[key];
}
}
return obj1;
}
htmx.defineExtension('include-vals', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
var includeValsElt = htmx.closest(evt.detail.elt, "[include-vals],[data-include-vals]");
if (includeValsElt) {
var includeVals = includeValsElt.getAttribute("include-vals") || includeValsElt.getAttribute("data-include-vals");
var valuesToInclude = eval("({" + includeVals + "})");
mergeObjects(evt.detail.parameters, valuesToInclude);
}
}
}
});
})();

View File

@ -0,0 +1,12 @@
htmx.defineExtension('json-enc', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
evt.detail.headers['Content-Type'] = "application/json";
}
},
encodeParameters : function(xhr, parameters, elt) {
xhr.overrideMimeType('text/json');
return (JSON.stringify(parameters));
}
});

View File

@ -0,0 +1,11 @@
htmx.defineExtension('method-override', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {
var method = evt.detail.verb;
if (method !== "get" || method !== "post") {
evt.detail.headers['X-HTTP-Method-Override'] = method.toUpperCase();
evt.detail.verb = "post";
}
}
}
});

View File

@ -0,0 +1,11 @@
htmx.defineExtension('morphdom-swap', {
isInlineSwap: function(swapStyle) {
return swapStyle === 'morphdom';
},
handleSwap: function (swapStyle, target, fragment) {
if (swapStyle === 'morphdom') {
morphdom(target, fragment.outerHTML);
return true;
}
}
});

View File

@ -0,0 +1,35 @@
(function(){
function dependsOn(pathSpec, url) {
var dependencyPath = pathSpec.split("/");
var urlPath = url.split("/");
for (var i = 0; i < urlPath.length; i++) {
var dependencyElement = dependencyPath.shift();
var pathElement = urlPath[i];
if (dependencyElement !== pathElement && dependencyElement !== "*") {
return false;
}
if (dependencyPath.length === 0 || (dependencyPath.length === 1 && dependencyPath[0] === "")) {
return true;
}
}
return false;
}
htmx.defineExtension('path-deps', {
onEvent: function (name, evt) {
if (name === "afterRequest.htmx") {
var xhr = evt.detail.xhr;
// mutating call
if (xhr.method !== "GET") {
var eltsWithDeps = htmx.findAll("[path-deps]");
for (var i = 0; i < eltsWithDeps.length; i++) {
var elt = eltsWithDeps[i];
if (dependsOn(elt.getAttribute('path-deps'), xhr.url)) {
htmx.trigger(elt, "path-deps");
}
}
}
}
}
});
})();

View File

@ -0,0 +1,27 @@
(function(){
function maybeRemoveMe(elt) {
var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
if (timing) {
setTimeout(function () {
elt.parentElement.removeChild(elt);
}, htmx.parseInterval(timing));
}
}
htmx.defineExtension('remove-me', {
onEvent: function (name, evt) {
if (name === "processedNode.htmx") {
var elt = evt.detail.elt;
if (elt.getAttribute) {
maybeRemoveMe(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[remove-me], [data-remove-me");
for (var i = 0; i < children.length; i++) {
maybeRemoveMe(children[i]);
}
}
}
}
}
});
})();

1673
www/test/0.0.5/src/htmx.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
describe("hx-boost attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles basic anchor properly', function () {
this.server.respondWith("GET", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><a id="a1" href="/test">Foo</a></div>');
var a = byId('a1');
a.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
})
it('handles basic form post properly', function () {
this.server.respondWith("POST", "/test", "Boosted");
this.server.respondWith("POST", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test" method="post"><button id="b1">Submit</button></form></div>');
var btn = byId('b1');
btn.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
})
it('handles basic form get properly', function () {
this.server.respondWith("GET", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test" method="get"><button id="b1">Submit</button></form></div>');
var btn = byId('b1');
btn.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
})
it('handles basic form with no explicit method property', function () {
this.server.respondWith("GET", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test"><button id="b1">Submit</button></form></div>');
var btn = byId('b1');
btn.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
})
it('handles basic anchor properly w/ data-* prefix', function () {
this.server.respondWith("GET", "/test", "Boosted");
var div = make('<div data-hx-target="this" data-hx-boost="true"><a id="a1" href="/test">Foo</a></div>');
var a = byId('a1');
a.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
})
});

View File

@ -0,0 +1,34 @@
describe("hx-delete attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('issues a DELETE request', function()
{
this.server.respondWith("DELETE", "/test", function(xhr){
xhr.respond(200, {}, "Deleted!");
});
var btn = make('<button hx-delete="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Deleted!");
});
it('issues a DELETE request w/ data-* prefix', function()
{
this.server.respondWith("DELETE", "/test", function(xhr){
xhr.respond(200, {}, "Deleted!");
});
var btn = make('<button data-hx-delete="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Deleted!");
});
})

View File

@ -0,0 +1,98 @@
describe("hx-ext attribute", function() {
var ext1Calls, ext2Calls, ext3Calls;
beforeEach(function () {
ext1Calls = ext2Calls = ext3Calls = 0;
this.server = makeServer();
clearWorkArea();
htmx.defineExtension("ext-1", {
onEvent : function(name, evt) {
if(name === "afterRequest.htmx"){
ext1Calls++;
}
}
});
htmx.defineExtension("ext-2", {
onEvent : function(name, evt) {
if(name === "afterRequest.htmx"){
ext2Calls++;
}
}
});
htmx.defineExtension("ext-3", {
onEvent : function(name, evt) {
if(name === "afterRequest.htmx"){
ext3Calls++;
}
}
});
});
afterEach(function () {
this.server.restore();
clearWorkArea();
htmx.removeExtension("ext-1");
htmx.removeExtension("ext-2");
htmx.removeExtension("ext-3");
});
it('A simple extension is invoked properly', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="ext-1">Click Me!</button>')
btn.click();
this.server.respond();
ext1Calls.should.equal(1);
ext2Calls.should.equal(0);
ext3Calls.should.equal(0);
});
it('Extensions are merged properly', function () {
this.server.respondWith("GET", "/test", "Clicked!");
make('<div hx-ext="ext-1"><button id="btn-1" hx-get="/test" hx-ext="ext-2">Click Me!</button>' +
'<button id="btn-2" hx-get="/test" hx-ext="ext-3">Click Me!</button></div>')
var btn1 = byId("btn-1");
var btn2 = byId("btn-2");
btn1.click();
this.server.respond();
ext1Calls.should.equal(1);
ext2Calls.should.equal(1);
ext3Calls.should.equal(0);
btn2.click();
this.server.respond();
ext1Calls.should.equal(2);
ext2Calls.should.equal(1);
ext3Calls.should.equal(1);
});
it('supports comma separated lists', function () {
this.server.respondWith("GET", "/test", "Clicked!");
make('<div hx-ext="ext-1"><button id="btn-1" hx-get="/test" hx-ext="ext-2, ext-3 ">Click Me!</button></div>')
var btn1 = byId("btn-1");
var btn2 = byId("btn-2");
btn1.click();
this.server.respond();
ext1Calls.should.equal(1);
ext2Calls.should.equal(1);
ext3Calls.should.equal(1);
});
it('A simple extension is invoked properly w/ data-* prefix', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button data-hx-get="/test" data-hx-ext="ext-1">Click Me!</button>')
btn.click();
this.server.respond();
ext1Calls.should.equal(1);
ext2Calls.should.equal(0);
ext3Calls.should.equal(0);
});
});

View File

@ -0,0 +1,76 @@
describe("hx-get attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('issues a GET request on click and swaps content', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
});
it('GET does not include surrounding data by default', function () {
this.server.respondWith("GET", "/test", function (xhr) {
should.equal(getParameters(xhr)["i1"], undefined);
xhr.respond(200, {}, "Clicked!");
});
make('<form><input name="i1" value="value"/><button id="b1" hx-get="/test">Click Me!</button></form>')
var btn = byId("b1");
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
});
it('GET on form includes its own data by default', function () {
this.server.respondWith("GET", /\/test.*/, function (xhr) {
getParameters(xhr)["i1"].should.equal("value");
xhr.respond(200, {}, "Clicked!");
});
var form = make('<form hx-trigger="click" hx-get="/test"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('GET on form with existing parameters works properly', function () {
this.server.respondWith("GET", /\/test.*/, function (xhr) {
getParameters(xhr)["foo"].should.equal("bar");
getParameters(xhr)["i1"].should.equal("value");
xhr.respond(200, {}, "Clicked!");
});
var form = make('<form hx-trigger="click" hx-get="/test?foo=bar"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('GET on form with anchor works properly', function () {
this.server.respondWith("GET", /\/test.*/, function (xhr) {
getParameters(xhr)["foo"].should.equal("bar");
getParameters(xhr)["i1"].should.equal("value");
xhr.respond(200, {}, "Clicked!");
});
var form = make('<form hx-trigger="click" hx-get="/test?foo=bar#foo"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('issues a GET request on click and swaps content w/ data-* prefix', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button data-hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
});
});

View File

@ -0,0 +1,153 @@
describe("hx-include attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('By default an input includes itself', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div hx-target="this"><input hx-post="/include" hx-trigger="click" id="i1" name="i1" value="test"/></div>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('non-GET includes closest form', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<form hx-target="this"><div id="d1" hx-post="/include"></div><input name="i1" value="test"/></form>')
var input = byId("d1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('GET does not include closest form by default', function () {
this.server.respondWith("GET", "/include", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], undefined);
xhr.respond(200, {}, "Clicked!")
});
var div = make('<form hx-target="this"><div id="d1" hx-get="/include"></div><input name="i1" value="test"/></form>')
var input = byId("d1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('Input not included twice when in form', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<form hx-target="this"><input hx-post="/include" hx-trigger="click" id="i1" name="i1" value="test"/></form>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('Two inputs are included twice when they have the same name', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.deep.equal(["test", "test2"]);
xhr.respond(200, {}, "Clicked!")
});
var div = make('<form hx-target="this">' +
'<input hx-post="/include" hx-trigger="click" id="i1" name="i1" value="test"/>' +
'<input name="i1" value="test2"/>' +
'</form>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('Input not included twice when it explicitly refers to parent form', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<form id="f1" hx-target="this">' +
'<input hx-include="#f1" hx-post="/include" hx-trigger="click" id="i1" name="i1" value="test"/>' +
'</form>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('Input can be referred to externally', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<input id="i1" name="i1" value="test"/>');
var div = make('<div hx-post="/include" hx-include="#i1"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('Two inputs can be referred to externally', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
params['i2'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<input id="i1" name="i1" value="test"/>');
make('<input id="i2" name="i2" value="test"/>');
var div = make('<div hx-post="/include" hx-include="#i1, #i2"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('A form can be referred to externally', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
params['i2'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
make('<form id="f1">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'</form> ');
var div = make('<div hx-post="/include" hx-include="#f1"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
it('By default an input includes itself w/ data-* prefix', function () {
this.server.respondWith("POST", "/include", function (xhr) {
var params = getParameters(xhr);
params['i1'].should.equal("test");
xhr.respond(200, {}, "Clicked!")
});
var div = make('<div data-hx-target="this"><input data-hx-post="/include" data-hx-trigger="click" id="i1" name="i1" value="test"/></div>')
var input = byId("i1")
input.click();
this.server.respond();
div.innerHTML.should.equal("Clicked!");
});
});

View File

@ -0,0 +1,52 @@
describe("hx-indicator attribute", function(){
beforeEach(function() {
this.server = sinon.fakeServer.create();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('Indicator classes are properly put on element with no explicit indicator', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
btn.classList.contains("htmx-request").should.equal(true);
this.server.respond();
btn.classList.contains("htmx-request").should.equal(false);
});
it('Indicator classes are properly put on element with explicit indicator', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-indicator="#a1, #a2">Click Me!</button>')
var a1 = make('<a id="a1"></a>')
var a2 = make('<a id="a2"></a>')
btn.click();
btn.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(true);
a2.classList.contains("htmx-request").should.equal(true);
this.server.respond();
btn.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(false);
a2.classList.contains("htmx-request").should.equal(false);
});
it('Indicator classes are properly put on element with explicit indicator w/ data-* prefix', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" data-hx-indicator="#a1, #a2">Click Me!</button>')
var a1 = make('<a id="a1"></a>')
var a2 = make('<a id="a2"></a>')
btn.click();
btn.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(true);
a2.classList.contains("htmx-request").should.equal(true);
this.server.respond();
btn.classList.contains("htmx-request").should.equal(false);
a1.classList.contains("htmx-request").should.equal(false);
a2.classList.contains("htmx-request").should.equal(false);
});
})

View File

@ -0,0 +1,101 @@
describe("hx-params attribute", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('none excludes all params', function () {
this.server.respondWith("POST", "/params", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], undefined);
should.equal(params['i2'], undefined);
should.equal(params['i3'], undefined);
xhr.respond(200, {}, "Clicked!")
});
var form = make('<form hx-trigger="click" hx-post="/params" hx-params="none">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'<input name="i3" value="test"/>' +
'</form> ');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('"*" includes all params', function () {
this.server.respondWith("POST", "/params", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], "test");
should.equal(params['i2'], "test");
should.equal(params['i3'], "test");
xhr.respond(200, {}, "Clicked!")
});
var form = make('<form hx-trigger="click" hx-post="/params" hx-params="*">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'<input name="i3" value="test"/>' +
'</form> ');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('named includes works', function () {
this.server.respondWith("POST", "/params", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], "test");
should.equal(params['i2'], undefined);
should.equal(params['i3'], "test");
xhr.respond(200, {}, "Clicked!")
});
var form = make('<form hx-trigger="click" hx-post="/params" hx-params="i1, i3">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'<input name="i3" value="test"/>' +
'</form> ');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('named exclude works', function () {
this.server.respondWith("POST", "/params", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], undefined);
should.equal(params['i2'], "test");
should.equal(params['i3'], undefined);
xhr.respond(200, {}, "Clicked!")
});
var form = make('<form hx-trigger="click" hx-post="/params" hx-params="not i1, i3">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'<input name="i3" value="test"/>' +
'</form> ');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('named exclude works w/ data-* prefix', function () {
this.server.respondWith("POST", "/params", function (xhr) {
var params = getParameters(xhr);
should.equal(params['i1'], undefined);
should.equal(params['i2'], "test");
should.equal(params['i3'], undefined);
xhr.respond(200, {}, "Clicked!")
});
var form = make('<form data-hx-trigger="click" data-hx-post="/params" data-hx-params="not i1, i3">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="test"/>' +
'<input name="i3" value="test"/>' +
'</form> ');
form.click();
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
});

View File

@ -0,0 +1,34 @@
describe("hx-patch attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('issues a PATCH request', function()
{
this.server.respondWith("PATCH", "/test", function(xhr){
xhr.respond(200, {}, "Patched!");
});
var btn = make('<button hx-patch="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Patched!");
});
it('issues a PATCH request w/ data-* prefix', function()
{
this.server.respondWith("PATCH", "/test", function(xhr){
xhr.respond(200, {}, "Patched!");
});
var btn = make('<button data-hx-patch="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Patched!");
});
})

View File

@ -0,0 +1,36 @@
describe("hx-post attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('issues a POST request with proper headers', function()
{
this.server.respondWith("POST", "/test", function(xhr){
should.equal(xhr.requestHeaders['X-HTTP-Method-Override'], undefined);
xhr.respond(200, {}, "Posted!");
});
var btn = make('<button hx-post="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Posted!");
});
it('issues a POST request with proper headers w/ data-* prefix', function()
{
this.server.respondWith("POST", "/test", function(xhr){
should.equal(xhr.requestHeaders['X-HTTP-Method-Override'], undefined);
xhr.respond(200, {}, "Posted!");
});
var btn = make('<button data-hx-post="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Posted!");
});
})

View File

@ -0,0 +1,138 @@
describe("hx-push-url attribute", function() {
var HTMX_HISTORY_CACHE_NAME = "htmx-history-cache";
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
afterEach(function () {
this.server.restore();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
it("navigation should push an element into the cache ", function () {
this.server.respondWith("GET", "/test", "second");
getWorkArea().innerHTML.should.be.equal("");
var div = make('<div hx-push-url="true" hx-get="/test">first</div>');
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(1);
});
it("restore should return old value", function () {
this.server.respondWith("GET", "/test1", '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>');
this.server.respondWith("GET", "/test2", '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>');
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>');
byId("d1").click();
this.server.respond();
var workArea = getWorkArea();
workArea.textContent.should.equal("test1")
byId("d2").click();
this.server.respond();
workArea.textContent.should.equal("test2")
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(2);
htmx._('restoreHistory')("/test1")
this.server.respond();
getWorkArea().textContent.should.equal("test1")
});
it("cache should only store 10 entries", function () {
var x = 0;
this.server.respondWith("GET", /test.*/, function(xhr){
x++;
xhr.respond(200, {}, '<div id="d1" hx-push-url="true" hx-get="/test' + x + '" hx-swap="outerHTML settle:0"></div>')
});
getWorkArea().innerHTML.should.be.equal("");
make('<div id="d1" hx-push-url="true" hx-get="/test" hx-swap="outerHTML settle:0"></div>');
for (var i = 0; i < 20; i++) { // issue 20 requests
byId("d1").click();
this.server.respond();
}
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(10); // should only be 10 elements
});
it("cache miss should issue another GET", function () {
this.server.respondWith("GET", "/test1", '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>');
this.server.respondWith("GET", "/test2", '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>');
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>');
byId("d1").click();
this.server.respond();
var workArea = getWorkArea();
workArea.textContent.should.equal("test1")
byId("d2").click();
this.server.respond();
workArea.textContent.should.equal("test2")
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(2);
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME); // clear cache
htmx._('restoreHistory')("/test1")
this.server.respond();
getWorkArea().textContent.should.equal("test1")
});
it("navigation should push an element into the cache w/ data-* prefix", function () {
this.server.respondWith("GET", "/test", "second");
getWorkArea().innerHTML.should.be.equal("");
var div = make('<div data-hx-push-url="true" data-hx-get="/test">first</div>');
div.click();
this.server.respond();
getWorkArea().textContent.should.equal("second")
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME));
cache.length.should.equal(1);
});
it("afterSettle.htmx is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("afterSettle.htmx", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("afterSettle.htmx", handler);
}
});
it("should include parameters on a get", function () {
var path = "";
var handler = htmx.on("pushedIntoHistory.htmx", function (evt) {
path = evt.detail.path;
});
try {
this.server.respondWith("GET", /test.*/, function (xhr) {
xhr.respond(200, {}, "second")
});
var form = make('<form hx-trigger="click" hx-push-url="true" hx-get="/test"><input type="hidden" name="foo" value="bar"/>first</form>');
form.click();
this.server.respond();
form.textContent.should.equal("second")
path.should.equal("/test?foo=bar")
} finally {
htmx.off("pushedIntoHistory.htmx", handler);
}
});
});

View File

@ -0,0 +1,34 @@
describe("hx-put attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('issues a PUT request', function()
{
this.server.respondWith("PUT", "/test", function(xhr){
xhr.respond(200, {}, "Putted!");
});
var btn = make('<button hx-put="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Putted!");
});
it('issues a PUT request w/ data-* prefix', function()
{
this.server.respondWith("PUT", "/test", function(xhr){
xhr.respond(200, {}, "Putted!");
});
var btn = make('<button data-hx-put="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Putted!");
});
})

View File

@ -0,0 +1,40 @@
describe("BOOTSTRAP - htmx AJAX Tests", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('properly handles a partial of HTML', function()
{
var i = 1;
this.server.respondWith("GET", "/test", "<div id='d1'>foo</div><div id='d2'>bar</div>");
var div = make('<div hx-get="/test" hx-select="#d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
});
it('properly handles a full HTML document', function()
{
var i = 1;
this.server.respondWith("GET", "/test", "<html><body><div id='d1'>foo</div><div id='d2'>bar</div></body></html>");
var div = make('<div hx-get="/test" hx-select="#d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
});
it('properly handles a full HTML document w/ data-* prefix', function()
{
var i = 1;
this.server.respondWith("GET", "/test", "<html><body><div id='d1'>foo</div><div id='d2'>bar</div></body></html>");
var div = make('<div hx-get="/test" data-hx-select="#d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
});
})

View File

@ -0,0 +1,120 @@
describe("hx-sse attribute", function() {
function mockEventSource() {
var listeners = {};
var wasClosed = false;
var mockEventSource = {
addEventListener: function (message, l) {
listeners[message] = l;
},
sendEvent: function (event) {
var listener = listeners[event];
if (listener) {
listener();
}
},
close: function () {
wasClosed = true;
},
wasClosed: function () {
return wasClosed;
}
};
return mockEventSource;
}
beforeEach(function () {
this.server = makeServer();
var eventSource = mockEventSource();
this.eventSource = eventSource;
clearWorkArea();
htmx.createEventSource = function(){ return eventSource };
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles basic sse triggering', function () {
this.server.respondWith("GET", "/d1", "div1 updated");
this.server.respondWith("GET", "/d2", "div2 updated");
var div = make('<div hx-sse="connect /foo">' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
'<div id="d2" hx-trigger="sse:e2" hx-get="/d2">div2</div>' +
'</div>');
this.eventSource.sendEvent("e1");
this.server.respond();
byId("d1").innerHTML.should.equal("div1 updated");
byId("d2").innerHTML.should.equal("div2");
this.eventSource.sendEvent("e2");
this.server.respond();
byId("d1").innerHTML.should.equal("div1 updated");
byId("d2").innerHTML.should.equal("div2 updated");
})
it('does not trigger events that arent named', function () {
this.server.respondWith("GET", "/d1", "div1 updated");
var div = make('<div hx-sse="connect /foo">' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
'</div>');
this.eventSource.sendEvent("foo");
this.server.respond();
byId("d1").innerHTML.should.equal("div1");
this.eventSource.sendEvent("e2");
this.server.respond();
byId("d1").innerHTML.should.equal("div1");
this.eventSource.sendEvent("e1");
this.server.respond();
byId("d1").innerHTML.should.equal("div1 updated");
})
it('does not trigger events not on decendents', function () {
this.server.respondWith("GET", "/d1", "div1 updated");
var div = make('<div hx-sse="connect /foo"></div>' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>');
this.eventSource.sendEvent("foo");
this.server.respond();
byId("d1").innerHTML.should.equal("div1");
this.eventSource.sendEvent("e2");
this.server.respond();
byId("d1").innerHTML.should.equal("div1");
this.eventSource.sendEvent("e1");
this.server.respond();
byId("d1").innerHTML.should.equal("div1");
})
it('is closed after removal', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-sse="connect /foo">' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
'</div>');
div.click();
this.server.respond();
this.eventSource.wasClosed().should.equal(true)
})
it('is closed after removal with no close and activity', function () {
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-sse="connect /foo">' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
'</div>');
div.parentElement.removeChild(div);
this.eventSource.sendEvent("e1")
this.eventSource.wasClosed().should.equal(true)
})
});

View File

@ -0,0 +1,75 @@
describe("hx-swap-oob attribute", function () {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles basic response properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-oob='true'>Swapped</div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped");
})
it('handles more than one oob swap properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-oob='true'>Swapped1</div><div id='d2' hx-swap-oob='true'>Swapped2</div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"></div>');
make('<div id="d2"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped1");
byId("d2").innerHTML.should.equal("Swapped2");
})
it('handles no id match properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-oob='true'>Swapped</div>");
var div = make('<div hx-get="/test">click me</div>');
div.click();
this.server.respond();
div.innerText.should.equal("Clicked");
})
it('handles basic response properly w/ data-* prefix', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' data-hx-swap-oob='true'>Swapped</div>");
var div = make('<div data-hx-get="/test">click me</div>');
make('<div id="d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped");
})
it('handles outerHTML response properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' foo='bar' hx-swap-oob='outerHTML'>Swapped</div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"></div>');
div.click();
this.server.respond();
byId("d1").getAttribute("foo").should.equal("bar");
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped");
})
it('handles innerHTML response properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' foo='bar' hx-swap-oob='innerHTML'>Swapped</div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"></div>');
div.click();
this.server.respond();
should.equal(byId("d1").getAttribute("foo"), null);
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped");
})
});

View File

@ -0,0 +1,274 @@
describe("hx-swap attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('swap innerHTML properly', function()
{
this.server.respondWith("GET", "/test", '<a hx-get="/test2">Click Me</a>');
this.server.respondWith("GET", "/test2", "Clicked!");
var div = make('<div hx-get="/test"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal('<a hx-get="/test2">Click Me</a>');
var a = div.querySelector('a');
a.click();
this.server.respond();
a.innerHTML.should.equal('Clicked!');
});
it('swap outerHTML properly', function()
{
this.server.respondWith("GET", "/test", '<a id="a1" hx-get="/test2">Click Me</a>');
this.server.respondWith("GET", "/test2", "Clicked!");
var div = make('<div id="d1" hx-get="/test" hx-swap="outerHTML"></div>')
div.click();
should.equal(byId("d1"), div);
this.server.respond();
should.equal(byId("d1"), null);
byId("a1").click();
this.server.respond();
byId("a1").innerHTML.should.equal('Clicked!');
});
it('swap beforebegin properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforebegin">*</div>')
var parent = div.parentElement;
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("1*");
byId("a1").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*2*");
byId("a2").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("***");
});
it('swap afterbegin properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterbegin">*</div>')
div.click();
this.server.respond();
div.innerText.should.equal("1*");
byId("a1").click();
this.server.respond();
div.innerText.should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("2**");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("***");
});
it('swap afterbegin properly with no initial content', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterbegin"></div>')
div.click();
this.server.respond();
div.innerText.should.equal("1");
byId("a1").click();
this.server.respond();
div.innerText.should.equal("*");
div.click();
this.server.respond();
div.innerText.should.equal("2*");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("**");
});
it('swap afterend properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterend">*</div>')
var parent = div.parentElement;
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*1");
byId("a1").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*2*");
byId("a2").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("***");
});
it('swap beforeend properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforeend">*</div>')
div.click();
this.server.respond();
div.innerText.should.equal("*1");
byId("a1").click();
this.server.respond();
div.innerText.should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("**2");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("***");
});
it('swap beforeend properly with no initial content', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforeend"></div>')
div.click();
this.server.respond();
div.innerText.should.equal("1");
byId("a1").click();
this.server.respond();
div.innerText.should.equal("*");
div.click();
this.server.respond();
div.innerText.should.equal("*2");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("**");
});
it('properly parses various swap specifications', function(){
var swapSpec = htmx._("getSwapSpecification"); // internal function for swap spec
swapSpec(make("<div/>")).swapStyle.should.equal("innerHTML")
swapSpec(make("<div hx-swap='innerHTML'/>")).swapStyle.should.equal("innerHTML")
swapSpec(make("<div hx-swap='innerHTML'/>")).swapDelay.should.equal(0)
swapSpec(make("<div hx-swap='innerHTML'/>")).settleDelay.should.equal(0) // set to 0 in tests
swapSpec(make("<div hx-swap='innerHTML swap:10'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='innerHTML settle:10'/>")).settleDelay.should.equal(10)
swapSpec(make("<div hx-swap='innerHTML swap:10 settle:11'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='innerHTML swap:10 settle:11'/>")).settleDelay.should.equal(11)
swapSpec(make("<div hx-swap='innerHTML settle:11 swap:10'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='innerHTML settle:11 swap:10'/>")).settleDelay.should.equal(11)
swapSpec(make("<div hx-swap='innerHTML nonsense settle:11 swap:10'/>")).settleDelay.should.equal(11)
swapSpec(make("<div hx-swap='innerHTML nonsense settle:11 swap:10 '/>")).settleDelay.should.equal(11)
})
it('works with a swap delay', function(done) {
this.server.respondWith("GET", "/test", "Clicked!");
var div = make("<div hx-get='/test' hx-swap='innerHTML swap:10ms'></div>");
div.click();
this.server.respond();
div.innerText.should.equal("");
setTimeout(function () {
div.innerText.should.equal("Clicked!");
done();
}, 30);
});
it('works with a settle delay', function(done) {
this.server.respondWith("GET", "/test", "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML settle:10ms'></div>");
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML settle:10ms'></div>");
div.click();
this.server.respond();
div.classList.contains('foo').should.equal(false);
setTimeout(function () {
byId('d1').classList.contains('foo').should.equal(true);
done();
}, 30);
});
it('swap outerHTML properly w/ data-* prefix', function()
{
this.server.respondWith("GET", "/test", '<a id="a1" data-hx-get="/test2">Click Me</a>');
this.server.respondWith("GET", "/test2", "Clicked!");
var div = make('<div id="d1" data-hx-get="/test" data-hx-swap="outerHTML"></div>')
div.click();
should.equal(byId("d1"), div);
this.server.respond();
should.equal(byId("d1"), null);
byId("a1").click();
this.server.respond();
byId("a1").innerHTML.should.equal('Clicked!');
});
it('swap none works properly', function()
{
this.server.respondWith("GET", "/test", 'Ooops, swapped');
var div = make('<div hx-swap="none" hx-get="/test">Foo</div>')
div.click();
this.server.respond();
div.innerHTML.should.equal('Foo');
});
})

View File

@ -0,0 +1,83 @@
describe("hx-target attribute", function(){
beforeEach(function() {
this.server = sinon.fakeServer.create();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('targets an adjacent element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-target="#d1" hx-get="/test">Click Me!</button>')
var div1 = make('<div id="d1"></div>')
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
it('targets a parent element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var div1 = make('<div id="d1"><button id="b1" hx-target="#d1" hx-get="/test">Click Me!</button></div>')
var btn = byId("b1")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
it('targets a `this` element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var div1 = make('<div hx-target="this"><button id="b1" hx-get="/test">Click Me!</button></div>')
var btn = byId("b1")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
it('targets a `closest` element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var div1 = make('<div><p><i><button id="b1" hx-target="closest div" hx-get="/test">Click Me!</button></i></p></div>')
var btn = byId("b1")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
it('targets an inner element properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-target="#d1" hx-get="/test">Click Me!<div id="d1"></div></button>')
var div1 = byId("d1")
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
it('handles bad target gracefully', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-target="bad" hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Click Me!");
});
it('targets an adjacent element properly w/ data-* prefix', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button data-hx-target="#d1" data-hx-get="/test">Click Me!</button>')
var div1 = make('<div id="d1"></div>')
btn.click();
this.server.respond();
div1.innerHTML.should.equal("Clicked!");
});
})

View File

@ -0,0 +1,160 @@
describe("hx-trigger attribute", function(){
beforeEach(function() {
this.server = sinon.fakeServer.create();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('non-default value works', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var form = make('<form hx-get="/test" hx-trigger="click">Click Me!</form>');
form.click();
form.innerHTML.should.equal("Click Me!");
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('changed modifier works', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var input = make('<input hx-trigger="click changed" hx-target="#d1" hx-get="/test" value="foo"/>');
var div = make('<div id="d1"></div>');
input.click();
this.server.respond();
div.innerHTML.should.equal("");
input.click();
this.server.respond();
div.innerHTML.should.equal("");
input.value = "bar";
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
});
it('once modifier works', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var input = make('<input hx-trigger="click once" hx-target="#d1" hx-get="/test" value="foo"/>');
var div = make('<div id="d1"></div>');
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.value = "bar";
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
});
it('polling works', function(complete)
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
if (requests > 5) {
complete();
// cancel polling with a
xhr.respond(286, {}, "Requests: " + requests);
} else {
xhr.respond(200, {}, "Requests: " + requests);
}
});
this.server.autoRespond = true;
this.server.autoRespondAfter = 0;
make('<div hx-trigger="every 10ms" hx-get="/test"/>');
});
it('non-default value works w/ data-* prefix', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var form = make('<form data-hx-get="/test" data-hx-trigger="click">Click Me!</form>');
form.click();
form.innerHTML.should.equal("Click Me!");
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('works with multiple events', function()
{
var requests = 0;
this.server.respondWith("GET", "/test", function (xhr) {
requests++;
xhr.respond(200, {}, "Requests: " + requests);
});
var div = make('<div hx-trigger="load,click" hx-get="/test">Requests: 0</div>');
div.innerHTML.should.equal("Requests: 0");
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
div.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 2");
});
it("parses spec strings", function()
{
var specExamples = {
"": [{trigger: 'click'}],
"every 1s": [{trigger: 'every', pollInterval: 1000}],
"click": [{trigger: 'click'}],
"customEvent": [{trigger: 'customEvent'}],
"event changed": [{trigger: 'event', changed: true}],
"event once": [{trigger: 'event', once: true}],
"event delay:1s": [{trigger: 'event', delay: 1000}],
"event throttle:1s": [{trigger: 'event', throttle: 1000}],
"event changed once delay:1s": [{trigger: 'event', changed: true, once: true, delay: 1000}],
"event1,event2": [{trigger: 'event1'}, {trigger: 'event2'}],
"event1, event2": [{trigger: 'event1'}, {trigger: 'event2'}],
"event1 once, event2 changed": [{trigger: 'event1', once: true}, {trigger: 'event2', changed: true}],
"event1,": [{trigger: 'event1'}],
",event1": [{trigger: 'event1'}],
" ": [{trigger: 'click'}],
",": [{trigger: 'click'}]
}
for (var specString in specExamples) {
var div = make("<div hx-trigger='" + specString + "'></div>");
var spec = htmx._('getTriggerSpecs')(div);
spec.should.deep.equal(specExamples[specString], "Found : " + JSON.stringify(spec) + ", expected : " + JSON.stringify(specExamples[specString]) + " for spec: " + specString);
}
});
it('sets default trigger for forms', function()
{
var form = make('<form></form>');
var spec = htmx._('getTriggerSpecs')(form);
spec.should.deep.equal([{trigger: 'submit'}]);
})
it('sets default trigger for form elements', function()
{
var form = make('<input></input>');
var spec = htmx._('getTriggerSpecs')(form);
spec.should.deep.equal([{trigger: 'change'}]);
})
})

View File

@ -0,0 +1,73 @@
describe("hx-ws attribute", function() {
function mockWebsocket() {
var listener;
var lastSent;
var wasClosed = false;
var mockSocket = {
addEventListener : function(message, l) {
listener = l;
},
write : function(content) {
return listener({data:content});
},
send : function(data) {
lastSent = data;
},
getLastSent : function() {
return lastSent;
},
close : function() {
wasClosed = true;
},
wasClosed : function () {
return wasClosed;
}
};
return mockSocket;
}
beforeEach(function () {
this.server = makeServer();
var socket = mockWebsocket();
this.socket = socket;
clearWorkArea();
htmx.createWebSocket = function(){ return socket };
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles a basic call back', function () {
var div = make('<div hx-ws="connect wss:/foo"><div id="d1">div1</div><div id="d2">div2</div></div>');
this.socket.write("<div id=\"d1\">replaced</div>")
byId("d1").innerHTML.should.equal("replaced");
byId("d2").innerHTML.should.equal("div2");
})
it('handles a basic send', function () {
var div = make('<div hx-ws="connect wss:/foo"><div hx-ws="send" id="d1">div1</div></div>');
byId("d1").click();
var lastSent = this.socket.getLastSent();
var data = JSON.parse(lastSent);
data.HEADERS["HX-Request"].should.equal("true");
})
it('is closed after removal', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ws="connect wss:/foo"></div>');
div.click();
this.server.respond();
this.socket.wasClosed().should.equal(true)
})
it('is closed after removal with no close and activity', function () {
var div = make('<div hx-ws="connect wss:/foo"></div>');
div.parentElement.removeChild(div);
this.socket.write("<div id=\"d1\">replaced</div>")
this.socket.wasClosed().should.equal(true)
})
});

View File

@ -0,0 +1,463 @@
describe("Core htmx AJAX Tests", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
// bootstrap test
it('issues a GET request on click and swaps content', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
});
it('processes inner content properly', function()
{
this.server.respondWith("GET", "/test", '<a hx-get="/test2">Click Me</a>');
this.server.respondWith("GET", "/test2", "Clicked!");
var div = make('<div hx-get="/test"></div>')
div.click();
this.server.respond();
div.innerHTML.should.equal('<a hx-get="/test2">Click Me</a>');
var a = div.querySelector('a');
a.click();
this.server.respond();
a.innerHTML.should.equal('Clicked!');
});
it('handles swap outerHTML properly', function()
{
this.server.respondWith("GET", "/test", '<a id="a1" hx-get="/test2">Click Me</a>');
this.server.respondWith("GET", "/test2", "Clicked!");
var div = make('<div id="d1" hx-get="/test" hx-swap="outerHTML"></div>')
div.click();
should.equal(byId("d1"), div);
this.server.respond();
should.equal(byId("d1"), null);
byId("a1").click();
this.server.respond();
byId("a1").innerHTML.should.equal('Clicked!');
});
it('handles beforebegin properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforebegin">*</div>')
var parent = div.parentElement;
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("1*");
byId("a1").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*2*");
byId("a2").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("***");
});
it('handles afterbegin properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterbegin">*</div>')
div.click();
this.server.respond();
div.innerText.should.equal("1*");
byId("a1").click();
this.server.respond();
div.innerText.should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("2**");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("***");
});
it('handles afterbegin properly with no initial content', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterbegin"></div>')
div.click();
this.server.respond();
div.innerText.should.equal("1");
byId("a1").click();
this.server.respond();
div.innerText.should.equal("*");
div.click();
this.server.respond();
div.innerText.should.equal("2*");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("**");
});
it('handles afterend properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="afterend">*</div>')
var parent = div.parentElement;
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*1");
byId("a1").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("*");
removeWhiteSpace(parent.innerText).should.equal("*2*");
byId("a2").click();
this.server.respond();
removeWhiteSpace(parent.innerText).should.equal("***");
});
it('handles beforeend properly', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforeend">*</div>')
div.click();
this.server.respond();
div.innerText.should.equal("*1");
byId("a1").click();
this.server.respond();
div.innerText.should.equal("**");
div.click();
this.server.respond();
div.innerText.should.equal("**2");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("***");
});
it('handles beforeend properly with no initial content', function()
{
var i = 0;
this.server.respondWith("GET", "/test", function(xhr){
i++;
xhr.respond(200, {}, '<a id="a' + i + '" hx-get="/test2" hx-swap="innerHTML">' + i + '</a>');
});
this.server.respondWith("GET", "/test2", "*");
var div = make('<div hx-get="/test" hx-swap="beforeend"></div>')
div.click();
this.server.respond();
div.innerText.should.equal("1");
byId("a1").click();
this.server.respond();
div.innerText.should.equal("*");
div.click();
this.server.respond();
div.innerText.should.equal("*2");
byId("a2").click();
this.server.respond();
div.innerText.should.equal("**");
});
it('handles hx-target properly', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-target="#s1">Click Me!</button>');
var target = make('<span id="s1">Initial</span>');
btn.click();
target.innerHTML.should.equal("Initial");
this.server.respond();
target.innerHTML.should.equal("Clicked!");
});
it('handles 204 NO CONTENT responses properly', function()
{
this.server.respondWith("GET", "/test", [204, {}, "No Content!"]);
var btn = make('<button hx-get="/test">Click Me!</button>');
btn.click();
btn.innerHTML.should.equal("Click Me!");
this.server.respond();
btn.innerHTML.should.equal("Click Me!");
});
it('handles hx-trigger with non-default value', function()
{
this.server.respondWith("GET", "/test", "Clicked!");
var form = make('<form hx-get="/test" hx-trigger="click">Click Me!</form>');
form.click();
form.innerHTML.should.equal("Click Me!");
this.server.respond();
form.innerHTML.should.equal("Clicked!");
});
it('handles hx-trigger with load event', function()
{
this.server.respondWith("GET", "/test", "Loaded!");
var div = make('<div hx-get="/test" hx-trigger="load">Load Me!</div>');
div.innerHTML.should.equal("Load Me!");
this.server.respond();
div.innerHTML.should.equal("Loaded!");
});
it('sets the content type of the request properly', function (done) {
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "done");
xhr.overriddenMimeType.should.equal("text/html");
done();
});
var div = make('<div hx-get="/test">Click Me!</div>');
div.click();
this.server.respond();
});
it('doesnt issue two requests when clicked twice before response', function()
{
var i = 1;
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, "click " + i);
i++
});
var div = make('<div hx-get="/test"></div>');
div.click();
div.click();
this.server.respond();
div.innerHTML.should.equal("click 1");
});
it('properly handles hx-select for basic situation', function()
{
var i = 1;
this.server.respondWith("GET", "/test", "<div id='d1'>foo</div><div id='d2'>bar</div>");
var div = make('<div hx-get="/test" hx-select="#d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
});
it('properly handles hx-select for full html document situation', function()
{
var i = 1;
this.server.respondWith("GET", "/test", "<html><body><div id='d1'>foo</div><div id='d2'>bar</div></body></html>");
var div = make('<div hx-get="/test" hx-select="#d1"></div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
});
it('properly settles attributes on interior elements', function(done)
{
this.server.respondWith("GET", "/test", "<div hx-get='/test'><div foo='bar' id='d1'></div></div>");
var div = make("<div hx-get='/test' hx-swap='outerHTML settle:10ms'><div id='d1'></div></div>");
div.click();
this.server.respond();
should.equal(byId("d1").getAttribute("foo"), null);
setTimeout(function () {
should.equal(byId("d1").getAttribute("foo"), "bar");
done();
}, 20);
});
it('properly handles checkbox inputs', 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="cb1" name="c1" value="cb1" type="checkbox">'+
'<input id="cb2" name="c1" value="cb2" type="checkbox">'+
'<input id="cb3" name="c1" value="cb3" type="checkbox">'+
'<input id="cb4" name="c2" value="cb4" type="checkbox">'+
'<input id="cb5" name="c2" value="cb5" type="checkbox">'+
'<input id="cb6" name="c3" value="cb6" type="checkbox">'+
'</form>');
form.click();
this.server.respond();
values.should.deep.equal({});
byId("cb1").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:"cb1"});
byId("cb1").checked = true;
byId("cb2").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb2"]});
byId("cb1").checked = true;
byId("cb2").checked = true;
byId("cb3").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb2", "cb3"]});
byId("cb1").checked = true;
byId("cb2").checked = true;
byId("cb3").checked = true;
byId("cb4").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb2", "cb3"], c2:"cb4"});
byId("cb1").checked = true;
byId("cb2").checked = true;
byId("cb3").checked = true;
byId("cb4").checked = true;
byId("cb5").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb2", "cb3"], c2:["cb4", "cb5"]});
byId("cb1").checked = true;
byId("cb2").checked = true;
byId("cb3").checked = true;
byId("cb4").checked = true;
byId("cb5").checked = true;
byId("cb6").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb2", "cb3"], c2:["cb4", "cb5"], c3:"cb6"});
byId("cb1").checked = true;
byId("cb2").checked = false;
byId("cb3").checked = true;
byId("cb4").checked = false;
byId("cb5").checked = true;
byId("cb6").checked = true;
form.click();
this.server.respond();
values.should.deep.equal({c1:["cb1", "cb3"], c2:"cb5", c3:"cb6"});
});
it('text nodes dont screw up settling via variable capture', function()
{
this.server.respondWith("GET", "/test", "<div id='d1' hx-get='/test2'></div>fooo");
this.server.respondWith("GET", "/test2", "clicked");
var div = make("<div hx-get='/test'/>");
div.click();
this.server.respond();
byId("d1").click();
this.server.respond();
byId("d1").innerHTML.should.equal("clicked");
});
var globalWasCalled = false;
window.callGlobal = function() {
globalWasCalled = true;
}
it('script nodes evaluate', function()
{
try {
this.server.respondWith("GET", "/test", "<div></div><script type='text/javascript'>callGlobal()</script>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
globalWasCalled.should.equal(true);
} finally {
delete window.callGlobal;
}
});
it('script node exceptions do not break rendering', function()
{
this.server.respondWith("GET", "/test", "clicked<script type='text/javascript'>throw 'foo';</script>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
div.innerText.should.equal("clicked");
});
it('allows empty verb values', function()
{
var path = null;
var div = make("<div hx-get=''/>");
htmx.on(div, "configRequest.htmx", function (evt) {
path = evt.detail.path;
return false;
});
div.click();
path.should.not.be.null;
});
it('allows blank verb values', function()
{
var path = null;
var div = make("<div hx-get/>");
htmx.on(div, "configRequest.htmx", function (evt) {
path = evt.detail.path;
return false;
});
div.click();
path.should.not.be.null;
});
})

View File

@ -0,0 +1,167 @@
describe("Core htmx API test", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('onLoad is called... onLoad', function(){
// also tests on/off
this.server.respondWith("GET", "/test", "<div id='d1' hx-get='/test'></div>")
var helper = htmx.onLoad(function (elt) {
elt.setAttribute("foo", "bar");
});
try {
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML'></div>");
div.click();
this.server.respond();
byId("d1").getAttribute("foo").should.equal("bar");
} finally {
htmx.off("load.htmx", helper);
}
});
it('triggers properly', function () {
var div = make("<div/>");
var myEventCalled = false;
var detailStr = "";
htmx.on("myEvent", function(evt){
myEventCalled = true;
detailStr = evt.detail.str;
})
htmx.trigger(div, "myEvent", {str:"foo"})
myEventCalled.should.equal(true);
detailStr.should.equal("foo");
});
it('triggers with no details properly', function () {
var div = make("<div/>");
var myEventCalled = false;
htmx.on("myEvent", function(evt){
myEventCalled = true;
})
htmx.trigger(div, "myEvent")
myEventCalled.should.equal(true);
});
it('should find properly', function(){
var div = make("<div id='d1' class='c1 c2'>");
div.should.equal(htmx.find("#d1"));
div.should.equal(htmx.find(".c1"));
div.should.equal(htmx.find(".c2"));
div.should.equal(htmx.find(".c1.c2"));
});
it('should find properly from elt', function(){
var div = make("<div><a id='a1'></a><a id='a2'></a></div>");
htmx.find(div, "a").id.should.equal('a1');
});
it('should find all properly', function(){
var div = make("<div class='c1 c2 c3'><div class='c1 c2'><div class='c1'>");
htmx.findAll(".c1").length.should.equal(3);
htmx.findAll(".c2").length.should.equal(2);
htmx.findAll(".c3").length.should.equal(1);
});
it('should find all properly from elt', function(){
var div = make("<div><div class='c1 c2 c3'><div class='c1 c2'><div class='c1'></div>");
htmx.findAll(div, ".c1").length.should.equal(3);
htmx.findAll(div, ".c2").length.should.equal(2);
htmx.findAll(div,".c3").length.should.equal(1);
});
it('should find closest element properly', function () {
var div = make("<div><a id='a1'></a><a id='a2'></a></div>");
var a = htmx.find(div, "a");
htmx.closest(a, "div").should.equal(div);
});
it('should remove element properly', function () {
var div = make("<div><a></a></div>");
var a = htmx.find(div, "a");
htmx.remove(a);
div.innerHTML.should.equal("");
});
it('should add class properly', function () {
var div = make("<div></div>");
div.classList.contains("foo").should.equal(false);
htmx.addClass(div, "foo");
div.classList.contains("foo").should.equal(true);
});
it('should add class properly after delay', function (done) {
var div = make("<div></div>");
div.classList.contains("foo").should.equal(false);
htmx.addClass(div, "foo", 10);
div.classList.contains("foo").should.equal(false);
setTimeout(function () {
div.classList.contains("foo").should.equal(true);
done();
}, 20);
});
it('should remove class properly', function () {
var div = make("<div></div>");
htmx.addClass(div, "foo");
div.classList.contains("foo").should.equal(true);
htmx.removeClass(div, "foo");
div.classList.contains("foo").should.equal(false);
});
it('should add class properly after delay', function (done) {
var div = make("<div></div>");
htmx.addClass(div, "foo");
div.classList.contains("foo").should.equal(true);
htmx.removeClass(div, "foo", 10);
div.classList.contains("foo").should.equal(true);
setTimeout(function () {
div.classList.contains("foo").should.equal(false);
done();
}, 20);
});
it('should toggle class properly', function () {
var div = make("<div></div>");
div.classList.contains("foo").should.equal(false);
htmx.toggleClass(div, "foo");
div.classList.contains("foo").should.equal(true);
htmx.toggleClass(div, "foo");
div.classList.contains("foo").should.equal(false);
});
it('should take class properly', function () {
var div1 = make("<div></div>");
var div2 = make("<div></div>");
var div3 = make("<div></div>");
div1.classList.contains("foo").should.equal(false);
div2.classList.contains("foo").should.equal(false);
div3.classList.contains("foo").should.equal(false);
htmx.takeClass(div1, "foo");
div1.classList.contains("foo").should.equal(true);
div2.classList.contains("foo").should.equal(false);
div3.classList.contains("foo").should.equal(false);
htmx.takeClass(div2, "foo");
div1.classList.contains("foo").should.equal(false);
div2.classList.contains("foo").should.equal(true);
div3.classList.contains("foo").should.equal(false);
htmx.takeClass(div3, "foo");
div1.classList.contains("foo").should.equal(false);
div2.classList.contains("foo").should.equal(false);
div3.classList.contains("foo").should.equal(true);
});
})

View File

@ -0,0 +1,122 @@
describe("Core htmx Events", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it("load.htmx fires properly", function () {
var called = false;
var handler = htmx.on("load.htmx", function (evt) {
called = true;
});
try {
this.server.respondWith("GET", "/test", "");
this.server.respondWith("GET", "/test", "<div></div>");
var div = make("<div hx-get='/test'></div>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("load.htmx", handler);
}
});
it("configRequest.htmx allows attribute addition", function () {
var handler = htmx.on("configRequest.htmx", function (evt) {
evt.detail.parameters['param'] = "true";
});
try {
var param = null;
this.server.respondWith("POST", "/test", function (xhr) {
param = getParameters(xhr)['param'];
xhr.respond(200, {}, "");
});
var div = make("<div hx-post='/test'></div>");
div.click();
this.server.respond();
param.should.equal("true");
} finally {
htmx.off("configRequest.htmx", handler);
}
});
it("configRequest.htmx allows attribute removal", function () {
var param = "foo";
var handler = htmx.on("configRequest.htmx", function (evt) {
delete evt.detail.parameters['param'];
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
param = getParameters(xhr)['param'];
xhr.respond(200, {}, "");
});
var div = make("<form hx-trigger='click' hx-post='/test'><input name='param' value='foo'></form>");
div.click();
this.server.respond();
should.equal(param, undefined);
} finally {
htmx.off("configRequest.htmx", handler);
}
});
it("configRequest.htmx allows header tweaking", function () {
var header = "foo";
var handler = htmx.on("configRequest.htmx", function (evt) {
evt.detail.headers['X-My-Header'] = "bar";
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
header = xhr.requestHeaders['X-My-Header'];
xhr.respond(200, {}, "");
});
var div = make("<form hx-trigger='click' hx-post='/test'><input name='param' value='foo'></form>");
div.click();
this.server.respond();
should.equal(header, "bar");
} finally {
htmx.off("configRequest.htmx", handler);
}
});
it("afterSwap.htmx is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("afterSwap.htmx", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("afterSwap.htmx", handler);
}
});
it("afterSettle.htmx is called when replacing outerHTML", function () {
var called = false;
var handler = htmx.on("afterSettle.htmx", function (evt) {
called = true;
});
try {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
});
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>");
div.click();
this.server.respond();
should.equal(called, true);
} finally {
htmx.off("afterSettle.htmx", handler);
}
});
});

View File

@ -0,0 +1,110 @@
describe("Core htmx AJAX headers", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it("should include the HX-Request header", function(){
this.server.respondWith("GET", "/test", function(xhr){
xhr.requestHeaders['HX-Request'].should.be.equal('true');
xhr.respond(200, {}, "");
});
var div = make('<div hx-get="/test"></div>');
div.click();
this.server.respond();
})
it("should include the HX-Trigger header", function(){
this.server.respondWith("GET", "/test", function(xhr){
xhr.requestHeaders['HX-Trigger'].should.equal('d1');
xhr.respond(200, {}, "");
});
var div = make('<div id="d1" hx-get="/test"></div>');
div.click();
this.server.respond();
})
it("should include the HX-Trigger-Name header", function(){
this.server.respondWith("GET", "/test", function(xhr){
xhr.requestHeaders['HX-Trigger-Name'].should.equal('n1');
xhr.respond(200, {}, "");
});
var div = make('<button name="n1" hx-get="/test"></button>');
div.click();
this.server.respond();
})
it("should include the HX-Target header", function(){
this.server.respondWith("GET", "/test", function(xhr){
xhr.requestHeaders['HX-Target'].should.equal('d1');
xhr.respond(200, {}, "");
});
var div = make('<div hx-target="#d1" hx-get="/test"></div><div id="d1" ></div>');
div.click();
this.server.respond();
})
it("should handle simple string HX-Trigger response header properly", function(){
this.server.respondWith("GET", "/test", [200, {"HX-Trigger" : "foo"}, ""]);
var div = make('<div hx-get="/test"></div>');
var invokedEvent = false;
div.addEventListener("foo", function (evt) {
invokedEvent = true;
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
})
it("should handle basic JSON HX-Trigger response header properly", function(){
this.server.respondWith("GET", "/test", [200, {"HX-Trigger" : "{\"foo\":null}"}, ""]);
var div = make('<div hx-get="/test"></div>');
var invokedEvent = false;
div.addEventListener("foo", function (evt) {
invokedEvent = true;
should.equal(null, evt.detail.value);
evt.detail.elt.should.equal(div);
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
})
it("should handle JSON with array arg HX-Trigger response header properly", function(){
this.server.respondWith("GET", "/test", [200, {"HX-Trigger" : "{\"foo\":[1, 2, 3]}"}, ""]);
var div = make('<div hx-get="/test"></div>');
var invokedEvent = false;
div.addEventListener("foo", function (evt) {
invokedEvent = true;
evt.detail.elt.should.equal(div);
evt.detail.value.should.deep.equal([1, 2, 3]);
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
})
it("should handle JSON with array arg HX-Trigger response header properly", function(){
this.server.respondWith("GET", "/test", [200, {"HX-Trigger" : "{\"foo\":{\"a\":1, \"b\":2}}"}, ""]);
var div = make('<div hx-get="/test"></div>');
var invokedEvent = false;
div.addEventListener("foo", function (evt) {
invokedEvent = true;
evt.detail.elt.should.equal(div);
evt.detail.a.should.equal(1);
evt.detail.b.should.equal(2);
});
div.click();
this.server.respond();
invokedEvent.should.equal(true);
})
});

View File

@ -0,0 +1,23 @@
describe("Core htmx internals Tests", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it("makeFragment works with janky stuff", function(){
htmx._("makeFragment")("<html></html>").tagName.should.equal("BODY");
htmx._("makeFragment")("<html><body></body></html>").tagName.should.equal("BODY");
//NB - the tag name should be the *parent* element hosting the HTML since we use the fragment children
// for the swap
htmx._("makeFragment")("<td></td>").tagName.should.equal("TR");
htmx._("makeFragment")("<thead></thead>").tagName.should.equal("TABLE");
htmx._("makeFragment")("<col></col>").tagName.should.equal("COLGROUP");
htmx._("makeFragment")("<tr></tr>").tagName.should.equal("TBODY");
})
});

View File

@ -0,0 +1,121 @@
describe("Core htmx Parameter Handling", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('Input includes value', function () {
var input = make('<input name="foo" value="bar"/>');
var vals = htmx._('getInputValues')(input);
vals['foo'].should.equal('bar');
})
it('Input includes value on get', function () {
var input = make('<input name="foo" value="bar"/>');
var vals = htmx._('getInputValues')(input, "get");
vals['foo'].should.equal('bar');
})
it('Input includes form', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/></form>');
var input = byId('i1');
var vals = htmx._('getInputValues')(input);
vals['foo'].should.equal('bar');
vals['do'].should.equal('rey');
})
it('Input doesnt include form on get', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/></form>');
var input = byId('i1');
var vals = htmx._('getInputValues')(input, 'get');
vals['foo'].should.equal('bar');
should.equal(vals['do'], undefined);
})
it('non-input includes form', function () {
var form = make('<form><div id="d1"/><input id="i2" name="do" value="rey"/></form>');
var div = byId('d1');
var vals = htmx._('getInputValues')(div, "post");
vals['do'].should.equal('rey');
})
it('non-input doesnt include form on get', function () {
var form = make('<form><div id="d1"/><input id="i2" name="do" value="rey"/></form>');
var div = byId('d1');
var vals = htmx._('getInputValues')(div, "get");
should.equal(vals['do'], undefined);
})
it('Basic form works on get', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/></form>');
var vals = htmx._('getInputValues')(form, 'get');
vals['foo'].should.equal('bar');
vals['do'].should.equal('rey');
})
it('Basic form works on non-get', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/></form>');
var vals = htmx._('getInputValues')(form, 'post');
vals['foo'].should.equal('bar');
vals['do'].should.equal('rey');
})
it('Double values are included as array', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>');
var vals = htmx._('getInputValues')(form);
vals['foo'].should.equal('bar');
vals['do'].should.deep.equal(['rey', 'rey']);
})
it('Double values are included as array in correct order', function () {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey1"/><input id="i3" name="do" value="rey2"/></form>');
var vals = htmx._('getInputValues')(byId("i3"));
vals['foo'].should.equal('bar');
vals['do'].should.deep.equal(['rey1', 'rey2']);
})
it('hx-include works with form', function () {
var form = make('<form id="f1"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>');
var div = make('<div hx-include="#f1"></div>');
var vals = htmx._('getInputValues')(div);
vals['foo'].should.equal('bar');
vals['do'].should.deep.equal(['rey', 'rey']);
})
it('hx-include works with input', function () {
var form = make('<form id="f1"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>');
var div = make('<div hx-include="#i1"></div>');
var vals = htmx._('getInputValues')(div);
vals['foo'].should.equal('bar');
should.equal(vals['do'], undefined);
})
it('hx-include works with two inputs', function () {
var form = make('<form id="f1"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>');
var div = make('<div hx-include="#i1, #i2"></div>');
var vals = htmx._('getInputValues')(div);
vals['foo'].should.equal('bar');
vals['do'].should.deep.equal(['rey', 'rey']);
})
it('hx-include works with two inputs, plus form', function () {
var form = make('<form id="f1"><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>');
var div = make('<div hx-include="#i1, #i2, #f1"></div>');
var vals = htmx._('getInputValues')(div);
vals['foo'].should.equal('bar');
vals['do'].should.deep.equal(['rey', 'rey']);
})
it('correctly URL escapes values', function () {
htmx._("urlEncode")({}).should.equal("");
htmx._("urlEncode")({"foo": "bar"}).should.equal("foo=bar");
htmx._("urlEncode")({"foo": "bar", "do" : "rey"}).should.equal("foo=bar&do=rey");
htmx._("urlEncode")({"foo": "bar", "do" : ["rey", "blah"]}).should.equal("foo=bar&do=rey&do=blah");
});
});

View File

@ -0,0 +1,70 @@
describe("Core htmx perf Tests", function() {
var HTMX_HISTORY_CACHE_NAME = "htmx-history-cache";
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
afterEach(function () {
this.server.restore();
clearWorkArea();
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME);
});
function stringRepeat(str, num) {
num = Number(num);
var result = '';
while (true) {
if (num & 1) { // (1)
result += str;
}
num >>>= 1; // (2)
if (num <= 0) break;
str += str;
}
return result;
}
it("DOM processing should be fast", function(){
this.server.respondWith("GET", "/test", "Clicked!");
// create an entry with a large content string (256k) and see how fast we can write and read it
// to local storage as a single entry
var str = stringRepeat("<div>", 30) + stringRepeat("<div><div><span><button hx-get='/test'> Test Get Button </button></span></div></div>\n", 1000) + stringRepeat("</div>", 30);
var start = performance.now();
var stuff = make(str);
var end = performance.now();
var timeInMs = end - start;
// make sure the DOM actually processed
var firstBtn = stuff.querySelector("button");
firstBtn.click();
this.server.respond();
firstBtn.innerHTML.should.equal("Clicked!");
chai.assert(timeInMs < 100, "Should take less than 100ms on most platforms, took: " + timeInMs + "ms");
})
it("history implementation should be fast", function(){
// create an entry with a large content string (256k) and see how fast we can write and read it
// to local storage as a single entry
var entry = {url: stringRepeat("x", 32), content:stringRepeat("x", 256*1024)}
var array = [];
for (var i = 0; i < 10; i++) {
array.push(entry);
}
var start = performance.now();
var string = JSON.stringify(array);
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, string);
var reReadString = localStorage.getItem(HTMX_HISTORY_CACHE_NAME);
var finalJson = JSON.parse(reReadString);
var end = performance.now();
var timeInMs = end - start;
chai.assert(timeInMs < 300, "Should take less than 300ms on most platforms");
})
})

View File

@ -0,0 +1,68 @@
describe("Core htmx Regression Tests", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('SVGs process properly in IE11', function()
{
var btn = make('<svg onclick="document.getElementById(\'contents\').classList.toggle(\'show\')" class="hamburger" viewBox="0 0 100 80" width="25" height="25" style="margin-bottom:-5px">\n' +
'<rect width="100" height="20" style="fill:rgb(52, 101, 164)" rx="10"></rect>\n' +
'<rect y="30" width="100" height="20" style="fill:rgb(52, 101, 164)" rx="10"></rect>\n' +
'<rect y="60" width="100" height="20" style="fill:rgb(52, 101, 164)" rx="10"></rect>\n' +
'</svg>')
});
it ('Handles https://github.com/bigskysoftware/htmx/issues/4 properly', function() {
this.server.respondWith("GET", "/index2a.php",
"<div id='message' hx-swap-oob='true'>I came from message oob swap I should be second</div>" +
"<div id='message2' hx-swap-oob='true'>I came from a message2 oob swap I should be third but I am in the wrong spot</div>" +
"I'm page2 content (non-swap) I should be first")
var h1 = make("<h1 hx-get='/index2a.php' hx-target='#page2' hx-trigger='click'>Kutty CLICK ME</h1>" +
"<div id='page2' ></div>" +
"<div id='message'></div>" +
"<div id='message2'></div>")
h1.click();
this.server.respond();
htmx.find("#page2").innerHTML.should.equal("I'm page2 content (non-swap) I should be first")
htmx.find("#message").innerHTML.should.equal("I came from message oob swap I should be second")
htmx.find("#message2").innerHTML.should.equal("I came from a message2 oob swap I should be third but I am in the wrong spot")
});
it ('Handles https://github.com/bigskysoftware/htmx/issues/33 "empty values" properly', function() {
this.server.respondWith("POST", "/htmx.php", function (xhr) {
xhr.respond(200, {}, xhr.requestBody);
});
var form = make('<form hx-trigger="click" hx-post="/htmx.php">\n' +
'<input type="text" name="variable" value="">\n' +
'<button type="submit">Submit</button>\n' +
'</form>')
form.click();
this.server.respond();
form.innerHTML.should.equal("variable=")
});
it ('name=id doesnt cause an error', function(){
this.server.respondWith("GET", "/test", "Foo<form><input name=\"id\"/></form>")
var div = make('<div hx-get="/test">Get It</div>')
div.click();
this.server.respond();
div.innerText.should.contain("Foo")
});
it ('empty id doesnt cause an error', function(){
this.server.respondWith("GET", "/test", "Foo\n<div id=''></div>")
var div = make('<div hx-get="/test">Get It</div>')
div.click();
this.server.respond();
div.innerText.should.contain("Foo")
});
})

View File

@ -0,0 +1,44 @@
describe("Core htmx AJAX Verbs", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles basic posts properly', function () {
this.server.respondWith("POST", "/test", "post");
var div = make('<div hx-post="/test">click me</div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("post");
})
it('handles basic put properly', function () {
this.server.respondWith("PUT", "/test", "put");
var div = make('<div hx-put="/test">click me</div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("put");
})
it('handles basic patch properly', function () {
this.server.respondWith("PATCH", "/test", "patch");
var div = make('<div hx-patch="/test">click me</div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("patch");
})
it('handles basic delete properly', function () {
this.server.respondWith("DELETE", "/test", "delete");
var div = make('<div hx-delete="/test">click me</div>');
div.click();
this.server.respond();
div.innerHTML.should.equal("delete");
})
});

View File

@ -0,0 +1,21 @@
describe("ajax-header extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('Sends the X-Requested-With header', function () {
this.server.respondWith("GET", "/test", function (xhr) {
xhr.respond(200, {}, xhr.requestHeaders['X-Requested-With'])
});
var btn = make('<button hx-get="/test" hx-ext="ajax-header">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("XMLHttpRequest");
});
});

View File

@ -0,0 +1,27 @@
describe("bad extension", function() {
htmx.defineExtension("bad-extension", {
onEvent : function(name, evt) {throw "onEvent"},
transformResponse : function(text, xhr, elt) {throw "transformRequest"},
isInlineSwap : function(swapStyle) {throw "isInlineSwap"},
handleSwap : function(swapStyle, target, fragment, settleInfo) {throw "handleSwap"},
encodeParameters : function(xhr, parameters, elt) {throw "encodeParmeters"}
}
)
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('does not blow up rendering', function () {
this.server.respondWith("GET", "/test", "clicked!");
var div = make('<div hx-get="/test" hx-ext="bad-extension">Click Me!</div>')
div.click();
this.server.respond();
div.innerHTML.should.equal("clicked!");
});
});

View File

@ -0,0 +1,55 @@
describe("class-tools extension", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('adds classes properly', function(done)
{
var div = make('<div hx-ext="class-tools" classes="add c1">Click Me!</div>')
should.equal(div.classList.length, 0);
setTimeout(function(){
should.equal(div.classList.contains("c1"), true);
done();
}, 100);
});
it('removes classes properly', function(done)
{
var div = make('<div class="foo bar" hx-ext="class-tools" classes="remove bar">Click Me!</div>')
should.equal(div.classList.contains("foo"), true);
should.equal(div.classList.contains("bar"), true);
setTimeout(function(){
should.equal(div.classList.contains("foo"), true);
should.equal(div.classList.contains("bar"), false);
done();
}, 100);
});
it('adds classes properly w/ data-* prefix', function(done)
{
var div = make('<div hx-ext="class-tools" data-classes="add c1">Click Me!</div>')
should.equal(div.classList.length, 0);
setTimeout(function(){
should.equal(div.classList.contains("c1"), true);
done();
}, 100);
});
it('extension can be on parent', function(done)
{
var div = make('<div hx-ext="class-tools"><div id="d1" classes="add c1">Click Me!</div></div>')
should.equal(div.classList.length, 0);
setTimeout(function(){
should.equal(div.classList.contains("c1"), false);
should.equal(byId("d1").classList.contains("c1"), true);
done();
}, 100);
});
})

View File

@ -0,0 +1,30 @@
describe("client-side-templates extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('works on basic mustache template', function () {
this.server.respondWith("GET", "/test", '{"foo":"bar"}');
var btn = make('<button hx-get="/test" hx-ext="client-side-templates" mustache-template="mt1">Click Me!</button>')
make('<script id="mt1" type="x-tmpl-mustache">*{{foo}}*</script>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("*bar*");
});
it('works on basic handlebars template', function () {
this.server.respondWith("GET", "/test", '{"foo":"bar"}');
var btn = make('<button hx-get="/test" hx-ext="client-side-templates" handlebars-template="hb1">Click Me!</button>')
Handlebars.partials["hb1"] = Handlebars.compile("*{{foo}}*");
btn.click();
this.server.respond();
btn.innerHTML.should.equal("*bar*");
});
});

View File

@ -0,0 +1,19 @@
describe("debug extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('works on basic request', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button hx-get="/test" hx-ext="debug">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Clicked!");
});
});

View File

@ -0,0 +1,37 @@
describe("hyperscript integration", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('can trigger with a custom event', function () {
this.server.respondWith("GET", "/test", "Custom Event Sent!");
var btn = make('<button _="on click send customEvent" hx-trigger="customEvent" hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
btn.innerHTML.should.equal("Custom Event Sent!");
});
it('can handle htmx driven events', function () {
this.server.respondWith("GET", "/test", "Clicked!");
var btn = make('<button _="on afterSettle.htmx add .afterSettle" hx-get="/test">Click Me!</button>')
btn.classList.contains("afterSettle").should.equal(false);
btn.click();
this.server.respond();
btn.classList.contains("afterSettle").should.equal(true);
});
it('can handle htmx error events', function () {
this.server.respondWith("GET", "/test", [404, {}, "Bad request"]);
var div = make('<div id="d1"></div>')
var btn = make('<button _="on error.htmx(errorInfo) put errorInfo.error into #d1.innerHTML" hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
div.innerHTML.should.equal("Response Status Error Code 404 from /test");
});
});

View File

@ -0,0 +1,23 @@
describe("include-vals extension", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('Includes values properly', function () {
var params = {};
this.server.respondWith("POST", "/test", function (xhr) {
params = getParameters(xhr);
xhr.respond(200, {}, "clicked");
});
var btn = make('<button hx-post="/test" hx-ext="include-vals" include-vals="foo:\'bar\'">Click Me!</button>')
btn.click();
this.server.respond();
params['foo'].should.equal("bar");
});
});

Some files were not shown because too many files have changed in this diff Show More