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

295
dist/htmx.js vendored
View File

@ -123,7 +123,6 @@ var HTMx = HTMx || (function () {
function getTarget(elt) { function getTarget(elt) {
var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null}); var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null});
if (explicitTarget) { if (explicitTarget) {
var targetStr = getRawAttribute(explicitTarget, "hx-target"); var targetStr = getRawAttribute(explicitTarget, "hx-target");
if (targetStr === "this") { 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) { function cloneAttributes(mergeTo, mergeFrom) {
forEach(mergeTo.attributes, function (attr) { forEach(mergeTo.attributes, function (attr) {
if (!mergeFrom.hasAttribute(attr.name)) { if (!mergeFrom.hasAttribute(attr.name)) {
@ -205,58 +151,119 @@ var HTMx = HTMx || (function () {
}); });
} }
function mergeChildren(mergeTo, mergeFrom) { function handleOutOfBandSwaps(fragment) {
var oldChildren = toArray(mergeTo.children); forEach(fragment.children, function(child){
var marker = getDocument().createElement("span"); if (getAttributeValue(child, "hx-swap-oob") === "true") {
mergeTo.insertBefore(marker, mergeTo.firstChild); var target = getDocument().getElementById(child.id);
forEach(mergeFrom.childNodes, function (newChild) { if (target) {
var match = findMatch(newChild, oldChildren); var fragment = new DocumentFragment()
if (match) { fragment.append(child);
while (marker.nextSibling && marker.nextSibling !== match) { swapOuterHTML(target, fragment);
mergeTo.removeChild(marker.nextSibling); } else {
child.parentNode.removeChild(child);
triggerEvent(getDocument().body, "oobErrorNoTarget.hx", {id:child.id, content:child})
} }
mergeTo.insertBefore(marker, match.nextSibling); }
mergeInto(match, newChild); })
} else { }
mergeTo.insertBefore(newChild, marker);
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) { setTimeout(function () {
mergeTo.removeChild(marker.nextSibling); forEach(attributeSwaps, function (swap) {
swap.call();
});
}, 100);
}
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);
}
} }
mergeTo.removeChild(marker);
} }
function mergeInto(mergeTo, mergeFrom) { function swapOuterHTML(target, fragment) {
cloneAttributes(mergeTo, mergeFrom); if (target.tagName === "BODY") {
mergeChildren(mergeTo, mergeFrom); swapInnerHTML(target, fragment);
} else {
insertNodesBefore(parentElt(target), target, fragment);
parentElt(target).removeChild(target);
}
} }
function mergeResponse(target, resp, selector) { function swapPrepend(target, fragment) {
var fragment = makeFragment(resp); insertNodesBefore(target, target.firstChild, fragment);
mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild);
} }
function swapResponse(target, elt, resp, after) { 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 swapStyle = getClosestAttributeValue(elt, "hx-swap");
var selector = getClosestAttributeValue(elt, "hx-select"); if (swapStyle === "outerHTML") {
if (swapStyle === "merge") { swapOuterHTML(target, fragment);
mergeResponse(target, resp, selector);
} else if (swapStyle === "outerHTML") {
processResponseNodes(parentElt(target), target, resp, after, selector);
parentElt(target).removeChild(target);
} else if (swapStyle === "prepend") { } else if (swapStyle === "prepend") {
processResponseNodes(target, target.firstChild, resp, after, selector); swapPrepend(target, fragment);
} else if (swapStyle === "prependBefore") { } else if (swapStyle === "prependBefore") {
processResponseNodes(parentElt(target), target, resp, after, selector); swapPrependBefore(target, fragment);
} else if (swapStyle === "append") { } else if (swapStyle === "append") {
processResponseNodes(target, null, resp, after, selector); swapAppend(target, fragment);
} else if (swapStyle === "appendAfter") { } else if (swapStyle === "appendAfter") {
processResponseNodes(parentElt(target), target.nextSibling, resp, after, selector); swapAppendAfter(target, fragment);
} else { } else {
target.innerHTML = ""; swapInnerHTML(target, fragment);
processResponseNodes(target, null, resp, after, selector); }
if(callBack) {
callBack.call();
} }
} }
@ -423,9 +430,12 @@ var HTMx = HTMx || (function () {
} }
function initSSESource(elt, sseSrc) { function initSSESource(elt, sseSrc) {
var config = {withCredentials: true}; var details = {
triggerEvent(elt, "initSSE.mx", config) initializer: function() { new EventSource(sseSrc, details.config) },
var source = new EventSource(sseSrc); config:{withCredentials: true}
};
triggerEvent(elt, "initSSE.mx", {config:details})
var source = details.initializer();
source.onerror = function (e) { source.onerror = function (e) {
triggerEvent(elt, "sseError.mx", {error:e, source:source}); triggerEvent(elt, "sseError.mx", {error:e, source:source});
maybeCloseSSESource(elt); maybeCloseSSESource(elt);
@ -433,51 +443,67 @@ var HTMx = HTMx || (function () {
getInternalData(elt).sseSource = source; getInternalData(elt).sseSource = source;
} }
function processSSETrigger(sseEventName, elt, verb, path) {
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")
}
}
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);
} else {
addEventListener(elt, verb, path, nodeData, trigger);
}
}
});
return explicitAction;
}
function processNode(elt) { function processNode(elt) {
var nodeData = getInternalData(elt); var nodeData = getInternalData(elt);
if (!nodeData.processed) { if (!nodeData.processed) {
nodeData.processed = true; nodeData.processed = true;
var trigger = getTrigger(elt); var trigger = getTrigger(elt);
var explicitAction = false; var explicitAction = processVerbs(elt, nodeData, trigger);
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") { if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") {
boostElement(elt, nodeData, trigger); boostElement(elt, nodeData, trigger);
} }
@ -612,8 +638,7 @@ var HTMx = HTMx || (function () {
var historyKey = data['hx-history-key']; var historyKey = data['hx-history-key'];
var content = localStorage.getItem('hx-history-' + historyKey); var content = localStorage.getItem('hx-history-' + historyKey);
var elt = getHistoryElement(); var elt = getHistoryElement();
elt.innerHTML = ""; swapInnerHTML(elt, makeFragment(content));
processResponseNodes(elt, null, content);
} }
function shouldPush(elt) { function shouldPush(elt) {
@ -895,7 +920,7 @@ var HTMx = HTMx || (function () {
return { return {
processElement: processNode, processElement: processNode,
on: addHTMxEventListener, on: addHTMxEventListener,
version: "0.0.1", version: "0.0.2",
_:internalEval _: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", "unpkg": "dist/htmx.min.js",
"scripts": { "scripts": {
"test": "mocha-chrome test/index.html", "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", "www": "node scripts/www.js",
"uglify": "uglifyjs -m eval -o dist/htmx.min.js dist/htmx.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) { function getTarget(elt) {
var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null}); var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null});
if (explicitTarget) { if (explicitTarget) {
var targetStr = getRawAttribute(explicitTarget, "hx-target"); var targetStr = getRawAttribute(explicitTarget, "hx-target");
if (targetStr === "this") { 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) { function cloneAttributes(mergeTo, mergeFrom) {
forEach(mergeTo.attributes, function (attr) { forEach(mergeTo.attributes, function (attr) {
if (!mergeFrom.hasAttribute(attr.name)) { if (!mergeFrom.hasAttribute(attr.name)) {
@ -205,58 +151,119 @@ var HTMx = HTMx || (function () {
}); });
} }
function mergeChildren(mergeTo, mergeFrom) { function handleOutOfBandSwaps(fragment) {
var oldChildren = toArray(mergeTo.children); forEach(fragment.children, function(child){
var marker = getDocument().createElement("span"); if (getAttributeValue(child, "hx-swap-oob") === "true") {
mergeTo.insertBefore(marker, mergeTo.firstChild); var target = getDocument().getElementById(child.id);
forEach(mergeFrom.childNodes, function (newChild) { if (target) {
var match = findMatch(newChild, oldChildren); var fragment = new DocumentFragment()
if (match) { fragment.append(child);
while (marker.nextSibling && marker.nextSibling !== match) { swapOuterHTML(target, fragment);
mergeTo.removeChild(marker.nextSibling); } else {
child.parentNode.removeChild(child);
triggerEvent(getDocument().body, "oobErrorNoTarget.hx", {id:child.id, content:child})
} }
mergeTo.insertBefore(marker, match.nextSibling); }
mergeInto(match, newChild); })
} else { }
mergeTo.insertBefore(newChild, marker);
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) { setTimeout(function () {
mergeTo.removeChild(marker.nextSibling); forEach(attributeSwaps, function (swap) {
swap.call();
});
}, 100);
}
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);
}
} }
mergeTo.removeChild(marker);
} }
function mergeInto(mergeTo, mergeFrom) { function swapOuterHTML(target, fragment) {
cloneAttributes(mergeTo, mergeFrom); if (target.tagName === "BODY") {
mergeChildren(mergeTo, mergeFrom); swapInnerHTML(target, fragment);
} else {
insertNodesBefore(parentElt(target), target, fragment);
parentElt(target).removeChild(target);
}
} }
function mergeResponse(target, resp, selector) { function swapPrepend(target, fragment) {
var fragment = makeFragment(resp); insertNodesBefore(target, target.firstChild, fragment);
mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild);
} }
function swapResponse(target, elt, resp, after) { 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 swapStyle = getClosestAttributeValue(elt, "hx-swap");
var selector = getClosestAttributeValue(elt, "hx-select"); if (swapStyle === "outerHTML") {
if (swapStyle === "merge") { swapOuterHTML(target, fragment);
mergeResponse(target, resp, selector);
} else if (swapStyle === "outerHTML") {
processResponseNodes(parentElt(target), target, resp, after, selector);
parentElt(target).removeChild(target);
} else if (swapStyle === "prepend") { } else if (swapStyle === "prepend") {
processResponseNodes(target, target.firstChild, resp, after, selector); swapPrepend(target, fragment);
} else if (swapStyle === "prependBefore") { } else if (swapStyle === "prependBefore") {
processResponseNodes(parentElt(target), target, resp, after, selector); swapPrependBefore(target, fragment);
} else if (swapStyle === "append") { } else if (swapStyle === "append") {
processResponseNodes(target, null, resp, after, selector); swapAppend(target, fragment);
} else if (swapStyle === "appendAfter") { } else if (swapStyle === "appendAfter") {
processResponseNodes(parentElt(target), target.nextSibling, resp, after, selector); swapAppendAfter(target, fragment);
} else { } else {
target.innerHTML = ""; swapInnerHTML(target, fragment);
processResponseNodes(target, null, resp, after, selector); }
if(callBack) {
callBack.call();
} }
} }
@ -436,51 +443,67 @@ var HTMx = HTMx || (function () {
getInternalData(elt).sseSource = source; getInternalData(elt).sseSource = source;
} }
function processSSETrigger(sseEventName, elt, verb, path) {
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")
}
}
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);
} else {
addEventListener(elt, verb, path, nodeData, trigger);
}
}
});
return explicitAction;
}
function processNode(elt) { function processNode(elt) {
var nodeData = getInternalData(elt); var nodeData = getInternalData(elt);
if (!nodeData.processed) { if (!nodeData.processed) {
nodeData.processed = true; nodeData.processed = true;
var trigger = getTrigger(elt); var trigger = getTrigger(elt);
var explicitAction = false; var explicitAction = processVerbs(elt, nodeData, trigger);
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") { if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") {
boostElement(elt, nodeData, trigger); boostElement(elt, nodeData, trigger);
} }
@ -575,10 +598,11 @@ var HTMx = HTMx || (function () {
} }
function saveLocalHistoryData(historyData) { function saveLocalHistoryData(historyData) {
triggerEvent(getDocument().body, "historySave.hx", {data:historyData});
localStorage.setItem('hx-history', JSON.stringify(historyData)); localStorage.setItem('hx-history', JSON.stringify(historyData));
} }
function getLocalHistoryData() { function getHistoryMetadata() {
var historyEntry = localStorage.getItem('hx-history'); var historyEntry = localStorage.getItem('hx-history');
var historyData; var historyData;
if (historyEntry) { if (historyEntry) {
@ -592,9 +616,10 @@ var HTMx = HTMx || (function () {
} }
function newHistoryData() { function newHistoryData() {
var historyData = getLocalHistoryData(); var historyData = getHistoryMetadata();
var newId = makeHistoryId(); var newId = makeHistoryId();
var slots = historyData.slots; var slots = historyData.slots;
triggerEvent(getDocument().body, "historyNew.hx", {data:historyData});
if (slots.length > 20) { if (slots.length > 20) {
var toEvict = slots.shift(); var toEvict = slots.shift();
localStorage.removeItem('hx-history-' + toEvict); localStorage.removeItem('hx-history-' + toEvict);
@ -606,17 +631,19 @@ var HTMx = HTMx || (function () {
function updateCurrentHistoryContent() { function updateCurrentHistoryContent() {
var elt = getHistoryElement(); 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); history.replaceState({"hx-history-key": historyData.current}, getDocument().title, window.location.href);
localStorage.setItem('hx-history-' + historyData.current, elt.innerHTML); localStorage.setItem('hx-history-' + historyData.current, elt.innerHTML);
} }
function restoreHistory(data) { function restoreHistory(data) {
updateCurrentHistoryContent();
var historyKey = data['hx-history-key']; var historyKey = data['hx-history-key'];
triggerEvent(getDocument().body, "historyUpdate.hx", {data:historyKey});
var content = localStorage.getItem('hx-history-' + historyKey); var content = localStorage.getItem('hx-history-' + historyKey);
var elt = getHistoryElement(); var elt = getHistoryElement();
elt.innerHTML = ""; swapInnerHTML(elt, makeFragment(content));
processResponseNodes(elt, null, content);
} }
function shouldPush(elt) { function shouldPush(elt) {

View File

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

View File

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

View File

@ -1,23 +1,18 @@
<html lang="en"> <html lang="en">
<head> <head>
<style> <style>
div {
transition: all 1000ms ease-in;
}
.indicator { .indicator {
opacity: 0; opacity: 0;
transition: all 200ms ease-in;
} }
.hx-show-indicator .indicator { .hx-show-indicator .indicator {
opacity: 100%; opacity: 100%;
} }
div {
transition: all 1000ms ease-in;
}
div.foo {
color: red;
transition: all 1000ms ease-in;
}
</style> </style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"> <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 src="scratch_server.js"></script>
<script> <script>
this.server.respondWith("POST", "/test", "Boosted"); // this.server.respondWith("GET", "/test", "Clicked!");
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 = make('<button hx-get="/test">Click Me!</button>')
var btn = byId('b1');
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> </script>
@ -45,6 +42,7 @@ Autorespond: <input id="autorespond" type="checkbox" onclick="toggleAutoRespond(
<hr/> <hr/>
<div id="work-area" class="hx-history-element"> <div id="work-area" class="hx-history-element">
</div> </div>
</body> </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 { body {
margin: 40px auto; margin: 0px;
max-width: 740px;
line-height: 1.6; line-height: 1.6;
font-size: 18px; font-size: 18px;
color: #333; color: #333;
padding: 0 10px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif !important;
} }
h1, h2, h3 {
line-height: 1.2 h2 {
} border-bottom: 2px solid whitesmoke;
h1 {
}
.flair {
color: rgb(52, 101, 164); 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 { .hero {
text-align: center; text-align: center;
font-size: 4em; font-size: 5em;
margin: 0;
line-height: 1em;
}
.root .hero {
opacity: 5%; opacity: 5%;
position: relative; position: relative;
top: -20px; top: -20px;
} }
.hero.settle { .root .hero.settle {
top: 0px; top: 0px;
opacity: 100%; opacity: 100%;
transition: 500ms ease-in; transition: 500ms ease-in;
@ -32,16 +55,125 @@ h1 {
.nav { .nav {
margin: 12px; margin: 12px;
position: absolute; /*position: absolute;*/
top: 180px; top: 180px;
left: 10px; left: 10px;
} }
a { a {
text-decoration: none; text-decoration: none;
color:rgb(52, 101, 164)
}
.center {
text-align: center;
} }
.nav ul { .nav ul {
padding-left: 12px;
list-style: none; 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 title: HTMx - HTML Extensions / Attributes
--- ---

View File

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

View File

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

View File

@ -1,22 +1,29 @@
--- ---
layout: layout.html layout: layout.njk
title: HTMx - HTML Extensions title: HTMx - HTML Extensions
--- ---
HTMx is a set of extensions to HTML that bring many of the useful features of modern web browsers directly ## Introduction
into HTML. It fills gaps in functionality found in standard HTML, dramatically expanding its expressiveness while
retaining the fundamental simplicity of declarative hypertext.</p>
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 ``` html
<button hx-get="/example" hx-target="#myDiv"> <!-- Load from unpkg -->
Click Me <script src="https://unpkg.com/htmx.org@0.0.1"></script>
</button>
<!-- 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 This code tells HTMx that:
HTML into the element with the id `myDiv`
> "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.

File diff suppressed because it is too large Load Diff