lots of work:

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

295
dist/htmx.js vendored
View File

@ -123,7 +123,6 @@ var HTMx = HTMx || (function () {
function getTarget(elt) {
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

File diff suppressed because one or more lines are too long

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

Binary file not shown.

View File

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

View File

@ -123,7 +123,6 @@ var HTMx = HTMx || (function () {
function getTarget(elt) {
var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null});
if (explicitTarget) {
var targetStr = getRawAttribute(explicitTarget, "hx-target");
if (targetStr === "this") {
@ -141,59 +140,6 @@ var HTMx = HTMx || (function () {
}
}
function directSwap(child) {
var swapDirect = getAttributeValue(child, 'hx-swap-direct');
if (swapDirect) {
var target = getDocument().getElementById(getRawAttribute(child,'id'));
if (target) {
if (swapDirect === "merge") {
mergeInto(target, child);
} else {
var newParent = parentElt(target);
newParent.insertBefore(child, target);
newParent.removeChild(target);
return true;
}
}
}
return false;
}
function processResponseNodes(parentNode, insertBefore, text, executeAfter, selector) {
var fragment = makeFragment(text);
var nodesToProcess;
if (selector) {
nodesToProcess = toArray(fragment.querySelectorAll(selector));
} else {
nodesToProcess = toArray(fragment.childNodes);
}
forEach(nodesToProcess, function(child){
if (!directSwap(child)) {
parentNode.insertBefore(child, insertBefore);
}
if (child.nodeType !== Node.TEXT_NODE) {
triggerEvent(child, 'load.hx', {parent:parentElt(child)});
processNode(child);
}
});
if(executeAfter) {
executeAfter.call();
}
}
function findMatch(elt, possible) {
for (var i = 0; i < possible.length; i++) {
var candidate = possible[i];
if (elt.hasAttribute("id") && elt.id === candidate.id) {
return candidate;
}
if (!candidate.hasAttribute("id") && elt.tagName === candidate.tagName) {
return candidate;
}
}
return null;
}
function cloneAttributes(mergeTo, mergeFrom) {
forEach(mergeTo.attributes, function (attr) {
if (!mergeFrom.hasAttribute(attr.name)) {
@ -205,58 +151,119 @@ var HTMx = HTMx || (function () {
});
}
function mergeChildren(mergeTo, mergeFrom) {
var oldChildren = toArray(mergeTo.children);
var marker = getDocument().createElement("span");
mergeTo.insertBefore(marker, mergeTo.firstChild);
forEach(mergeFrom.childNodes, function (newChild) {
var match = findMatch(newChild, oldChildren);
if (match) {
while (marker.nextSibling && marker.nextSibling !== match) {
mergeTo.removeChild(marker.nextSibling);
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) {

View File

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

View File

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

View File

@ -1,23 +1,18 @@
<html lang="en">
<head>
<style>
div {
transition: all 1000ms ease-in;
}
.indicator {
opacity: 0;
transition: all 200ms ease-in;
}
.hx-show-indicator .indicator {
opacity: 100%;
}
div {
transition: all 1000ms ease-in;
}
div.foo {
color: red;
transition: all 1000ms ease-in;
}
</style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
@ -29,9 +24,11 @@
<script src="scratch_server.js"></script>
<script>
this.server.respondWith("POST", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test" method="post"><button id="b1">Submit</button></form></div>');
var btn = byId('b1');
// this.server.respondWith("GET", "/test", "Clicked!");
// var btn = make('<button hx-get="/test">Click Me!</button>')
this.server.respondWith("GET", "/test", '<div id="d1" style="color: red; margin: 100px">Foo</div>');
make('<div hx-swap="outerHTML" hx-get="/test" hx-push-url="true" id="d1">Foo</div>');
</script>
@ -45,6 +42,7 @@ Autorespond: <input id="autorespond" type="checkbox" onclick="toggleAutoRespond(
<hr/>
<div id="work-area" class="hx-history-element">
</div>
</body>

View File

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

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

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

View File

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

244
www/docs.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff