mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-09-28 05:21:18 +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) {
|
||||
var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null});
|
||||
|
||||
if (explicitTarget) {
|
||||
var targetStr = getRawAttribute(explicitTarget, "hx-target");
|
||||
if (targetStr === "this") {
|
||||
@ -141,59 +140,6 @@ var HTMx = HTMx || (function () {
|
||||
}
|
||||
}
|
||||
|
||||
function directSwap(child) {
|
||||
var swapDirect = getAttributeValue(child, 'hx-swap-direct');
|
||||
if (swapDirect) {
|
||||
var target = getDocument().getElementById(getRawAttribute(child,'id'));
|
||||
if (target) {
|
||||
if (swapDirect === "merge") {
|
||||
mergeInto(target, child);
|
||||
} else {
|
||||
var newParent = parentElt(target);
|
||||
newParent.insertBefore(child, target);
|
||||
newParent.removeChild(target);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function processResponseNodes(parentNode, insertBefore, text, executeAfter, selector) {
|
||||
var fragment = makeFragment(text);
|
||||
var nodesToProcess;
|
||||
if (selector) {
|
||||
nodesToProcess = toArray(fragment.querySelectorAll(selector));
|
||||
} else {
|
||||
nodesToProcess = toArray(fragment.childNodes);
|
||||
}
|
||||
forEach(nodesToProcess, function(child){
|
||||
if (!directSwap(child)) {
|
||||
parentNode.insertBefore(child, insertBefore);
|
||||
}
|
||||
if (child.nodeType !== Node.TEXT_NODE) {
|
||||
triggerEvent(child, 'load.hx', {parent:parentElt(child)});
|
||||
processNode(child);
|
||||
}
|
||||
});
|
||||
if(executeAfter) {
|
||||
executeAfter.call();
|
||||
}
|
||||
}
|
||||
|
||||
function findMatch(elt, possible) {
|
||||
for (var i = 0; i < possible.length; i++) {
|
||||
var candidate = possible[i];
|
||||
if (elt.hasAttribute("id") && elt.id === candidate.id) {
|
||||
return candidate;
|
||||
}
|
||||
if (!candidate.hasAttribute("id") && elt.tagName === candidate.tagName) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function cloneAttributes(mergeTo, mergeFrom) {
|
||||
forEach(mergeTo.attributes, function (attr) {
|
||||
if (!mergeFrom.hasAttribute(attr.name)) {
|
||||
@ -205,58 +151,119 @@ var HTMx = HTMx || (function () {
|
||||
});
|
||||
}
|
||||
|
||||
function mergeChildren(mergeTo, mergeFrom) {
|
||||
var oldChildren = toArray(mergeTo.children);
|
||||
var marker = getDocument().createElement("span");
|
||||
mergeTo.insertBefore(marker, mergeTo.firstChild);
|
||||
forEach(mergeFrom.childNodes, function (newChild) {
|
||||
var match = findMatch(newChild, oldChildren);
|
||||
if (match) {
|
||||
while (marker.nextSibling && marker.nextSibling !== match) {
|
||||
mergeTo.removeChild(marker.nextSibling);
|
||||
function handleOutOfBandSwaps(fragment) {
|
||||
forEach(fragment.children, function(child){
|
||||
if (getAttributeValue(child, "hx-swap-oob") === "true") {
|
||||
var target = getDocument().getElementById(child.id);
|
||||
if (target) {
|
||||
var fragment = new DocumentFragment()
|
||||
fragment.append(child);
|
||||
swapOuterHTML(target, fragment);
|
||||
} else {
|
||||
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) {
|
||||
mergeTo.removeChild(marker.nextSibling);
|
||||
setTimeout(function () {
|
||||
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) {
|
||||
cloneAttributes(mergeTo, mergeFrom);
|
||||
mergeChildren(mergeTo, mergeFrom);
|
||||
function swapOuterHTML(target, fragment) {
|
||||
if (target.tagName === "BODY") {
|
||||
swapInnerHTML(target, fragment);
|
||||
} else {
|
||||
insertNodesBefore(parentElt(target), target, fragment);
|
||||
parentElt(target).removeChild(target);
|
||||
}
|
||||
}
|
||||
|
||||
function mergeResponse(target, resp, selector) {
|
||||
var fragment = makeFragment(resp);
|
||||
mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild);
|
||||
function swapPrepend(target, fragment) {
|
||||
insertNodesBefore(target, target.firstChild, fragment);
|
||||
}
|
||||
|
||||
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 selector = getClosestAttributeValue(elt, "hx-select");
|
||||
if (swapStyle === "merge") {
|
||||
mergeResponse(target, resp, selector);
|
||||
} else if (swapStyle === "outerHTML") {
|
||||
processResponseNodes(parentElt(target), target, resp, after, selector);
|
||||
parentElt(target).removeChild(target);
|
||||
if (swapStyle === "outerHTML") {
|
||||
swapOuterHTML(target, fragment);
|
||||
} else if (swapStyle === "prepend") {
|
||||
processResponseNodes(target, target.firstChild, resp, after, selector);
|
||||
swapPrepend(target, fragment);
|
||||
} else if (swapStyle === "prependBefore") {
|
||||
processResponseNodes(parentElt(target), target, resp, after, selector);
|
||||
swapPrependBefore(target, fragment);
|
||||
} else if (swapStyle === "append") {
|
||||
processResponseNodes(target, null, resp, after, selector);
|
||||
swapAppend(target, fragment);
|
||||
} else if (swapStyle === "appendAfter") {
|
||||
processResponseNodes(parentElt(target), target.nextSibling, resp, after, selector);
|
||||
swapAppendAfter(target, fragment);
|
||||
} else {
|
||||
target.innerHTML = "";
|
||||
processResponseNodes(target, null, resp, after, selector);
|
||||
swapInnerHTML(target, fragment);
|
||||
}
|
||||
|
||||
if(callBack) {
|
||||
callBack.call();
|
||||
}
|
||||
}
|
||||
|
||||
@ -423,9 +430,12 @@ var HTMx = HTMx || (function () {
|
||||
}
|
||||
|
||||
function initSSESource(elt, sseSrc) {
|
||||
var config = {withCredentials: true};
|
||||
triggerEvent(elt, "initSSE.mx", config)
|
||||
var source = new EventSource(sseSrc);
|
||||
var details = {
|
||||
initializer: function() { new EventSource(sseSrc, details.config) },
|
||||
config:{withCredentials: true}
|
||||
};
|
||||
triggerEvent(elt, "initSSE.mx", {config:details})
|
||||
var source = details.initializer();
|
||||
source.onerror = function (e) {
|
||||
triggerEvent(elt, "sseError.mx", {error:e, source:source});
|
||||
maybeCloseSSESource(elt);
|
||||
@ -433,51 +443,67 @@ var HTMx = HTMx || (function () {
|
||||
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) {
|
||||
var nodeData = getInternalData(elt);
|
||||
if (!nodeData.processed) {
|
||||
nodeData.processed = true;
|
||||
|
||||
var trigger = getTrigger(elt);
|
||||
var explicitAction = false;
|
||||
forEach(VERBS, function(verb){
|
||||
var path = getAttributeValue(elt, 'hx-' + verb);
|
||||
if (path) {
|
||||
nodeData.path = path;
|
||||
nodeData.verb = verb;
|
||||
explicitAction = true;
|
||||
if (trigger.indexOf("sse:") === 0) {
|
||||
var sseEventName = trigger.substr(4);
|
||||
var sseSource = getClosestMatch(elt, function(parent) {return parent.sseSource;});
|
||||
if (sseSource) {
|
||||
var sseListener = function () {
|
||||
if (!maybeCloseSSESource(sseSource)) {
|
||||
if (bodyContains(elt)) {
|
||||
issueAjaxRequest(elt, verb, path);
|
||||
} else {
|
||||
sseSource.sseSource.removeEventListener(sseEventName, sseListener);
|
||||
}
|
||||
}
|
||||
};
|
||||
sseSource.sseSource.addEventListener(sseEventName, sseListener);
|
||||
} else {
|
||||
triggerEvent(elt, "noSSESourceError.mx")
|
||||
}
|
||||
} if (trigger === 'revealed') {
|
||||
initScrollHandler();
|
||||
maybeReveal(elt);
|
||||
} else if (trigger === 'load') {
|
||||
if (!nodeData.loaded) {
|
||||
nodeData.loaded = true;
|
||||
issueAjaxRequest(elt, verb, path);
|
||||
}
|
||||
} else if (trigger.trim().indexOf('every ') === 0) {
|
||||
nodeData.polling = true;
|
||||
processPolling(elt, verb, path);
|
||||
} else {
|
||||
addEventListener(elt, verb, path, nodeData, trigger);
|
||||
}
|
||||
}
|
||||
});
|
||||
var explicitAction = processVerbs(elt, nodeData, trigger);
|
||||
|
||||
if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") {
|
||||
boostElement(elt, nodeData, trigger);
|
||||
}
|
||||
@ -612,8 +638,7 @@ var HTMx = HTMx || (function () {
|
||||
var historyKey = data['hx-history-key'];
|
||||
var content = localStorage.getItem('hx-history-' + historyKey);
|
||||
var elt = getHistoryElement();
|
||||
elt.innerHTML = "";
|
||||
processResponseNodes(elt, null, content);
|
||||
swapInnerHTML(elt, makeFragment(content));
|
||||
}
|
||||
|
||||
function shouldPush(elt) {
|
||||
@ -895,7 +920,7 @@ var HTMx = HTMx || (function () {
|
||||
return {
|
||||
processElement: processNode,
|
||||
on: addHTMxEventListener,
|
||||
version: "0.0.1",
|
||||
version: "0.0.2",
|
||||
_:internalEval
|
||||
}
|
||||
}
|
||||
|
2
dist/htmx.min.js
vendored
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",
|
||||
"scripts": {
|
||||
"test": "mocha-chrome test/index.html",
|
||||
"dist": "cp src/htmx.js dist/ && npm run-script uglify && exit",
|
||||
"dist": "cp src/htmx.js dist/ && npm run-script uglify && gzip -k -f dist/htmx.min.js > dist/htmx.min.js.gz && exit",
|
||||
"www": "node scripts/www.js",
|
||||
"uglify": "uglifyjs -m eval -o dist/htmx.min.js dist/htmx.js"
|
||||
},
|
||||
|
295
src/htmx.js
295
src/htmx.js
@ -123,7 +123,6 @@ var HTMx = HTMx || (function () {
|
||||
|
||||
function getTarget(elt) {
|
||||
var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null});
|
||||
|
||||
if (explicitTarget) {
|
||||
var targetStr = getRawAttribute(explicitTarget, "hx-target");
|
||||
if (targetStr === "this") {
|
||||
@ -141,59 +140,6 @@ var HTMx = HTMx || (function () {
|
||||
}
|
||||
}
|
||||
|
||||
function directSwap(child) {
|
||||
var swapDirect = getAttributeValue(child, 'hx-swap-direct');
|
||||
if (swapDirect) {
|
||||
var target = getDocument().getElementById(getRawAttribute(child,'id'));
|
||||
if (target) {
|
||||
if (swapDirect === "merge") {
|
||||
mergeInto(target, child);
|
||||
} else {
|
||||
var newParent = parentElt(target);
|
||||
newParent.insertBefore(child, target);
|
||||
newParent.removeChild(target);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function processResponseNodes(parentNode, insertBefore, text, executeAfter, selector) {
|
||||
var fragment = makeFragment(text);
|
||||
var nodesToProcess;
|
||||
if (selector) {
|
||||
nodesToProcess = toArray(fragment.querySelectorAll(selector));
|
||||
} else {
|
||||
nodesToProcess = toArray(fragment.childNodes);
|
||||
}
|
||||
forEach(nodesToProcess, function(child){
|
||||
if (!directSwap(child)) {
|
||||
parentNode.insertBefore(child, insertBefore);
|
||||
}
|
||||
if (child.nodeType !== Node.TEXT_NODE) {
|
||||
triggerEvent(child, 'load.hx', {parent:parentElt(child)});
|
||||
processNode(child);
|
||||
}
|
||||
});
|
||||
if(executeAfter) {
|
||||
executeAfter.call();
|
||||
}
|
||||
}
|
||||
|
||||
function findMatch(elt, possible) {
|
||||
for (var i = 0; i < possible.length; i++) {
|
||||
var candidate = possible[i];
|
||||
if (elt.hasAttribute("id") && elt.id === candidate.id) {
|
||||
return candidate;
|
||||
}
|
||||
if (!candidate.hasAttribute("id") && elt.tagName === candidate.tagName) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function cloneAttributes(mergeTo, mergeFrom) {
|
||||
forEach(mergeTo.attributes, function (attr) {
|
||||
if (!mergeFrom.hasAttribute(attr.name)) {
|
||||
@ -205,58 +151,119 @@ var HTMx = HTMx || (function () {
|
||||
});
|
||||
}
|
||||
|
||||
function mergeChildren(mergeTo, mergeFrom) {
|
||||
var oldChildren = toArray(mergeTo.children);
|
||||
var marker = getDocument().createElement("span");
|
||||
mergeTo.insertBefore(marker, mergeTo.firstChild);
|
||||
forEach(mergeFrom.childNodes, function (newChild) {
|
||||
var match = findMatch(newChild, oldChildren);
|
||||
if (match) {
|
||||
while (marker.nextSibling && marker.nextSibling !== match) {
|
||||
mergeTo.removeChild(marker.nextSibling);
|
||||
function handleOutOfBandSwaps(fragment) {
|
||||
forEach(fragment.children, function(child){
|
||||
if (getAttributeValue(child, "hx-swap-oob") === "true") {
|
||||
var target = getDocument().getElementById(child.id);
|
||||
if (target) {
|
||||
var fragment = new DocumentFragment()
|
||||
fragment.append(child);
|
||||
swapOuterHTML(target, fragment);
|
||||
} else {
|
||||
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) {
|
||||
mergeTo.removeChild(marker.nextSibling);
|
||||
setTimeout(function () {
|
||||
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) {
|
||||
cloneAttributes(mergeTo, mergeFrom);
|
||||
mergeChildren(mergeTo, mergeFrom);
|
||||
function swapOuterHTML(target, fragment) {
|
||||
if (target.tagName === "BODY") {
|
||||
swapInnerHTML(target, fragment);
|
||||
} else {
|
||||
insertNodesBefore(parentElt(target), target, fragment);
|
||||
parentElt(target).removeChild(target);
|
||||
}
|
||||
}
|
||||
|
||||
function mergeResponse(target, resp, selector) {
|
||||
var fragment = makeFragment(resp);
|
||||
mergeInto(target, selector ? fragment.querySelector(selector) : fragment.firstElementChild);
|
||||
function swapPrepend(target, fragment) {
|
||||
insertNodesBefore(target, target.firstChild, fragment);
|
||||
}
|
||||
|
||||
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 selector = getClosestAttributeValue(elt, "hx-select");
|
||||
if (swapStyle === "merge") {
|
||||
mergeResponse(target, resp, selector);
|
||||
} else if (swapStyle === "outerHTML") {
|
||||
processResponseNodes(parentElt(target), target, resp, after, selector);
|
||||
parentElt(target).removeChild(target);
|
||||
if (swapStyle === "outerHTML") {
|
||||
swapOuterHTML(target, fragment);
|
||||
} else if (swapStyle === "prepend") {
|
||||
processResponseNodes(target, target.firstChild, resp, after, selector);
|
||||
swapPrepend(target, fragment);
|
||||
} else if (swapStyle === "prependBefore") {
|
||||
processResponseNodes(parentElt(target), target, resp, after, selector);
|
||||
swapPrependBefore(target, fragment);
|
||||
} else if (swapStyle === "append") {
|
||||
processResponseNodes(target, null, resp, after, selector);
|
||||
swapAppend(target, fragment);
|
||||
} else if (swapStyle === "appendAfter") {
|
||||
processResponseNodes(parentElt(target), target.nextSibling, resp, after, selector);
|
||||
swapAppendAfter(target, fragment);
|
||||
} else {
|
||||
target.innerHTML = "";
|
||||
processResponseNodes(target, null, resp, after, selector);
|
||||
swapInnerHTML(target, fragment);
|
||||
}
|
||||
|
||||
if(callBack) {
|
||||
callBack.call();
|
||||
}
|
||||
}
|
||||
|
||||
@ -436,51 +443,67 @@ var HTMx = HTMx || (function () {
|
||||
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) {
|
||||
var nodeData = getInternalData(elt);
|
||||
if (!nodeData.processed) {
|
||||
nodeData.processed = true;
|
||||
|
||||
var trigger = getTrigger(elt);
|
||||
var explicitAction = false;
|
||||
forEach(VERBS, function(verb){
|
||||
var path = getAttributeValue(elt, 'hx-' + verb);
|
||||
if (path) {
|
||||
nodeData.path = path;
|
||||
nodeData.verb = verb;
|
||||
explicitAction = true;
|
||||
if (trigger.indexOf("sse:") === 0) {
|
||||
var sseEventName = trigger.substr(4);
|
||||
var sseSource = getClosestMatch(elt, function(parent) {return parent.sseSource;});
|
||||
if (sseSource) {
|
||||
var sseListener = function () {
|
||||
if (!maybeCloseSSESource(sseSource)) {
|
||||
if (bodyContains(elt)) {
|
||||
issueAjaxRequest(elt, verb, path);
|
||||
} else {
|
||||
sseSource.sseSource.removeEventListener(sseEventName, sseListener);
|
||||
}
|
||||
}
|
||||
};
|
||||
sseSource.sseSource.addEventListener(sseEventName, sseListener);
|
||||
} else {
|
||||
triggerEvent(elt, "noSSESourceError.mx")
|
||||
}
|
||||
} if (trigger === 'revealed') {
|
||||
initScrollHandler();
|
||||
maybeReveal(elt);
|
||||
} else if (trigger === 'load') {
|
||||
if (!nodeData.loaded) {
|
||||
nodeData.loaded = true;
|
||||
issueAjaxRequest(elt, verb, path);
|
||||
}
|
||||
} else if (trigger.trim().indexOf('every ') === 0) {
|
||||
nodeData.polling = true;
|
||||
processPolling(elt, verb, path);
|
||||
} else {
|
||||
addEventListener(elt, verb, path, nodeData, trigger);
|
||||
}
|
||||
}
|
||||
});
|
||||
var explicitAction = processVerbs(elt, nodeData, trigger);
|
||||
|
||||
if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") {
|
||||
boostElement(elt, nodeData, trigger);
|
||||
}
|
||||
@ -575,10 +598,11 @@ var HTMx = HTMx || (function () {
|
||||
}
|
||||
|
||||
function saveLocalHistoryData(historyData) {
|
||||
triggerEvent(getDocument().body, "historySave.hx", {data:historyData});
|
||||
localStorage.setItem('hx-history', JSON.stringify(historyData));
|
||||
}
|
||||
|
||||
function getLocalHistoryData() {
|
||||
function getHistoryMetadata() {
|
||||
var historyEntry = localStorage.getItem('hx-history');
|
||||
var historyData;
|
||||
if (historyEntry) {
|
||||
@ -592,9 +616,10 @@ var HTMx = HTMx || (function () {
|
||||
}
|
||||
|
||||
function newHistoryData() {
|
||||
var historyData = getLocalHistoryData();
|
||||
var historyData = getHistoryMetadata();
|
||||
var newId = makeHistoryId();
|
||||
var slots = historyData.slots;
|
||||
triggerEvent(getDocument().body, "historyNew.hx", {data:historyData});
|
||||
if (slots.length > 20) {
|
||||
var toEvict = slots.shift();
|
||||
localStorage.removeItem('hx-history-' + toEvict);
|
||||
@ -606,17 +631,19 @@ var HTMx = HTMx || (function () {
|
||||
|
||||
function updateCurrentHistoryContent() {
|
||||
var elt = getHistoryElement();
|
||||
var historyData = getLocalHistoryData();
|
||||
var historyData = getHistoryMetadata();
|
||||
triggerEvent(getDocument().body, "historyUpdate.hx", {data:historyData});
|
||||
history.replaceState({"hx-history-key": historyData.current}, getDocument().title, window.location.href);
|
||||
localStorage.setItem('hx-history-' + historyData.current, elt.innerHTML);
|
||||
}
|
||||
|
||||
function restoreHistory(data) {
|
||||
updateCurrentHistoryContent();
|
||||
var historyKey = data['hx-history-key'];
|
||||
triggerEvent(getDocument().body, "historyUpdate.hx", {data:historyKey});
|
||||
var content = localStorage.getItem('hx-history-' + historyKey);
|
||||
var elt = getHistoryElement();
|
||||
elt.innerHTML = "";
|
||||
processResponseNodes(elt, null, content);
|
||||
swapInnerHTML(elt, makeFragment(content));
|
||||
}
|
||||
|
||||
function shouldPush(elt) {
|
||||
|
@ -25,7 +25,7 @@
|
||||
<script src="indicators.js"></script>
|
||||
<script src="values.js"></script>
|
||||
<script src="events.js"></script>
|
||||
<script src="swap_direct.js"></script>
|
||||
<script src="oob.js"></script>
|
||||
|
||||
<!--TODO figure out how to test stuff w/ history involved-->
|
||||
<!--<script src="history.js"></script>-->
|
||||
|
@ -9,7 +9,7 @@ describe("HTMx Direct Swap", function () {
|
||||
});
|
||||
|
||||
it('handles basic response properly', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-direct='true'>Swapped</div>");
|
||||
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-oob='true'>Swapped</div>");
|
||||
var div = make('<div hx-get="/test">click me</div>');
|
||||
make('<div id="d1"></div>');
|
||||
div.click();
|
||||
@ -19,11 +19,11 @@ describe("HTMx Direct Swap", function () {
|
||||
})
|
||||
|
||||
it('handles no id match properly', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-direct='true'>Swapped</div>");
|
||||
this.server.respondWith("GET", "/test", "Clicked<div id='d1' hx-swap-oob='true'>Swapped</div>");
|
||||
var div = make('<div hx-get="/test">click me</div>');
|
||||
div.click();
|
||||
this.server.respond();
|
||||
div.innerText.should.equal("Clicked\nSwapped");
|
||||
div.innerText.should.equal("Clicked");
|
||||
})
|
||||
|
||||
|
@ -1,23 +1,18 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<style>
|
||||
div {
|
||||
transition: all 1000ms ease-in;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
opacity: 0;
|
||||
transition: all 200ms ease-in;
|
||||
}
|
||||
|
||||
.hx-show-indicator .indicator {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
div {
|
||||
transition: all 1000ms ease-in;
|
||||
}
|
||||
|
||||
div.foo {
|
||||
color: red;
|
||||
transition: all 1000ms ease-in;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
|
||||
|
||||
@ -29,9 +24,11 @@
|
||||
<script src="scratch_server.js"></script>
|
||||
|
||||
<script>
|
||||
this.server.respondWith("POST", "/test", "Boosted");
|
||||
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test" method="post"><button id="b1">Submit</button></form></div>');
|
||||
var btn = byId('b1');
|
||||
// this.server.respondWith("GET", "/test", "Clicked!");
|
||||
// var btn = make('<button hx-get="/test">Click Me!</button>')
|
||||
|
||||
this.server.respondWith("GET", "/test", '<div id="d1" style="color: red; margin: 100px">Foo</div>');
|
||||
make('<div hx-swap="outerHTML" hx-get="/test" hx-push-url="true" id="d1">Foo</div>');
|
||||
</script>
|
||||
|
||||
|
||||
@ -45,6 +42,7 @@ Autorespond: <input id="autorespond" type="checkbox" onclick="toggleAutoRespond(
|
||||
<hr/>
|
||||
|
||||
<div id="work-area" class="hx-history-element">
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
@ -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 {
|
||||
margin: 40px auto;
|
||||
max-width: 740px;
|
||||
margin: 0px;
|
||||
line-height: 1.6;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
padding: 0 10px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif !important;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
line-height: 1.2
|
||||
}
|
||||
h1 {
|
||||
}
|
||||
.flair {
|
||||
|
||||
h2 {
|
||||
border-bottom: 2px solid whitesmoke;
|
||||
color: rgb(52, 101, 164);
|
||||
}
|
||||
|
||||
#content {
|
||||
margin-top: 32px
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
pre[class*="language-"] {
|
||||
font-size: 16px;
|
||||
margin-top: 24px !important;
|
||||
margin-bottom: 24px !important;
|
||||
margin-left: 48px !important;
|
||||
margin-right: 48px !important;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid whitesmoke;
|
||||
margin-left: 32px;
|
||||
margin-right: 32px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
font-size: 4em;
|
||||
font-size: 5em;
|
||||
margin: 0;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.root .hero {
|
||||
opacity: 5%;
|
||||
position: relative;
|
||||
top: -20px;
|
||||
}
|
||||
|
||||
.hero.settle {
|
||||
.root .hero.settle {
|
||||
top: 0px;
|
||||
opacity: 100%;
|
||||
transition: 500ms ease-in;
|
||||
@ -32,16 +55,125 @@ h1 {
|
||||
|
||||
.nav {
|
||||
margin: 12px;
|
||||
position: absolute;
|
||||
/*position: absolute;*/
|
||||
top: 180px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color:rgb(52, 101, 164)
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav ul {
|
||||
padding-left: 12px;
|
||||
list-style: none;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/* customized version of lit.css */
|
||||
* + *{
|
||||
box-sizing: border-box;
|
||||
margin: .5em 0;
|
||||
}
|
||||
|
||||
@media(max-width:45em) {
|
||||
.nav {
|
||||
text-align: center;
|
||||
}
|
||||
.nav ul {
|
||||
padding: 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
.nav li {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media(min-width:45em) {
|
||||
.col {
|
||||
display: table-cell;
|
||||
}
|
||||
.\31 {
|
||||
width: 5%;
|
||||
}
|
||||
.\33 {
|
||||
width: 22%;
|
||||
}
|
||||
.\34 {
|
||||
width: 30%;
|
||||
}
|
||||
.\35 {
|
||||
width: 40%;
|
||||
}
|
||||
.\32 {
|
||||
width: 15%;
|
||||
}
|
||||
.row {
|
||||
display: table;
|
||||
border-spacing: 1em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.w-100,
|
||||
.row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card:focus {
|
||||
outline: 0;
|
||||
border: solid rgb(52, 101, 164);
|
||||
}
|
||||
|
||||
hr {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1em;
|
||||
border: solid #eee;
|
||||
}
|
||||
|
||||
a[href]:hover, .btn:hover {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.c {
|
||||
max-width: 55em;
|
||||
padding: 1em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
color: white;
|
||||
background: rgb(52, 101, 164);
|
||||
border: solid rgb(52, 101, 164);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1em;
|
||||
text-align: left;
|
||||
border-bottom: solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 1em;
|
||||
text-align: left;
|
||||
border-bottom: solid #eee;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 1em;
|
||||
letter-spacing: .1em;
|
||||
text-transform: uppercase;
|
||||
background: white;
|
||||
border: solid;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
244
www/docs.md
Normal file
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
|
||||
---
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
layout: layout.html
|
||||
layout: layout.njk
|
||||
title: HTMx - HTML Extensions / Attributes
|
||||
---
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
layout: layout.html
|
||||
layout: layout.njk
|
||||
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
|
||||
---
|
||||
|
||||
HTMx is a set of extensions to HTML that bring many of the useful features of modern web browsers directly
|
||||
into HTML. It fills gaps in functionality found in standard HTML, dramatically expanding its expressiveness while
|
||||
retaining the fundamental simplicity of declarative hypertext.</p>
|
||||
## Introduction
|
||||
|
||||
Here is a simple example of HTMx in action:
|
||||
HTMx is a small (<12Kb) & dependency-free library that surfaces the features of modern browsers using HTML
|
||||
attributes. Using HTMx you can implement many [UX patterns](/demo) that would typically require writing javascript.
|
||||
|
||||
HTMx is unobtrusive, plays well with other tools, can be adopted incrementally with no up-front rewrites.
|
||||
|
||||
## Quick Start
|
||||
|
||||
``` html
|
||||
<button hx-get="/example" hx-target="#myDiv">
|
||||
Click Me
|
||||
</button>
|
||||
<!-- Load from unpkg -->
|
||||
<script src="https://unpkg.com/htmx.org@0.0.1"></script>
|
||||
|
||||
<!-- enhance a button -->
|
||||
<button hx-get="/example">Click Me</button>
|
||||
```
|
||||
|
||||
This example issues an AJAX request to <code>/example</code> when a user clicks on it, and swaps the response
|
||||
HTML into the element with the id `myDiv`
|
||||
This code tells HTMx that:
|
||||
|
||||
> "When a user clicks on this button, issue an AJAX request to /example, and load the content into the body
|
||||
> of the button"
|
||||
|
||||
HTMx is based on [intercooler.js](http://intercoolerjs.org) and is the successor to that project.
|
||||
|
||||
HTMx is based on [intercooler.js](http://intercoolerjs.org), and aims to be a minimalist &
|
||||
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