hx-boost attribute

This commit is contained in:
carson 2020-05-02 05:31:18 -07:00
parent 474ea8ed74
commit d1b37b387b
5 changed files with 182 additions and 71 deletions

View File

@ -12,10 +12,8 @@
## TODOS ## TODOS
* hx-swap="merge"
* hx-swap-direct="merge"
* hx-boost
* hx-error-url * hx-error-url
* hx-select (select from response)
* transition model for content swaps * transition model for content swaps
* history support * history support
* Implement LRU * Implement LRU
@ -23,6 +21,11 @@
* sse support * sse support
* delay (ic-trigger="keyup" ic-delay="1s") * delay (ic-trigger="keyup" ic-delay="1s")
* change support * change support
* Testing
* polling
* history
* merge
* hx-boost
* distribute on https://unpkg.com/ * distribute on https://unpkg.com/
* build website with 11ty * build website with 11ty
* landing page * landing page

View File

@ -37,20 +37,28 @@ var HTMx = HTMx || (function () {
return document; return document;
} }
function getClosestAttributeValue(elt, attributeName) { function getClosestMatch(elt, condition) {
var attribute = getAttributeValue(elt, attributeName); if (condition(elt)) {
if (attribute) { return elt;
return attribute;
} else if (parentElt(elt)) { } else if (parentElt(elt)) {
return getClosestAttributeValue(parentElt(elt, attributeName)); return getClosestMatch(parentElt(elt), condition);
} else { } else {
return null; return null;
} }
} }
function getClosestAttributeValue(elt, attributeName) {
var closestAttr = null;
getClosestMatch(elt, function (e) {
return closestAttr = getRawAttribute(e, attributeName);
});
return closestAttr;
}
function matches(elt, selector) { function matches(elt, selector) {
// noinspection JSUnresolvedVariable // noinspection JSUnresolvedVariable
return (elt != null) &&(elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector).call(elt, selector); return (elt != null) &&(elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector
|| elt.webkitMatchesSelector || elt.oMatchesSelector).call(elt, selector);
} }
function closest (elt, selector) { function closest (elt, selector) {
@ -78,14 +86,13 @@ var HTMx = HTMx || (function () {
function toArray(object) { function toArray(object) {
var arr = []; var arr = [];
for (var i = 0; i < object.length; i++) { forEach(object, function(elt) {
arr.push(object[i]) arr.push(elt)
} });
return arr; return arr;
} }
function forEach(arr, func) { function forEach(arr, func) {
for (var i = 0; i < arr.length; i++) { for (var i = 0; i < arr.length; i++) {
func(arr[i]); func(arr[i]);
} }
} }
@ -95,11 +102,22 @@ var HTMx = HTMx || (function () {
//==================================================================== //====================================================================
function getTarget(elt) { function getTarget(elt) {
var targetVal = getClosestAttributeValue(elt, "hx-target"); var explicitTarget = getClosestMatch(elt, function(e){return getRawAttribute(e,"hx-target") !== null});
if (targetVal) {
return getDocument().querySelector(targetVal); if (explicitTarget) {
var targetStr = getRawAttribute(explicitTarget, "hx-target");
if (targetStr === "this") {
return explicitTarget;
} else {
return getDocument().querySelector(targetStr);
}
} else { } else {
return elt; var data = getInternalData(elt);
if (data.boosted) {
return getDocument().body;
} else {
return elt;
}
} }
} }
@ -129,7 +147,7 @@ var HTMx = HTMx || (function () {
} }
if (child.nodeType !== Node.TEXT_NODE) { if (child.nodeType !== Node.TEXT_NODE) {
triggerEvent(child, 'load.hx', {parent:parentElt(child)}); triggerEvent(child, 'load.hx', {parent:parentElt(child)});
processElement(child); processNode(child);
} }
}) })
if(executeAfter) { if(executeAfter) {
@ -300,46 +318,76 @@ var HTMx = HTMx || (function () {
} }
} }
function processElement(elt) { function isLocalLink(elt) {
var nodeData = getInternalData(elt); return location.hostname === elt.hostname &&
if (nodeData.processed) { getRawAttribute(elt,'href') &&
return; !getRawAttribute(elt,'href').startsWith("#")
} else { }
nodeData.processed = true;
} function boostElement(elt, nodeData, trigger) {
forEach(VERBS, function(verb){ if ((elt.tagName === "A" && isLocalLink(elt)) || elt.tagName === "FORM") {
var path = getAttributeValue(elt, 'hx-' + verb); nodeData.boosted = true;
if (path) { var verb, path;
var trigger = getTrigger(elt); if (elt.tagName === "A") {
if (trigger === 'load') { verb = "get";
if (!nodeData.loaded) { path = getRawAttribute(elt, 'href');
nodeData.loaded = true; } else {
issueAjaxRequest(elt, verb, path); var rawAttribute = getRawAttribute(elt, "method");
} verb = rawAttribute ? rawAttribute.toLowerCase() : "get";
} else if (trigger.trim().indexOf('every ') === 0) { path = getRawAttribute(elt, 'action');
nodeData.polling = true;
processPolling(elt, verb, path);
} else {
var eventListener = function (evt) {
var eventData = getInternalData(evt);
if (!eventData.handled) {
eventData.handled = true;
issueAjaxRequest(elt, verb, path, evt.target);
}
};
nodeData.trigger = trigger;
nodeData.eventListener = eventListener;
elt.addEventListener(trigger, eventListener);
}
} }
}); addEventListener(elt, verb, path, nodeData, trigger, true);
if (getAttributeValue(elt, 'hx-add-class')) {
processClassList(elt, getAttributeValue(elt, 'hx-add-class'), "add");
} }
if (getAttributeValue(elt, 'hx-remove-class')) { }
processClassList(elt, getAttributeValue(elt, 'hx-remove-class'), "remove");
function addEventListener(elt, verb, path, nodeData, trigger, cancel) {
var eventListener = function (evt) {
if(cancel) evt.preventDefault();
var eventData = getInternalData(evt);
if (!eventData.handled) {
eventData.handled = true;
issueAjaxRequest(elt, verb, path, evt.target);
}
};
nodeData.trigger = trigger;
nodeData.eventListener = eventListener;
elt.addEventListener(trigger, eventListener);
}
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) {
explicitAction = true;
if (trigger === 'load') {
if (!nodeData.loaded) {
nodeData.loaded = true;
issueAjaxRequest(elt, verb, path);
}
} else if (trigger.trim().indexOf('every ') === 0) {
nodeData.polling = true;
processPolling(elt, verb, path);
} else {
addEventListener(elt, verb, path, nodeData, trigger);
}
}
});
if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") == "true") {
boostElement(elt, nodeData, trigger);
}
if (getAttributeValue(elt, 'hx-add-class')) {
processClassList(elt, getAttributeValue(elt, 'hx-add-class'), "add");
}
if (getAttributeValue(elt, 'hx-remove-class')) {
processClassList(elt, getAttributeValue(elt, 'hx-remove-class'), "remove");
}
} }
forEach(elt.children, function(child) { processElement(child) }); forEach(elt.children, function(child) { processNode(child) });
} }
//==================================================================== //====================================================================
@ -404,15 +452,20 @@ var HTMx = HTMx || (function () {
processResponseNodes(elt, null, content); processResponseNodes(elt, null, content);
} }
function shouldPush(elt) {
return getClosestAttributeValue(elt, "hx-push-url") === "true" ||
(elt.tagName === "A" && getInternalData(elt).boosted);
}
function snapshotForCurrentHistoryEntry(elt) { function snapshotForCurrentHistoryEntry(elt) {
if (getClosestAttributeValue(elt, "hx-push-url") === "true") { if (shouldPush(elt)) {
// TODO event to allow de-initialization of HTML elements in target // TODO event to allow de-initialization of HTML elements in target
updateCurrentHistoryContent(); updateCurrentHistoryContent();
} }
} }
function initNewHistoryEntry(elt, url) { function initNewHistoryEntry(elt, url) {
if (getClosestAttributeValue(elt, "hx-push-url") === "true") { if (shouldPush(elt)) {
newHistoryData(); newHistoryData();
history.pushState({}, "", url); history.pushState({}, "", url);
updateCurrentHistoryContent(); updateCurrentHistoryContent();
@ -475,9 +528,9 @@ var HTMx = HTMx || (function () {
} }
if (matches(elt, 'form')) { if (matches(elt, 'form')) {
var inputs = elt.elements; var inputs = elt.elements;
for (var i = 0; i < inputs.length; i++) { forEach(inputs, function(input) {
processInputValue(processed, values, inputs[i]); processInputValue(processed, values, input);
} });
} }
} }
@ -491,10 +544,9 @@ var HTMx = HTMx || (function () {
var includes = getAttributeValue(elt, "hx-include"); var includes = getAttributeValue(elt, "hx-include");
if (includes) { if (includes) {
var nodes = getDocument().querySelectorAll(includes); var nodes = getDocument().querySelectorAll(includes);
for (var i = 0; i < nodes.length; i++) { forEach(nodes, function(node) {
var node = nodes[i];
processInputValue(processed, values, node); processInputValue(processed, values, node);
} });
} }
// include the closest form // include the closest form
@ -516,9 +568,9 @@ var HTMx = HTMx || (function () {
if (values.hasOwnProperty(name)) { if (values.hasOwnProperty(name)) {
var value = values[name]; var value = values[name];
if (Array.isArray(value)) { if (Array.isArray(value)) {
for (var i = 0; i < value.length; i++) { forEach(value, function(v) {
returnStr = appendParam(returnStr, name, value[i]); returnStr = appendParam(returnStr, name, v);
} });
} else { } else {
returnStr = appendParam(returnStr, name, value); returnStr = appendParam(returnStr, name, value);
} }
@ -647,7 +699,7 @@ var HTMx = HTMx || (function () {
// initialize the document // initialize the document
ready(function () { ready(function () {
processElement(getDocument().body); processNode(getDocument().body);
window.onpopstate = function (event) { window.onpopstate = function (event) {
restoreHistory(event.state); restoreHistory(event.state);
}; };
@ -659,7 +711,7 @@ var HTMx = HTMx || (function () {
// Public API // Public API
return { return {
processElement: processElement, processElement: processNode,
version: "0.0.1", version: "0.0.1",
_:internalEval _:internalEval
} }

55
test/boost.js Normal file
View File

@ -0,0 +1,55 @@
describe("HTMx Boost Tests", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles basic anchor properly', function () {
this.server.respondWith("GET", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><a id="a1" href="/test">Foo</a></div>');
var a = byId('a1');
a.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
history.back();
})
it('handles basic form post properly', function () {
this.server.respondWith("POST", "/test", "Boosted");
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');
btn.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
history.back();
})
it('handles basic form get properly', function () {
this.server.respondWith("GET", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test" method="get"><button id="b1">Submit</button></form></div>');
var btn = byId('b1');
btn.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
history.back();
})
it('handles basic form with no explicit method property', function () {
this.server.respondWith("GET", "/test", "Boosted");
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test"><button id="b1">Submit</button></form></div>');
var btn = byId('b1');
btn.click();
this.server.respond();
div.innerHTML.should.equal("Boosted");
history.back();
})
});

View File

@ -27,6 +27,7 @@
<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="swap_direct.js"></script>
<script src="boost.js"></script>
<script class="mocha-exec"> <script class="mocha-exec">
mocha.run(); mocha.run();

View File

@ -29,9 +29,9 @@
<script src="scratch_server.js"></script> <script src="scratch_server.js"></script>
<script> <script>
this.server.respondWith("GET", "/test", "Clicked<div id='d1' ic-swap-direct='true'>Swapped</div>"); this.server.respondWith("POST", "/test", "Boosted");
var div = make('<div hx-get="/test">click me</div>'); var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test" method="post"><button id="b1">Submit</button></form></div>');
make('<div id="d1"></div>'); var btn = byId('b1');
</script> </script>