lots of work:

* reworked the swap code did away with merge in favor of general attributes swapping
* updated docs
* changed hx-swap-direct to hx-swap-oob
* updated build script to create a .gz so I can know what size that is
* cleaned up code so it's easier to follow
This commit is contained in:
carson 2020-05-04 17:55:19 -07:00
parent 8be1f10636
commit fe9dbb8b3e
17 changed files with 1653 additions and 546 deletions

255
dist/htmx.js vendored
View File

@ -123,7 +123,6 @@ var HTMx = HTMx || (function () {
function getTarget(elt) {
var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null});
if (explicitTarget) {
var targetStr = getRawAttribute(explicitTarget, "hx-target");
if (targetStr === "this") {
@ -141,59 +140,6 @@ var HTMx = HTMx || (function () {
}
}
function directSwap(child) {
var swapDirect = getAttributeValue(child, 'hx-swap-direct');
if (swapDirect) {
var target = getDocument().getElementById(getRawAttribute(child,'id'));
if (target) {
if (swapDirect === "merge") {
mergeInto(target, child);
} else {
var newParent = parentElt(target);
newParent.insertBefore(child, target);
newParent.removeChild(target);
return true;
}
}
}
return false;
}
function processResponseNodes(parentNode, insertBefore, text, executeAfter, selector) {
var fragment = makeFragment(text);
var nodesToProcess;
if (selector) {
nodesToProcess = toArray(fragment.querySelectorAll(selector));
} else {
nodesToProcess = toArray(fragment.childNodes);
}
forEach(nodesToProcess, function(child){
if (!directSwap(child)) {
parentNode.insertBefore(child, insertBefore);
}
if (child.nodeType !== Node.TEXT_NODE) {
triggerEvent(child, 'load.hx', {parent:parentElt(child)});
processNode(child);
}
});
if(executeAfter) {
executeAfter.call();
}
}
function findMatch(elt, possible) {
for (var i = 0; i < possible.length; i++) {
var candidate = possible[i];
if (elt.hasAttribute("id") && elt.id === candidate.id) {
return candidate;
}
if (!candidate.hasAttribute("id") && elt.tagName === candidate.tagName) {
return candidate;
}
}
return null;
}
function cloneAttributes(mergeTo, mergeFrom) {
forEach(mergeTo.attributes, function (attr) {
if (!mergeFrom.hasAttribute(attr.name)) {
@ -205,58 +151,119 @@ var HTMx = HTMx || (function () {
});
}
function mergeChildren(mergeTo, mergeFrom) {
var oldChildren = toArray(mergeTo.children);
var marker = getDocument().createElement("span");
mergeTo.insertBefore(marker, mergeTo.firstChild);
forEach(mergeFrom.childNodes, function (newChild) {
var match = findMatch(newChild, oldChildren);
if (match) {
while (marker.nextSibling && marker.nextSibling !== match) {
mergeTo.removeChild(marker.nextSibling);
}
mergeTo.insertBefore(marker, match.nextSibling);
mergeInto(match, newChild);
function handleOutOfBandSwaps(fragment) {
forEach(fragment.children, function(child){
if (getAttributeValue(child, "hx-swap-oob") === "true") {
var target = getDocument().getElementById(child.id);
if (target) {
var fragment = new DocumentFragment()
fragment.append(child);
swapOuterHTML(target, fragment);
} else {
mergeTo.insertBefore(newChild, marker);
child.parentNode.removeChild(child);
triggerEvent(getDocument().body, "oobErrorNoTarget.hx", {id:child.id, content:child})
}
}
})
}
function handleAttributes(parentNode, fragment) {
var attributeSwaps = [];
forEach(fragment.querySelectorAll("[id]"), function (newNode) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]")
if (oldNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
attributeSwaps.push(function () {
cloneAttributes(newNode, newAttributes);
});
}
});
while (marker.nextSibling) {
mergeTo.removeChild(marker.nextSibling);
}
mergeTo.removeChild(marker);
setTimeout(function () {
forEach(attributeSwaps, function (swap) {
swap.call();
});
}, 100);
}
function mergeInto(mergeTo, mergeFrom) {
cloneAttributes(mergeTo, mergeFrom);
mergeChildren(mergeTo, mergeFrom);
function insertNodesBefore(parentNode, insertBefore, fragment) {
handleAttributes(parentNode, fragment);
while(fragment.childNodes.length > 0){
var child = fragment.firstChild;
parentNode.insertBefore(child, insertBefore);
if (child.nodeType !== Node.TEXT_NODE) {
triggerEvent(child, 'load.hx', {elt:child, parent:parentElt(child)});
processNode(child);
}
}
}
function mergeResponse(target, resp, selector) {
var fragment = makeFragment(resp);
mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild);
function swapOuterHTML(target, fragment) {
if (target.tagName === "BODY") {
swapInnerHTML(target, fragment);
} else {
insertNodesBefore(parentElt(target), target, fragment);
parentElt(target).removeChild(target);
}
}
function swapResponse(target, elt, resp, after) {
function swapPrepend(target, fragment) {
insertNodesBefore(target, target.firstChild, fragment);
}
function swapPrependBefore(target, fragment) {
insertNodesBefore(parentElt(target), target, fragment);
}
function swapAppend(target, fragment) {
insertNodesBefore(target, null, fragment);
}
function swapAppendAfter(target, fragment) {
insertNodesBefore(parentElt(target), target.nextSibling, fragment);
}
function swapInnerHTML(target, fragment) {
target.innerHTML = "";
insertNodesBefore(target, null, fragment);
}
function maybeSelectFromResponse(elt, fragment) {
var selector = getClosestAttributeValue(elt, "hx-select");
if (selector) {
var newFragment = new DocumentFragment();
forEach(fragment.querySelectorAll(selector), function (node) {
newFragment.append(node);
});
fragment = newFragment;
}
return fragment;
}
function swapResponse(target, elt, responseText, callBack) {
var fragment = makeFragment(responseText);
handleOutOfBandSwaps(fragment);
fragment = maybeSelectFromResponse(elt, fragment);
var swapStyle = getClosestAttributeValue(elt, "hx-swap");
var selector = getClosestAttributeValue(elt, "hx-select");
if (swapStyle === "merge") {
mergeResponse(target, resp, selector);
} else if (swapStyle === "outerHTML") {
processResponseNodes(parentElt(target), target, resp, after, selector);
parentElt(target).removeChild(target);
if (swapStyle === "outerHTML") {
swapOuterHTML(target, fragment);
} else if (swapStyle === "prepend") {
processResponseNodes(target, target.firstChild, resp, after, selector);
swapPrepend(target, fragment);
} else if (swapStyle === "prependBefore") {
processResponseNodes(parentElt(target), target, resp, after, selector);
swapPrependBefore(target, fragment);
} else if (swapStyle === "append") {
processResponseNodes(target, null, resp, after, selector);
swapAppend(target, fragment);
} else if (swapStyle === "appendAfter") {
processResponseNodes(parentElt(target), target.nextSibling, resp, after, selector);
swapAppendAfter(target, fragment);
} else {
target.innerHTML = "";
processResponseNodes(target, null, resp, after, selector);
swapInnerHTML(target, fragment);
}
if(callBack) {
callBack.call();
}
}
@ -423,9 +430,12 @@ var HTMx = HTMx || (function () {
}
function initSSESource(elt, sseSrc) {
var config = {withCredentials: true};
triggerEvent(elt, "initSSE.mx", config)
var source = new EventSource(sseSrc);
var details = {
initializer: function() { new EventSource(sseSrc, details.config) },
config:{withCredentials: true}
};
triggerEvent(elt, "initSSE.mx", {config:details})
var source = details.initializer();
source.onerror = function (e) {
triggerEvent(elt, "sseError.mx", {error:e, source:source});
maybeCloseSSESource(elt);
@ -433,21 +443,10 @@ var HTMx = HTMx || (function () {
getInternalData(elt).sseSource = source;
}
function processNode(elt) {
var nodeData = getInternalData(elt);
if (!nodeData.processed) {
nodeData.processed = true;
var trigger = getTrigger(elt);
var explicitAction = false;
forEach(VERBS, function(verb){
var path = getAttributeValue(elt, 'hx-' + verb);
if (path) {
nodeData.path = path;
nodeData.verb = verb;
explicitAction = true;
if (trigger.indexOf("sse:") === 0) {
var sseEventName = trigger.substr(4);
var sseSource = getClosestMatch(elt, function(parent) {return parent.sseSource;});
function processSSETrigger(sseEventName, elt, verb, path) {
var sseSource = getClosestMatch(elt, function (parent) {
return parent.sseSource;
});
if (sseSource) {
var sseListener = function () {
if (!maybeCloseSSESource(sseSource)) {
@ -462,14 +461,30 @@ var HTMx = HTMx || (function () {
} else {
triggerEvent(elt, "noSSESourceError.mx")
}
} if (trigger === 'revealed') {
initScrollHandler();
maybeReveal(elt);
} else if (trigger === 'load') {
}
function loadImmediately(nodeData, elt, verb, path) {
if (!nodeData.loaded) {
nodeData.loaded = true;
issueAjaxRequest(elt, verb, path);
}
}
function processVerbs(elt, nodeData, trigger) {
var explicitAction = false;
forEach(VERBS, function (verb) {
var path = getAttributeValue(elt, 'hx-' + verb);
if (path) {
explicitAction = true;
nodeData.path = path;
nodeData.verb = verb;
if (trigger.indexOf("sse:") === 0) {
processSSETrigger(trigger.substr(4), elt, verb, path);
} else if (trigger === 'revealed') {
initScrollHandler();
maybeReveal(elt);
} else if (trigger === 'load') {
loadImmediately(nodeData, elt, verb, path);
} else if (trigger.trim().indexOf('every ') === 0) {
nodeData.polling = true;
processPolling(elt, verb, path);
@ -478,6 +493,17 @@ var HTMx = HTMx || (function () {
}
}
});
return explicitAction;
}
function processNode(elt) {
var nodeData = getInternalData(elt);
if (!nodeData.processed) {
nodeData.processed = true;
var trigger = getTrigger(elt);
var explicitAction = processVerbs(elt, nodeData, trigger);
if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") {
boostElement(elt, nodeData, trigger);
}
@ -612,8 +638,7 @@ var HTMx = HTMx || (function () {
var historyKey = data['hx-history-key'];
var content = localStorage.getItem('hx-history-' + historyKey);
var elt = getHistoryElement();
elt.innerHTML = "";
processResponseNodes(elt, null, content);
swapInnerHTML(elt, makeFragment(content));
}
function shouldPush(elt) {
@ -895,7 +920,7 @@ var HTMx = HTMx || (function () {
return {
processElement: processNode,
on: addHTMxEventListener,
version: "0.0.1",
version: "0.0.2",
_:internalEval
}
}

2
dist/htmx.min.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/htmx.min.js.gz vendored Normal file

Binary file not shown.

View File

@ -21,7 +21,7 @@
"unpkg": "dist/htmx.min.js",
"scripts": {
"test": "mocha-chrome test/index.html",
"dist": "cp src/htmx.js dist/ && npm run-script uglify && exit",
"dist": "cp src/htmx.js dist/ && npm run-script uglify && gzip -k -f dist/htmx.min.js > dist/htmx.min.js.gz && exit",
"www": "node scripts/www.js",
"uglify": "uglifyjs -m eval -o dist/htmx.min.js dist/htmx.js"
},

View File

@ -123,7 +123,6 @@ var HTMx = HTMx || (function () {
function getTarget(elt) {
var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null});
if (explicitTarget) {
var targetStr = getRawAttribute(explicitTarget, "hx-target");
if (targetStr === "this") {
@ -141,59 +140,6 @@ var HTMx = HTMx || (function () {
}
}
function directSwap(child) {
var swapDirect = getAttributeValue(child, 'hx-swap-direct');
if (swapDirect) {
var target = getDocument().getElementById(getRawAttribute(child,'id'));
if (target) {
if (swapDirect === "merge") {
mergeInto(target, child);
} else {
var newParent = parentElt(target);
newParent.insertBefore(child, target);
newParent.removeChild(target);
return true;
}
}
}
return false;
}
function processResponseNodes(parentNode, insertBefore, text, executeAfter, selector) {
var fragment = makeFragment(text);
var nodesToProcess;
if (selector) {
nodesToProcess = toArray(fragment.querySelectorAll(selector));
} else {
nodesToProcess = toArray(fragment.childNodes);
}
forEach(nodesToProcess, function(child){
if (!directSwap(child)) {
parentNode.insertBefore(child, insertBefore);
}
if (child.nodeType !== Node.TEXT_NODE) {
triggerEvent(child, 'load.hx', {parent:parentElt(child)});
processNode(child);
}
});
if(executeAfter) {
executeAfter.call();
}
}
function findMatch(elt, possible) {
for (var i = 0; i < possible.length; i++) {
var candidate = possible[i];
if (elt.hasAttribute("id") && elt.id === candidate.id) {
return candidate;
}
if (!candidate.hasAttribute("id") && elt.tagName === candidate.tagName) {
return candidate;
}
}
return null;
}
function cloneAttributes(mergeTo, mergeFrom) {
forEach(mergeTo.attributes, function (attr) {
if (!mergeFrom.hasAttribute(attr.name)) {
@ -205,58 +151,119 @@ var HTMx = HTMx || (function () {
});
}
function mergeChildren(mergeTo, mergeFrom) {
var oldChildren = toArray(mergeTo.children);
var marker = getDocument().createElement("span");
mergeTo.insertBefore(marker, mergeTo.firstChild);
forEach(mergeFrom.childNodes, function (newChild) {
var match = findMatch(newChild, oldChildren);
if (match) {
while (marker.nextSibling && marker.nextSibling !== match) {
mergeTo.removeChild(marker.nextSibling);
}
mergeTo.insertBefore(marker, match.nextSibling);
mergeInto(match, newChild);
function handleOutOfBandSwaps(fragment) {
forEach(fragment.children, function(child){
if (getAttributeValue(child, "hx-swap-oob") === "true") {
var target = getDocument().getElementById(child.id);
if (target) {
var fragment = new DocumentFragment()
fragment.append(child);
swapOuterHTML(target, fragment);
} else {
mergeTo.insertBefore(newChild, marker);
child.parentNode.removeChild(child);
triggerEvent(getDocument().body, "oobErrorNoTarget.hx", {id:child.id, content:child})
}
}
})
}
function handleAttributes(parentNode, fragment) {
var attributeSwaps = [];
forEach(fragment.querySelectorAll("[id]"), function (newNode) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]")
if (oldNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
attributeSwaps.push(function () {
cloneAttributes(newNode, newAttributes);
});
}
});
while (marker.nextSibling) {
mergeTo.removeChild(marker.nextSibling);
}
mergeTo.removeChild(marker);
setTimeout(function () {
forEach(attributeSwaps, function (swap) {
swap.call();
});
}, 100);
}
function mergeInto(mergeTo, mergeFrom) {
cloneAttributes(mergeTo, mergeFrom);
mergeChildren(mergeTo, mergeFrom);
function insertNodesBefore(parentNode, insertBefore, fragment) {
handleAttributes(parentNode, fragment);
while(fragment.childNodes.length > 0){
var child = fragment.firstChild;
parentNode.insertBefore(child, insertBefore);
if (child.nodeType !== Node.TEXT_NODE) {
triggerEvent(child, 'load.hx', {elt:child, parent:parentElt(child)});
processNode(child);
}
}
}
function mergeResponse(target, resp, selector) {
var fragment = makeFragment(resp);
mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild);
function swapOuterHTML(target, fragment) {
if (target.tagName === "BODY") {
swapInnerHTML(target, fragment);
} else {
insertNodesBefore(parentElt(target), target, fragment);
parentElt(target).removeChild(target);
}
}
function swapResponse(target, elt, resp, after) {
function swapPrepend(target, fragment) {
insertNodesBefore(target, target.firstChild, fragment);
}
function swapPrependBefore(target, fragment) {
insertNodesBefore(parentElt(target), target, fragment);
}
function swapAppend(target, fragment) {
insertNodesBefore(target, null, fragment);
}
function swapAppendAfter(target, fragment) {
insertNodesBefore(parentElt(target), target.nextSibling, fragment);
}
function swapInnerHTML(target, fragment) {
target.innerHTML = "";
insertNodesBefore(target, null, fragment);
}
function maybeSelectFromResponse(elt, fragment) {
var selector = getClosestAttributeValue(elt, "hx-select");
if (selector) {
var newFragment = new DocumentFragment();
forEach(fragment.querySelectorAll(selector), function (node) {
newFragment.append(node);
});
fragment = newFragment;
}
return fragment;
}
function swapResponse(target, elt, responseText, callBack) {
var fragment = makeFragment(responseText);
handleOutOfBandSwaps(fragment);
fragment = maybeSelectFromResponse(elt, fragment);
var swapStyle = getClosestAttributeValue(elt, "hx-swap");
var selector = getClosestAttributeValue(elt, "hx-select");
if (swapStyle === "merge") {
mergeResponse(target, resp, selector);
} else if (swapStyle === "outerHTML") {
processResponseNodes(parentElt(target), target, resp, after, selector);
parentElt(target).removeChild(target);
if (swapStyle === "outerHTML") {
swapOuterHTML(target, fragment);
} else if (swapStyle === "prepend") {
processResponseNodes(target, target.firstChild, resp, after, selector);
swapPrepend(target, fragment);
} else if (swapStyle === "prependBefore") {
processResponseNodes(parentElt(target), target, resp, after, selector);
swapPrependBefore(target, fragment);
} else if (swapStyle === "append") {
processResponseNodes(target, null, resp, after, selector);
swapAppend(target, fragment);
} else if (swapStyle === "appendAfter") {
processResponseNodes(parentElt(target), target.nextSibling, resp, after, selector);
swapAppendAfter(target, fragment);
} else {
target.innerHTML = "";
processResponseNodes(target, null, resp, after, selector);
swapInnerHTML(target, fragment);
}
if(callBack) {
callBack.call();
}
}
@ -436,21 +443,10 @@ var HTMx = HTMx || (function () {
getInternalData(elt).sseSource = source;
}
function processNode(elt) {
var nodeData = getInternalData(elt);
if (!nodeData.processed) {
nodeData.processed = true;
var trigger = getTrigger(elt);
var explicitAction = false;
forEach(VERBS, function(verb){
var path = getAttributeValue(elt, 'hx-' + verb);
if (path) {
nodeData.path = path;
nodeData.verb = verb;
explicitAction = true;
if (trigger.indexOf("sse:") === 0) {
var sseEventName = trigger.substr(4);
var sseSource = getClosestMatch(elt, function(parent) {return parent.sseSource;});
function processSSETrigger(sseEventName, elt, verb, path) {
var sseSource = getClosestMatch(elt, function (parent) {
return parent.sseSource;
});
if (sseSource) {
var sseListener = function () {
if (!maybeCloseSSESource(sseSource)) {
@ -465,14 +461,30 @@ var HTMx = HTMx || (function () {
} else {
triggerEvent(elt, "noSSESourceError.mx")
}
} if (trigger === 'revealed') {
initScrollHandler();
maybeReveal(elt);
} else if (trigger === 'load') {
}
function loadImmediately(nodeData, elt, verb, path) {
if (!nodeData.loaded) {
nodeData.loaded = true;
issueAjaxRequest(elt, verb, path);
}
}
function processVerbs(elt, nodeData, trigger) {
var explicitAction = false;
forEach(VERBS, function (verb) {
var path = getAttributeValue(elt, 'hx-' + verb);
if (path) {
explicitAction = true;
nodeData.path = path;
nodeData.verb = verb;
if (trigger.indexOf("sse:") === 0) {
processSSETrigger(trigger.substr(4), elt, verb, path);
} else if (trigger === 'revealed') {
initScrollHandler();
maybeReveal(elt);
} else if (trigger === 'load') {
loadImmediately(nodeData, elt, verb, path);
} else if (trigger.trim().indexOf('every ') === 0) {
nodeData.polling = true;
processPolling(elt, verb, path);
@ -481,6 +493,17 @@ var HTMx = HTMx || (function () {
}
}
});
return explicitAction;
}
function processNode(elt) {
var nodeData = getInternalData(elt);
if (!nodeData.processed) {
nodeData.processed = true;
var trigger = getTrigger(elt);
var explicitAction = processVerbs(elt, nodeData, trigger);
if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") {
boostElement(elt, nodeData, trigger);
}
@ -575,10 +598,11 @@ var HTMx = HTMx || (function () {
}
function saveLocalHistoryData(historyData) {
triggerEvent(getDocument().body, "historySave.hx", {data:historyData});
localStorage.setItem('hx-history', JSON.stringify(historyData));
}
function getLocalHistoryData() {
function getHistoryMetadata() {
var historyEntry = localStorage.getItem('hx-history');
var historyData;
if (historyEntry) {
@ -592,9 +616,10 @@ var HTMx = HTMx || (function () {
}
function newHistoryData() {
var historyData = getLocalHistoryData();
var historyData = getHistoryMetadata();
var newId = makeHistoryId();
var slots = historyData.slots;
triggerEvent(getDocument().body, "historyNew.hx", {data:historyData});
if (slots.length > 20) {
var toEvict = slots.shift();
localStorage.removeItem('hx-history-' + toEvict);
@ -606,17 +631,19 @@ var HTMx = HTMx || (function () {
function updateCurrentHistoryContent() {
var elt = getHistoryElement();
var historyData = getLocalHistoryData();
var historyData = getHistoryMetadata();
triggerEvent(getDocument().body, "historyUpdate.hx", {data:historyData});
history.replaceState({"hx-history-key": historyData.current}, getDocument().title, window.location.href);
localStorage.setItem('hx-history-' + historyData.current, elt.innerHTML);
}
function restoreHistory(data) {
updateCurrentHistoryContent();
var historyKey = data['hx-history-key'];
triggerEvent(getDocument().body, "historyUpdate.hx", {data:historyKey});
var content = localStorage.getItem('hx-history-' + historyKey);
var elt = getHistoryElement();
elt.innerHTML = "";
processResponseNodes(elt, null, content);
swapInnerHTML(elt, makeFragment(content));
}
function shouldPush(elt) {

View File

@ -25,7 +25,7 @@
<script src="indicators.js"></script>
<script src="values.js"></script>
<script src="events.js"></script>
<script src="swap_direct.js"></script>
<script src="oob.js"></script>
<!--TODO figure out how to test stuff w/ history involved-->
<!--<script src="history.js"></script>-->

View File

@ -9,7 +9,7 @@ describe("HTMx Direct Swap", function () {
});
it('handles basic response properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-direct='true'>Swapped</div>");
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();
@ -19,11 +19,11 @@ describe("HTMx Direct Swap", function () {
})
it('handles no id match properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-direct='true'>Swapped</div>");
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\nSwapped");
div.innerText.should.equal("Clicked");
})

View File

@ -1,23 +1,18 @@
<html lang="en">
<head>
<style>
div {
transition: all 1000ms ease-in;
}
.indicator {
opacity: 0;
transition: all 200ms ease-in;
}
.hx-show-indicator .indicator {
opacity: 100%;
}
div {
transition: all 1000ms ease-in;
}
div.foo {
color: red;
transition: all 1000ms ease-in;
}
</style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
@ -29,9 +24,11 @@
<script src="scratch_server.js"></script>
<script>
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');
// this.server.respondWith("GET", "/test", "Clicked!");
// var btn = make('<button hx-get="/test">Click Me!</button>')
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>');
</script>
@ -45,6 +42,7 @@ Autorespond: <input id="autorespond" type="checkbox" onclick="toggleAutoRespond(
<hr/>
<div id="work-area" class="hx-history-element">
</div>
</body>

View File

@ -1,36 +0,0 @@
<html lang="en">
<head>
<title>HTMx - Teaching HTML new tricks</title>
<link rel="stylesheet" href="/css/site.css"/>
<link rel="stylesheet" href="/css/prism-theme.css"/>
<script src="/js/htmx.js"></script>
</head>
<body>
<h1 class="hero" hx-add-class="settle">&lt;<span class="flair">/</span>&gt; HTM<sub class="flair">x</sub></h1>
<div class="nav">
<h3>Navigation</h3>
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/docs">Docs</a>
<ul>
<li>
<a href="/docs/attributes">Attributes</a>
</li>
<li>
<a href="/docs/attributes">Events</a>
</li>
<li>
<a href="/docs/attributes">Headers</a>
</li>
</ul>
</li>
</ul>
</div>
<div id="content">
{{ content | safe }}
</div>
</body>
</html>

38
www/_includes/layout.njk Normal file
View File

@ -0,0 +1,38 @@
<html lang="en">
<head>
<title>HTMx - Teaching HTML new tricks</title>
<link rel="stylesheet" href="/css/site.css"/>
<link rel="stylesheet" href="/css/prism-theme.css"/>
<script src="/js/htmx.js"></script>
</head>
<body>
<div class="c">
<div class="{% if page.url == '/' %}root{% endif %} top-nav">
<h1 class="hero" hx-add-class="settle">&lt;<a>/</a>&gt; HTM<sub><a>x</a></sub></h1>
<div class="row center">
<div class="1 col">
<a href="/">home</a>
</div>
<div class="1 col">
<a href="/docs">docs</a>
</div>
<div class="1 col">
<a href="/docs/attributes">attributes</a>
</div>
<div class="1 col">
<a href="/docs/attributes">events</a>
</div>
<div class="1 col">
<a href="/docs/attributes">headers</a>
</div>
<div class="1 col">
<a href="https://github.com/bigskysoftware/htmx">github</a>
</div>
</div>
</div>
<div id="content">
{{ content | safe }}
</div>
</div>
</body>
</html>

View File

@ -1,30 +1,53 @@
body {
margin: 40px auto;
max-width: 740px;
margin: 0px;
line-height: 1.6;
font-size: 18px;
color: #333;
padding: 0 10px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif !important;
}
h1, h2, h3 {
line-height: 1.2
}
h1 {
}
.flair {
h2 {
border-bottom: 2px solid whitesmoke;
color: rgb(52, 101, 164);
}
#content {
margin-top: 32px
}
p {
margin: 16px;
}
pre[class*="language-"] {
font-size: 16px;
margin-top: 24px !important;
margin-bottom: 24px !important;
margin-left: 48px !important;
margin-right: 48px !important;
}
blockquote {
border-left: 4px solid whitesmoke;
margin-left: 32px;
margin-right: 32px;
}
.hero {
text-align: center;
font-size: 4em;
font-size: 5em;
margin: 0;
line-height: 1em;
}
.root .hero {
opacity: 5%;
position: relative;
top: -20px;
}
.hero.settle {
.root .hero.settle {
top: 0px;
opacity: 100%;
transition: 500ms ease-in;
@ -32,16 +55,125 @@ h1 {
.nav {
margin: 12px;
position: absolute;
/*position: absolute;*/
top: 180px;
left: 10px;
}
a {
text-decoration: none;
color:rgb(52, 101, 164)
}
.center {
text-align: center;
}
.nav ul {
padding-left: 12px;
list-style: none;
padding-left: 12px;
}
/* customized version of lit.css */
* + *{
box-sizing: border-box;
margin: .5em 0;
}
@media(max-width:45em) {
.nav {
text-align: center;
}
.nav ul {
padding: 0;
font-size: 22px;
}
.nav li {
}
}
@media(min-width:45em) {
.col {
display: table-cell;
}
.\31 {
width: 5%;
}
.\33 {
width: 22%;
}
.\34 {
width: 30%;
}
.\35 {
width: 40%;
}
.\32 {
width: 15%;
}
.row {
display: table;
border-spacing: 1em 0;
}
}
.w-100,
.row {
width: 100%;
}
.card:focus {
outline: 0;
border: solid rgb(52, 101, 164);
}
hr {
outline: 0;
}
.card {
padding: 1em;
border: solid #eee;
}
a[href]:hover, .btn:hover {
opacity: .6;
}
.c {
max-width: 55em;
padding: 1em;
margin: auto;
}
.btn.primary {
color: white;
background: rgb(52, 101, 164);
border: solid rgb(52, 101, 164);
}
td {
padding: 1em;
text-align: left;
border-bottom: solid #eee;
}
th {
padding: 1em;
text-align: left;
border-bottom: solid #eee;
}
.btn {
padding: 1em;
letter-spacing: .1em;
text-transform: uppercase;
background: white;
border: solid;
cursor:pointer;
}
pre {
overflow: auto;
}

244
www/docs.md Normal file
View File

@ -0,0 +1,244 @@
---
layout: layout.njk
title: HTMx - HTML Extensions
---
<div class="row">
<div class="2 col nav">
**Contents**
* [introduction](#introduction)
* [installing](#installing)
* [ajax](#ajax)
* [triggers](#triggers)
* [special events](#special-events)
* [polling](#polling)
* [SSE](#sse)
* [targets](#targets)
* [forms](#forms)
* [swapping](#swapping)
* [history](#history)
* [requests & responses](#requests)
* [misc](#misc)
* [events & logging](#events)
</div>
<div class="10 col">
## <a name="introduction"></a>[HTMx in a Nutshell](#introduction)
HTMx is a set of attributes in HTML that allow you to access modern browser features directly
from the browser. To understand how HTMx works, first lets take a look at an anchor tag:
``` html
<a href="/blog">Blog</a>
```
This anchor tag tells a browser:
targest
> "When a user clicks on this link, issue an HTTP GET request to '/blog' and load the response content
> into the browser window".
Now consider some HTMx code:
``` html
<a hx-post="/click">Click Me!</a>
```
This tells a browser:
> "When a user clicks on this link, issue an HTTP GET request to '/blog' and load the response content into the inner
> html of this element"
So the difference is that with HTMx:
* A different HTTP action is used
* The request is done via AJAX
* The response replaces the content of the element, rather than the entire page
HTMx expects responses to the AJAX calls that it makes to be *HTML* rather than *JSON*, as is more typical with AJAX
requests.
If you would prefer, you can use the `data-` prefix when using HTMx:
``` html
<a data-hx-post="/click">Click Me!</a>
```
## <a name="installing"></a> [Installing](#installing)
Intercooler is a dependency-free library, written in javascript.
It can be loaded via NPM as "`htmx.org`" or included from [unpkg](https://unpkg.com/browse/htmx.org/):
``` html
<script src="https://unpkg.com/htmx.org@0.0.1"></script>
```
## <a name="ajax"></a> [AJAX](#ajax)
HTMx provides attributes to allow you to issue AJAX requests directly from HTML. The main attributes are:
* [hx-get](/attributes/hx-get) - Issues a `GET` request to the given URL
* [hx-post](/attributes/hx-post) - Issues a `POST` request to the given URL
* [hx-put](/attributes/hx-put) - Issues a `PUT` request to the given URL (see [details](#htmx-request-details))
* [hx-patch](/attributes/hx-patch) - Issues a `PATCH` request to the given URL (see [details](#htmx-request-details))
* [hx-delete](/attributes/hx-delete) - Issues a `GET` request to the given URL (see [details](#htmx-request-details))
Each of these attributes takes a URL to issue an AJAX request to. The element will issue a request of the specified
type to the given URL when the element is triggered.
### <a name="triggers"></a> [Triggering Requests](#triggers)
By default, elements issue a request on the "natural" event:
* `input`, `textarea` & `select`: the `change` event
* `form`: the `submit` event
* everything else: the `click` event
You might not want to use the default event. In this case you can use the [hx-trigger](/attributes/hx-trigger)
attribute to specify the event you want the element to respond to. Here is a `div` that posts to `/mouse_entered`
when a mouse enters it:
```html
<div hx-post="/mouse_entered" hx-trigger="mouseenter">
[Here Mouse, Mouse!]
</div>
```
If you want a request to only happen once, you can use the [hx-trigger-once](/attributes/hx-trigger-once) attribute:
```html
<div hx-post="/mouse_entered" hx-trigger="mouseenter"
hx-trigger-once="true">
[Here Mouse, Mouse!]
</div>
```
If the element is an input, and you only want the request to happen when the value changes, you can use the
[hx-trigger-changed-only](/attributes/hx-trigger-changed-only) attribute.
This can be paired with the [hx-trigger-delay](/attributes/hx-trigger-delay) attribute to implement a common UX
pattern, [Live Search](/demo/live-search):
```html
<input type="text" name="q"
hx-get="/trigger_delay"
hx-trigger="keyup"
hx-target="#search-results"
hx-trigger-delay="500ms" placeholder="Search..."/>
<div id="search-results"></div>
```
This input will issue a request 500 milliseconds after a key up event if the input has been changed and puts the results
into the `div#search-results`.
#### <a name="special-events"></a> [Special Events](#special-events)
HTMx provides a few special events for use in [hx-trigger](/attributes/hx-trigger):
* `load` - fires once when the element is first loaded
* `revealed` - fires once when an element first scrolls into the viewport
You can also use custom events to trigger requests if you have an advanced use case.
### <a name="targets"></a> [Targets](#targets)
If you want the response to be loaded into a different element other than the one that made the request, you can
use the [hx-target](/attributes/hx-target) attribute, which takes a CSS selector. Looking back at our Live Search example:
```html
<input type="text" name="q"
hx-get="/trigger_delay"
hx-trigger="keyup"
hx-target="#search-results"
hx-trigger-delay="500ms" placeholder="Search..."/>
<div id="search-results"></div>
```
You can see that the results from the search are going to be loaded into `div#search-results`.
### <a name="forms"></a> [Forms & Input Values](#forms)
By default, an element will include its value if it has one. Additionally, if the element is in a form, all values
in the form will be included in the response.
If you wish to include the values of other elements, you can use the [hx-include](/attributes/hx-include) attribute
with a CSS selector of all the elements whose values you want to include in the request.
Finally, if you want to programatically modify the arguments, you can use the [values.hx](/events/values.hx) event to
do so.
### <a name="swapping"></a> [Swapping](#swapping)
HTMx offers a few different ways to swap the HTML returned into the DOM. By default, the content replaces the
`innerHTML` of the target element. You can modify this by using the [hx-swap](/attributes/hx-swap) attribute
with any of the following values:
* `innerHTML` - the default, puts the content inside the target element
* `outerHTML` - replaces the target element with the returned content
* `prepend` - prepends the content before the first child inside the target
* `prependBefore` - prepends the content before the target in the targets parent element
* `append` - appends the content after the last child inside the target
* `appendAfter` - appends the content after the target in the targets parent element
* `merge` - attempts to merge the response content into the target, reusing matching elements in the existing DOM
#### Out of Band Swaps
If you want to swap content from a response directly into the DOM by using the `id` attribute you can use the
[hx-swap-directly](/attributes/hx-swap-directly) attribute in the *response* html:
```html
<div id="message" hx-swap-directly="true">Swap me directly!</div>
Additional Content
```
In this response, `div#message` would be swapped directly into the matching DOM element, while the additional content
would be swapped into the target in the normal manner.
You can use this technique to "piggy-back" updates on other requests.
If you want the out of band content merged you can use the value `merge` for this attribute.
#### Selecting Content To Swap
If you want to select a subset of the response HTML to swap into the target, you can use the [hx-select](/attributes/hx-select)
attribute, which takes a CSS selector and selects the matching elements from the response.
## <a name="history"></a> [History Support](#history)
HTMx provides a simple mechanism for interacting with the [browser history API](https://developer.mozilla.org/en-US/docs/Web/API/History_API):
If you want a given element to push its request into the browser navigation bar and add the current state of the page
to the browsers history, include the [hx-push](/attributes/hx-push) attribute:
```html
<a hx-get="/Blog" hx-push="true">Blog</a>
```
When a user clicks on this link, HTMx will snapshot the current DOM and store it before it makes a request to /blog.
It then does the swap and pushes a new location onto the history stack.
When a user hits the back button, HTMx will retrieve the old content from storage and swap it back into the target,
simulating "going back" to the previous state.
### Specifying History Snapshot Element
By default, HTMx will use the `body` to take and restore the history snapshop from. This is usually good enough but
if you want to use a narrower element for snapshotting you can use the [hx-history-element](/attributes/hx-history-element)
attribute to specify a different one. Careful: this element will need to be on all pages or restoring from history
won't work reliably.
## <a name="requests">[Requests & Responses](#requests)
## Miscellaneous Attributes
### Class Swapping
### Timed Removal
### Boosting
## <a name="events"></a> [Events & Logging](#events)
</div>
</div>

View File

@ -1,5 +1,5 @@
---
layout: layout.html
layout: layout.njk
title: HTMx - HTML Extensions / Attributes
---

View File

@ -1,5 +1,5 @@
---
layout: layout.html
layout: layout.njk
title: HTMx - HTML Extensions / Attributes
---

View File

@ -1,5 +1,5 @@
---
layout: layout.html
layout: layout.njk
title: HTMx - HTML Extensions / Attributes
---

View File

@ -1,22 +1,29 @@
---
layout: layout.html
layout: layout.njk
title: HTMx - HTML Extensions
---
HTMx is a set of extensions to HTML that bring many of the useful features of modern web browsers directly
into HTML. It fills gaps in functionality found in standard HTML, dramatically expanding its expressiveness while
retaining the fundamental simplicity of declarative hypertext.</p>
## Introduction
Here is a simple example of HTMx in action:
HTMx is a small (<12Kb) &amp; dependency-free library that surfaces the features of modern browsers using HTML
attributes. Using HTMx you can implement many [UX patterns](/demo) that would typically require writing javascript.
HTMx is unobtrusive, plays well with other tools, can be adopted incrementally with no up-front rewrites.
## Quick Start
``` html
<button hx-get="/example" hx-target="#myDiv">
Click Me
</button>
<!-- Load from unpkg -->
<script src="https://unpkg.com/htmx.org@0.0.1"></script>
<!-- enhance a button -->
<button hx-get="/example">Click Me</button>
```
This example issues an AJAX request to <code>/example</code> when a user clicks on it, and swaps the response
HTML into the element with the id `myDiv`
This code tells HTMx that:
> "When a user clicks on this button, issue an AJAX request to /example, and load the content into the body
> of the button"
HTMx is based on [intercooler.js](http://intercoolerjs.org) and is the successor to that project.
HTMx is based on [intercooler.js](http://intercoolerjs.org), and aims to be a minimalist &amp;
dependency free successor to that project.

View File

@ -1,7 +1,13 @@
var HTMx = HTMx || (function()
{
// noinspection JSUnusedAssignment
var HTMx = HTMx || (function () {
'use strict';
var VERBS = ['get', 'post', 'put', 'delete', 'patch']
//====================================================================
// Utilities
//====================================================================
function parseInterval(str) {
if (str === "null" || str === "false" || str === "") {
return null;
@ -10,92 +16,250 @@ var HTMx = HTMx || (function()
} else if (str.lastIndexOf("s") === str.length - 1) {
return parseFloat(str.substr(0, str.length - 1)) * 1000;
} else {
return 1000;
return parseFloat(str);
}
}
function getRawAttribute(elt, name) {
return elt.getAttribute && elt.getAttribute(name);
}
// resolve with both hx and data-hx prefixes
function getAttributeValue(elt, qualifiedName) {
return elt.getAttribute(qualifiedName) || elt.getAttribute("data-" + qualifiedName);
return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName);
}
function getClosestAttributeValue(elt, attributeName)
{
var attribute = getAttributeValue(elt, attributeName);
if(attribute)
{
return attribute;
function parentElt(elt) {
return elt.parentElement;
}
else if (elt.parentElement)
{
return getClosestAttributeValue(elt.parentElement, attributeName);
function getDocument() {
return document;
}
else
{
function getClosestMatch(elt, condition) {
if (condition(elt)) {
return elt;
} else if (parentElt(elt)) {
return getClosestMatch(parentElt(elt), condition);
} else {
return null;
}
}
function getClosestAttributeValue(elt, attributeName) {
var closestAttr = null;
getClosestMatch(elt, function (e) {
return closestAttr = getRawAttribute(e, attributeName);
});
return closestAttr;
}
function matches(elt, selector) {
// noinspection JSUnresolvedVariable
return (elt != null) &&(elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector
|| elt.webkitMatchesSelector || elt.oMatchesSelector).call(elt, selector);
}
function closest (elt, selector) {
do if (elt == null || matches(elt, selector)) return elt;
while (elt = elt && parentElt(elt));
}
function makeFragment(resp) {
var range = getDocument().createRange();
return range.createContextualFragment(resp);
}
function isType(o, type) {
return Object.prototype.toString.call(o) === "[object " + type + "]";
}
function isFunction(o) {
return isType(o, "Function");
}
function isRawObject(o) {
return isType(o, "Object");
}
function getInternalData(elt) {
var dataProp = 'hx-data-internal';
var data = elt[dataProp];
if (!data) {
data = elt[dataProp] = {};
}
return data;
}
function toArray(object) {
var arr = [];
forEach(object, function(elt) {
arr.push(elt)
});
return arr;
}
function forEach(arr, func) {
for (var i = 0; i < arr.length; i++) {
func(arr[i]);
}
}
function isScrolledIntoView(el) {
var rect = el.getBoundingClientRect();
var elemTop = rect.top;
var elemBottom = rect.bottom;
return elemTop < window.innerHeight && elemBottom >= 0;
}
function bodyContains(elt) {
return getDocument().body.contains(elt);
}
//====================================================================
// Node processing
//====================================================================
function getTarget(elt) {
var targetVal = getClosestAttributeValue(elt, "hx-target");
if (targetVal) {
return document.querySelector(targetVal);
var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null});
if (explicitTarget) {
var targetStr = getRawAttribute(explicitTarget, "hx-target");
if (targetStr === "this") {
return explicitTarget;
} else {
return getDocument().querySelector(targetStr);
}
} else {
var data = getInternalData(elt);
if (data.boosted) {
return getDocument().body;
} else {
return elt;
}
}
function makeFragment(resp) {
var range = document.createRange();
return range.createContextualFragment(resp);
}
function processResponseNodes(parent, target, text) {
function directSwap(child) {
var swapDirect = getAttributeValue(child, 'hx-swap-direct');
if (swapDirect) {
var target = getDocument().getElementById(getRawAttribute(child,'id'));
if (target) {
if (swapDirect === "merge") {
mergeInto(target, child);
} else {
var newParent = parentElt(target);
newParent.insertBefore(child, target);
newParent.removeChild(target);
return true;
}
}
}
return false;
}
function processResponseNodes(parentNode, insertBefore, text, executeAfter, selector) {
var fragment = makeFragment(text);
for (var i = fragment.childNodes.length - 1; i >= 0; i--) {
var child = fragment.childNodes[i];
parent.insertBefore(child, target);
if (child.nodeType != Node.TEXT_NODE) {
processElement(child);
var nodesToProcess;
if (selector) {
nodesToProcess = toArray(fragment.querySelectorAll(selector));
} else {
nodesToProcess = toArray(fragment.childNodes);
}
forEach(nodesToProcess, function(child){
if (!directSwap(child)) {
parentNode.insertBefore(child, insertBefore);
}
if (child.nodeType !== Node.TEXT_NODE) {
triggerEvent(child, 'load.hx', {parent:parentElt(child)});
processNode(child);
}
});
if(executeAfter) {
executeAfter.call();
}
}
function swapResponse(elt, resp) {
var target = getTarget(elt);
function findMatch(elt, possible) {
for (var i = 0; i < possible.length; i++) {
var candidate = possible[i];
if (elt.hasAttribute("id") && elt.id === candidate.id) {
return candidate;
}
if (!candidate.hasAttribute("id") && elt.tagName === candidate.tagName) {
return candidate;
}
}
return null;
}
function cloneAttributes(mergeTo, mergeFrom) {
forEach(mergeTo.attributes, function (attr) {
if (!mergeFrom.hasAttribute(attr.name)) {
mergeTo.removeAttribute(attr.name)
}
});
forEach(mergeFrom.attributes, function (attr) {
mergeTo.setAttribute(attr.name, attr.value);
});
}
function mergeChildren(mergeTo, mergeFrom) {
var oldChildren = toArray(mergeTo.children);
var marker = getDocument().createElement("span");
mergeTo.insertBefore(marker, mergeTo.firstChild);
forEach(mergeFrom.childNodes, function (newChild) {
var match = findMatch(newChild, oldChildren);
if (match) {
while (marker.nextSibling && marker.nextSibling !== match) {
mergeTo.removeChild(marker.nextSibling);
}
mergeTo.insertBefore(marker, match.nextSibling);
mergeInto(match, newChild);
} else {
mergeTo.insertBefore(newChild, marker);
}
});
while (marker.nextSibling) {
mergeTo.removeChild(marker.nextSibling);
}
mergeTo.removeChild(marker);
}
function mergeInto(mergeTo, mergeFrom) {
cloneAttributes(mergeTo, mergeFrom);
mergeChildren(mergeTo, mergeFrom);
}
function mergeResponse(target, resp, selector) {
var fragment = makeFragment(resp);
mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild);
}
function swapResponse(target, elt, resp, after) {
var swapStyle = getClosestAttributeValue(elt, "hx-swap");
if (swapStyle === "outerHTML") {
processResponseNodes(target.parentElement, target, resp);
target.parentElement.removeChild(target);
var selector = getClosestAttributeValue(elt, "hx-select");
if (swapStyle === "merge") {
mergeResponse(target, resp, selector);
} else if (swapStyle === "outerHTML") {
processResponseNodes(parentElt(target), target, resp, after, selector);
parentElt(target).removeChild(target);
} else if (swapStyle === "prepend") {
processResponseNodes(target, target.firstChild, resp);
processResponseNodes(target, target.firstChild, resp, after, selector);
} else if (swapStyle === "prependBefore") {
processResponseNodes(target.parentElement, target, resp);
processResponseNodes(parentElt(target), target, resp, after, selector);
} else if (swapStyle === "append") {
processResponseNodes(target, null, resp);
processResponseNodes(target, null, resp, after, selector);
} else if (swapStyle === "appendAfter") {
processResponseNodes(target.parentElement, target.nextSibling, resp);
processResponseNodes(parentElt(target), target.nextSibling, resp, after, selector);
} else {
target.innerHTML = "";
processResponseNodes(target, null, resp);
processResponseNodes(target, null, resp, after, selector);
}
}
function triggerEvent(elt, eventName, details) {
details["elt"] = elt;
if (window.CustomEvent && typeof window.CustomEvent === 'function') {
var event = new CustomEvent(eventName, {detail: details});
} else {
var event = document.createEvent('CustomEvent');
event.initCustomEvent(eventName, true, true, details);
}
elt.dispatchEvent(event);
}
function isRawObject(o){
return Object.prototype.toString.call(o) === "[object Object]";
}
function handleTrigger(elt, trigger) {
if (trigger) {
if (trigger.indexOf("{") === 0) {
@ -115,43 +279,6 @@ var HTMx = HTMx || (function()
}
}
// core ajax request
function issueAjaxRequest(elt, url)
{
var request = new XMLHttpRequest();
// TODO - support more request types POST, PUT, DELETE, etc.
request.open('GET', url, true);
request.onload = function()
{
var trigger = this.getResponseHeader("X-HX-Trigger");
handleTrigger(elt, trigger);
if (this.status >= 200 && this.status < 400)
{
// don't process 'No Content' response
if (this.status != 204) {
// Success!
var resp = this.response;
swapResponse(elt, resp);
}
}
else
{
// TODO error handling
elt.innerHTML = "ERROR";
}
};
request.onerror = function () {
// TODO error handling
// There was a connection error of some sort
};
request.send();
}
function matches(el, selector) {
return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
}
function getTrigger(elt) {
var explicitTrigger = getClosestAttributeValue(elt, 'hx-trigger');
if (explicitTrigger) {
@ -169,65 +296,610 @@ var HTMx = HTMx || (function()
}
}
// DOM element processing
function processClassList(elt, classList, operation) {
var values = classList.split(",");
for (var i = 0; i < values.length; i++) {
forEach(values, function(value){
var cssClass = "";
var delay = 50;
if (values[i].trim().indexOf(":") > 0) {
var split = values[i].trim().split(':');
var trimmedValue = value.trim();
if (trimmedValue.indexOf(":") > 0) {
var split = trimmedValue.split(':');
cssClass = split[0];
delay = parseInterval(split[1]);
} else {
cssClass = values[i].trim();
cssClass = trimmedValue;
}
setTimeout(function () {
elt.classList[operation].call(elt.classList, cssClass);
}, delay);
});
}
function processPolling(elt, verb, path) {
var trigger = getTrigger(elt);
var nodeData = getInternalData(elt);
if (trigger.trim().indexOf("every ") === 0) {
var args = trigger.split(/\s+/);
var intervalStr = args[1];
if (intervalStr) {
var interval = parseInterval(intervalStr);
nodeData.timeout = setTimeout(function () {
if (bodyContains(elt)) {
issueAjaxRequest(elt, verb, path);
processPolling(elt, verb, getAttributeValue(elt, "hx-" + verb));
}
}, interval);
}
}
}
function processElement(elt) {
if(getAttributeValue(elt,'hx-get')) {
var trigger = getTrigger(elt);
if (trigger === 'load') {
issueAjaxRequest(elt, getAttributeValue(elt, 'hx-get'));
function isLocalLink(elt) {
return location.hostname === elt.hostname &&
getRawAttribute(elt,'href') &&
!getRawAttribute(elt,'href').startsWith("#")
}
function boostElement(elt, nodeData, trigger) {
if ((elt.tagName === "A" && isLocalLink(elt)) || elt.tagName === "FORM") {
nodeData.boosted = true;
var verb, path;
if (elt.tagName === "A") {
verb = "get";
path = getRawAttribute(elt, 'href');
} else {
elt.addEventListener(trigger, function(evt){
issueAjaxRequest(elt, getAttributeValue(elt, 'hx-get'));
evt.stopPropagation();
var rawAttribute = getRawAttribute(elt, "method");
verb = rawAttribute ? rawAttribute.toLowerCase() : "get";
path = getRawAttribute(elt, 'action');
}
addEventListener(elt, verb, path, nodeData, trigger, true);
}
}
function addEventListener(elt, verb, path, nodeData, trigger, cancel) {
var eventListener = function (evt) {
if(cancel) evt.preventDefault();
var eventData = getInternalData(evt);
var elementData = getInternalData(elt);
if (!eventData.handled) {
eventData.handled = true;
if (getAttributeValue(elt, "hx-trigger-once") === "true") {
if (elementData.triggeredOnce) {
return;
} else {
elementData.triggeredOnce = true;
}
}
if (getAttributeValue(elt, "hx-trigger-changed-only") === "true") {
if (elementData.lastValue === elt.value) {
return;
} else {
elementData.lastValue = elt.value;
}
}
if (elementData.delayed) {
clearTimeout(elementData.delayed);
}
var eventDelay = getAttributeValue(elt, "hx-trigger-delay");
var issueRequest = function(){
issueAjaxRequest(elt, verb, path, evt.target);
}
if (eventDelay) {
elementData.delayed = setTimeout(issueRequest, parseInterval(eventDelay));
} else {
issueRequest();
}
}
};
nodeData.trigger = trigger;
nodeData.eventListener = eventListener;
elt.addEventListener(trigger, eventListener);
}
function initScrollHandler() {
if (!window['hxScrollHandler']) {
var scrollHandler = function() {
forEach(getDocument().querySelectorAll("[hx-trigger='reveal']"), function (elt) {
maybeReveal(elt);
});
};
window['hxScrollHandler'] = scrollHandler;
window.addEventListener("scroll", scrollHandler)
}
}
function maybeReveal(elt) {
var nodeData = getInternalData(elt);
if (!nodeData.revealed && isScrolledIntoView(elt)) {
nodeData.revealed = true;
issueAjaxRequest(elt, nodeData.verb, nodeData.path);
}
}
function maybeCloseSSESource(elt) {
if (!bodyContains(elt)) {
elt.sseSource.close();
return true;
}
}
function initSSESource(elt, sseSrc) {
var details = {
initializer: function() { new EventSource(sseSrc, details.config) },
config:{withCredentials: true}
};
triggerEvent(elt, "initSSE.mx", {config:details})
var source = details.initializer();
source.onerror = function (e) {
triggerEvent(elt, "sseError.mx", {error:e, source:source});
maybeCloseSSESource(elt);
};
getInternalData(elt).sseSource = source;
}
function processNode(elt) {
var nodeData = getInternalData(elt);
if (!nodeData.processed) {
nodeData.processed = true;
var trigger = getTrigger(elt);
var explicitAction = false;
forEach(VERBS, function(verb){
var path = getAttributeValue(elt, 'hx-' + verb);
if (path) {
nodeData.path = path;
nodeData.verb = verb;
explicitAction = true;
if (trigger.indexOf("sse:") === 0) {
var sseEventName = trigger.substr(4);
var sseSource = getClosestMatch(elt, function(parent) {return parent.sseSource;});
if (sseSource) {
var sseListener = function () {
if (!maybeCloseSSESource(sseSource)) {
if (bodyContains(elt)) {
issueAjaxRequest(elt, verb, path);
} else {
sseSource.sseSource.removeEventListener(sseEventName, sseListener);
}
}
};
sseSource.sseSource.addEventListener(sseEventName, sseListener);
} else {
triggerEvent(elt, "noSSESourceError.mx")
}
} if (trigger === 'revealed') {
initScrollHandler();
maybeReveal(elt);
} else if (trigger === 'load') {
if (!nodeData.loaded) {
nodeData.loaded = true;
issueAjaxRequest(elt, verb, path);
}
} else if (trigger.trim().indexOf('every ') === 0) {
nodeData.polling = true;
processPolling(elt, verb, path);
} else {
addEventListener(elt, verb, path, nodeData, trigger);
}
}
});
if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") {
boostElement(elt, nodeData, trigger);
}
var sseSrc = getAttributeValue(elt, 'hx-sse-source');
if (sseSrc) {
initSSESource(elt, sseSrc);
}
var addClass = getAttributeValue(elt, 'hx-add-class');
if (addClass) {
processClassList(elt, addClass, "add");
}
var removeClass = getAttributeValue(elt, 'hx-remove-class');
if (removeClass) {
processClassList(elt, removeClass, "remove");
}
}
forEach(elt.children, function(child) { processNode(child) });
}
//====================================================================
// Event/Log Support
//====================================================================
function sendError(elt, eventName, details) {
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, "details" : details }));
}
}
function makeEvent(eventName, details) {
var evt;
if (window.CustomEvent && typeof window.CustomEvent === 'function') {
evt = new CustomEvent(eventName, {detail: details});
} else {
evt = getDocument().createEvent('CustomEvent');
evt.initCustomEvent(eventName, true, true, details);
}
return evt;
}
function triggerEvent(elt, eventName, details) {
details["elt"] = elt;
var event = makeEvent(eventName, details);
if (HTMx.logger) {
HTMx.logger(elt, eventName, details);
if (eventName.indexOf("Error") > 0) {
sendError(elt, eventName, details);
}
}
var eventResult = elt.dispatchEvent(event);
var allResult = elt.dispatchEvent(makeEvent("all.hx", {elt:elt, originalDetails:details, originalEvent: event}));
return eventResult && allResult;
}
function addHTMxEventListener(arg1, arg2, arg3) {
var target, event, listener;
if (isFunction(arg1)) {
target = getDocument().body;
event = "all.hx";
listener = arg1;
} else if (isFunction(arg2)) {
target = getDocument().body;
event = arg1;
listener = arg2;
} else {
target = arg1;
event = arg2;
listener = arg3;
}
return target.addEventListener(event, listener);
}
//====================================================================
// History Support
//====================================================================
function makeHistoryId() {
return Math.random().toString(36).substr(3, 9);
}
function getHistoryElement() {
var historyElt = getDocument().getElementsByClassName('hx-history-element');
if (historyElt.length > 0) {
return historyElt[0];
} else {
return getDocument().body;
}
}
function saveLocalHistoryData(historyData) {
localStorage.setItem('hx-history', JSON.stringify(historyData));
}
function getLocalHistoryData() {
var historyEntry = localStorage.getItem('hx-history');
var historyData;
if (historyEntry) {
historyData = JSON.parse(historyEntry);
} else {
var initialId = makeHistoryId();
historyData = {"current": initialId, "slots": [initialId]};
saveLocalHistoryData(historyData);
}
return historyData;
}
function newHistoryData() {
var historyData = getLocalHistoryData();
var newId = makeHistoryId();
var slots = historyData.slots;
if (slots.length > 20) {
var toEvict = slots.shift();
localStorage.removeItem('hx-history-' + toEvict);
}
slots.push(newId);
historyData.current = newId;
saveLocalHistoryData(historyData);
}
function updateCurrentHistoryContent() {
var elt = getHistoryElement();
var historyData = getLocalHistoryData();
history.replaceState({"hx-history-key": historyData.current}, getDocument().title, window.location.href);
localStorage.setItem('hx-history-' + historyData.current, elt.innerHTML);
}
function restoreHistory(data) {
var historyKey = data['hx-history-key'];
var content = localStorage.getItem('hx-history-' + historyKey);
var elt = getHistoryElement();
elt.innerHTML = "";
processResponseNodes(elt, null, content);
}
function shouldPush(elt) {
return getClosestAttributeValue(elt, "hx-push-url") === "true" ||
(elt.tagName === "A" && getInternalData(elt).boosted);
}
function snapshotForCurrentHistoryEntry(elt) {
if (shouldPush(elt)) {
// TODO event to allow de-initialization of HTML elements in target
updateCurrentHistoryContent();
}
}
function initNewHistoryEntry(elt, url) {
if (shouldPush(elt)) {
newHistoryData();
history.pushState({}, "", url);
updateCurrentHistoryContent();
}
}
function addRequestIndicatorClasses(elt) {
mutateRequestIndicatorClasses(elt, "add");
}
function removeRequestIndicatorClasses(elt) {
mutateRequestIndicatorClasses(elt, "remove");
}
function mutateRequestIndicatorClasses(elt, action) {
var indicator = getClosestAttributeValue(elt, 'hx-indicator');
if (indicator) {
var indicators = getDocument().querySelectorAll(indicator);
} else {
indicators = [elt];
}
forEach(indicators, function(ic) {
ic.classList[action].call(ic.classList, "hx-show-indicator");
});
}
//====================================================================
// Input Value Processing
//====================================================================
function haveSeenNode(processed, elt) {
for (var i = 0; i < processed.length; i++) {
var node = processed[i];
if (node.isSameNode(elt)) {
return true;
}
}
return false;
}
function processInputValue(processed, values, elt) {
if (elt == null || haveSeenNode(processed, elt)) {
return;
} else {
processed.push(elt);
}
var name = getRawAttribute(elt,"name");
var value = elt.value;
if (name && value) {
var current = values[name];
if(current) {
if (Array.isArray(current)) {
current.push(value);
} else {
values[name] = [current, value];
}
} else {
values[name] = value;
}
}
if (matches(elt, 'form')) {
var inputs = elt.elements;
forEach(inputs, function(input) {
processInputValue(processed, values, input);
});
}
}
if (getAttributeValue(elt, 'hx-add-class')) {
processClassList(elt, getAttributeValue(elt,'hx-add-class'), "add");
function getInputValues(elt) {
var processed = [];
var values = {};
// include the element itself
processInputValue(processed, values, elt);
// include any explicit includes
var includes = getAttributeValue(elt, "hx-include");
if (includes) {
var nodes = getDocument().querySelectorAll(includes);
forEach(nodes, function(node) {
processInputValue(processed, values, node);
});
}
if (getAttributeValue(elt, 'hx-remove-class')) {
processClassList(elt, getAttributeValue(elt,'hx-remove-class'), "remove");
// include the closest form
processInputValue(processed, values, closest(elt, 'form'));
return values;
}
for (var i = 0; i < elt.children.length; i++) {
var child = elt.children[i];
processElement(child);
function appendParam(returnStr, name, realValue) {
if (returnStr !== "") {
returnStr += "&";
}
returnStr += encodeURIComponent(name) + "=" + encodeURIComponent(realValue);
return returnStr;
}
function urlEncode(values) {
var returnStr = "";
for (var name in values) {
if (values.hasOwnProperty(name)) {
var value = values[name];
if (Array.isArray(value)) {
forEach(value, function(v) {
returnStr = appendParam(returnStr, name, v);
});
} else {
returnStr = appendParam(returnStr, name, value);
}
}
}
return returnStr;
}
//====================================================================
// Ajax
//====================================================================
function setHeader(xhr, name, value, noPrefix) {
xhr.setRequestHeader((noPrefix ? "" : "X-HX-") + name, value || "");
}
function issueAjaxRequest(elt, verb, path, eventTarget) {
var eltData = getInternalData(elt);
if (eltData.requestInFlight) {
return;
} else {
eltData.requestInFlight = true;
}
var endRequestLock = function(){
eltData.requestInFlight = false
}
var target = getTarget(elt);
var promptQuestion = getClosestAttributeValue(elt, "hx-prompt");
if (promptQuestion) {
var prompt = prompt(promptQuestion);
if(!triggerEvent(elt, 'prompt.hx', {prompt: prompt, target:target})) return endRequestLock();
}
var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
if (confirmQuestion) {
if(!confirm(confirmQuestion)) return endRequestLock();
}
var xhr = new XMLHttpRequest();
var inputValues = getInputValues(elt);
if(!triggerEvent(elt, 'values.hx', {values: inputValues, target:target})) return endRequestLock();
// request type
if (verb === 'get') {
var noValues = Object.keys(inputValues).length === 0;
xhr.open('GET', path + (noValues ? "" : "?" + urlEncode(inputValues)), true);
} else {
xhr.open('POST', path, true);
setHeader(xhr,'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8', true);
if (verb !== 'post') {
setHeader(xhr, 'X-HTTP-Method-Override', verb.toUpperCase(), true);
}
}
// TODO IE10 compatibility?
xhr.overrideMimeType("text/html");
// request headers
setHeader(xhr, "Request", "true");
setHeader(xhr,"Trigger-Id", getRawAttribute(elt,"id"));
setHeader(xhr,"Trigger-Name", getRawAttribute(elt, "name"));
setHeader(xhr,"Target-Id", getRawAttribute(target,"id"));
setHeader(xhr,"Current-URL", getDocument().location.href);
if (prompt) {
setHeader(xhr,"Prompt", prompt);
}
if (eventTarget) {
setHeader(xhr,"Event-Target", getRawAttribute(eventTarget,"id"));
}
if (getDocument().activeElement) {
setHeader(xhr,"Active-Element", getRawAttribute(getDocument().activeElement,"id"));
// noinspection JSUnresolvedVariable
if (getDocument().activeElement.value) {
setHeader(xhr,"Active-Element-Value", getDocument().activeElement.value);
}
}
xhr.onload = function () {
try {
if (!triggerEvent(elt, 'beforeOnLoad.hx', {xhr: xhr, target: target})) return;
snapshotForCurrentHistoryEntry(elt, path);
var trigger = this.getResponseHeader("X-HX-Trigger");
handleTrigger(elt, trigger);
initNewHistoryEntry(elt, path);
if (this.status >= 200 && this.status < 400) {
// don't process 'No Content' response
if (this.status !== 204) {
// Success!
var resp = this.response;
if (!triggerEvent(elt, 'beforeSwap.hx', {xhr: xhr, target: target})) return;
target.classList.add("hx-swapping");
var doSwap = function () {
try {
swapResponse(target, elt, resp, function () {
target.classList.remove("hx-swapping");
updateCurrentHistoryContent();
triggerEvent(elt, 'afterSwap.hx', {xhr: xhr, target: target});
});
} catch (e) {
triggerEvent(elt, 'swapError.hx', {xhr: xhr, response: xhr.response, status: xhr.status, target: target});
throw e;
}
};
var swapDelayStr = getAttributeValue(elt, "hx-swap-delay");
if (swapDelayStr) {
setTimeout(doSwap, parseInterval(swapDelayStr))
} else {
doSwap();
}
}
} else {
triggerEvent(elt, 'responseError.hx', {xhr: xhr, response: xhr.response, status: xhr.status, target: target});
}
} catch (e) {
triggerEvent(elt, 'onLoadError.hx', {xhr: xhr, response: xhr.response, status: xhr.status, target: target});
throw e;
} finally {
removeRequestIndicatorClasses(elt);
endRequestLock();
triggerEvent(elt, 'afterOnLoad.hx', {xhr: xhr, response: xhr.response, status: xhr.status, target: target});
}
}
xhr.onerror = function () {
removeRequestIndicatorClasses(elt);triggerEvent(elt, 'loadError.hx', {xhr:xhr});
endRequestLock();
}
if(!triggerEvent(elt, 'beforeRequest.hx', {xhr:xhr, values: inputValues, target:target})) return endRequestLock();
addRequestIndicatorClasses(elt);
xhr.send(verb === 'get' ? null : urlEncode(inputValues));
}
//====================================================================
// Initialization
//====================================================================
function ready(fn) {
if (document.readyState !== 'loading'){
if (getDocument().readyState !== 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
getDocument().addEventListener('DOMContentLoaded', fn);
}
}
// initialize the document
ready(function () {
processElement(document.body);
processNode(getDocument().body);
window.onpopstate = function (event) {
restoreHistory(event.state);
};
})
function internalEval(str){
return eval(str);
}
// Public API
return {
processElement : processElement,
version : "0.0.1"
processElement: processNode,
on: addHTMxEventListener,
version: "0.0.2",
_:internalEval
}
})();
}
)();