mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-10-02 15:25:26 +00:00
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:
parent
8be1f10636
commit
fe9dbb8b3e
295
dist/htmx.js
vendored
295
dist/htmx.js
vendored
@ -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
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
BIN
dist/htmx.min.js.gz
vendored
Normal file
Binary file not shown.
@ -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"
|
||||||
},
|
},
|
||||||
|
295
src/htmx.js
295
src/htmx.js
@ -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) {
|
||||||
|
@ -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>-->
|
||||||
|
@ -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");
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
@ -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"><<span class="flair">/</span>> 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
38
www/_includes/layout.njk
Normal 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"><<a>/</a>> 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>
|
160
www/css/site.css
160
www/css/site.css
@ -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
244
www/docs.md
Normal 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>
|
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
layout: layout.html
|
layout: layout.njk
|
||||||
title: HTMx - HTML Extensions / Attributes
|
title: HTMx - HTML Extensions / Attributes
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
layout: layout.html
|
layout: layout.njk
|
||||||
title: HTMx - HTML Extensions / Attributes
|
title: HTMx - HTML Extensions / Attributes
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
layout: layout.html
|
layout: layout.njk
|
||||||
title: HTMx - HTML Extensions / Attributes
|
title: HTMx - HTML Extensions / Attributes
|
||||||
---
|
---
|
||||||
|
|
||||||
|
31
www/index.md
31
www/index.md
@ -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) & 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 &
|
|
||||||
dependency free successor to that project.
|
|
||||||
|
1060
www/js/htmx.js
1060
www/js/htmx.js
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user