first pass at hx-sync attribute

This commit is contained in:
carson 2022-01-17 13:10:32 -07:00
parent 0a0f6b6bab
commit e6751be2ee
3 changed files with 306 additions and 30 deletions

View File

@ -1063,7 +1063,7 @@ return (function () {
triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
} else if (token === "from" && tokens[0] === ":") {
tokens.shift();
let from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
if (from_arg === "closest" || from_arg === "find") {
tokens.shift();
from_arg +=
@ -2226,42 +2226,87 @@ return (function () {
return; // do not issue requests for elements removed from the DOM
}
var target = etc.targetOverride || getTarget(elt);
if (target == null) {
if (target == null || target == DUMMY_ELT) {
triggerErrorEvent(elt, 'htmx:targetError', {target: getAttributeValue(elt, "hx-target")});
return;
}
var syncElt = elt;
var eltData = getInternalData(elt);
if (eltData.requestInFlight) {
var queueStrategy = 'last';
if (event) {
var eventData = getInternalData(event);
if (eventData && eventData.triggerSpec && eventData.triggerSpec.queue) {
queueStrategy = eventData.triggerSpec.queue;
var syncStrategy = getClosestAttributeValue(elt, "hx-sync");
var queueStrategy = null;
var abortable = false;
if (syncStrategy) {
var syncStrings = syncStrategy.split(":");
var selector = syncStrings[0].trim();
if (selector === "this") {
syncElt = getClosestMatch(elt, function (elt) {
return getAttributeValue(elt, "hx-sync") != null;
});
} else {
syncElt = querySelectorExt(elt, selector);
}
// default to the drop strategy
syncStrategy = (syncStrings[1] || 'drop').trim();
eltData = getInternalData(syncElt);
if (syncStrategy === "drop" && eltData.xhr && eltData.abortable !== true) {
return;
} else if (syncStrategy === "abort") {
if (eltData.xhr) {
return;
} else {
abortable = true;
}
} else if (syncStrategy === "replace") {
triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue
} else if (syncStrategy.indexOf("queue") === 0) {
var queueStrArray = syncStrategy.split(" ");
queueStrategy = (queueStrArray[1] || "last").trim();
}
if (eltData.queuedRequests == null) {
eltData.queuedRequests = [];
}
if (queueStrategy === "first" && eltData.queuedRequests.length === 0) {
eltData.queuedRequests.push(function () {
issueAjaxRequest(verb, path, elt, event, etc)
});
} else if (queueStrategy === "all") {
eltData.queuedRequests.push(function () {
issueAjaxRequest(verb, path, elt, event, etc)
});
} else if (queueStrategy === "last") {
eltData.queuedRequests = []; // dump existing queue
eltData.queuedRequests.push(function () {
issueAjaxRequest(verb, path, elt, event, etc)
});
}
return;
} else {
eltData.requestInFlight = true;
}
if (eltData.xhr) {
if (eltData.abortable) {
triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue
} else {
if(queueStrategy == null){
if (event) {
var eventData = getInternalData(event);
if (eventData && eventData.triggerSpec && eventData.triggerSpec.queue) {
queueStrategy = eventData.triggerSpec.queue;
}
}
if (queueStrategy == null) {
queueStrategy = "last";
}
}
if (eltData.queuedRequests == null) {
eltData.queuedRequests = [];
}
if (queueStrategy === "first" && eltData.queuedRequests.length === 0) {
eltData.queuedRequests.push(function () {
issueAjaxRequest(verb, path, elt, event, etc)
});
} else if (queueStrategy === "all") {
eltData.queuedRequests.push(function () {
issueAjaxRequest(verb, path, elt, event, etc)
});
} else if (queueStrategy === "last") {
eltData.queuedRequests = []; // dump existing queue
eltData.queuedRequests.push(function () {
issueAjaxRequest(verb, path, elt, event, etc)
});
}
return;
}
}
var xhr = new XMLHttpRequest();
eltData.xhr = xhr;
eltData.abortable = abortable;
var endRequestLock = function(){
eltData.requestInFlight = false
eltData.xhr = null;
eltData.abortable = false;
if (eltData.queuedRequests != null &&
eltData.queuedRequests.length > 0) {
var queuedRequest = eltData.queuedRequests.shift();
@ -2289,7 +2334,6 @@ return (function () {
}
}
var xhr = new XMLHttpRequest();
var headers = getHeaders(elt, target, promptResponse);
if (etc.headers) {
@ -2760,6 +2804,13 @@ return (function () {
insertIndicatorStyles();
var body = getDocument().body;
processNode(body);
body.addEventListener("htmx:abort", function (evt) {
var target = evt.target;
var internalData = getInternalData(target);
if (internalData && internalData.xhr) {
internalData.xhr.abort();
}
});
window.onpopstate = function (event) {
if (event.state && event.state.htmx) {
restoreHistory();

224
test/attributes/hx-sync.js Normal file
View File

@ -0,0 +1,224 @@
describe("hx-sync attribute", function(){
beforeEach(function() {
this.server = makeServer();
clearWorkArea();
});
afterEach(function() {
this.server.restore();
clearWorkArea();
});
it('can use drop strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this:drop"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b1.click();
b2.click();
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
});
it('defaults to the drop strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b1.click();
b2.click();
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
});
it('can use replace strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this:replace"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b1.click();
b2.click();
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Initial');
b2.innerHTML.should.equal('Click 0');
});
it('can use queue all strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this:queue all"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button>' +
' <button id="b3" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
b1.click();
var b2 = byId("b2");
b2.click();
var b3 = byId("b3");
b3.click();
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
b3.innerHTML.should.equal('Initial');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Click 1');
b3.innerHTML.should.equal('Initial');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Click 1');
b3.innerHTML.should.equal('Click 2');
});
it('can use queue last strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this:queue last"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button>' +
' <button id="b3" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
b1.click();
var b2 = byId("b2");
b2.click();
var b3 = byId("b3");
b3.click();
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
b3.innerHTML.should.equal('Initial');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
b3.innerHTML.should.equal('Click 1');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
b3.innerHTML.should.equal('Click 1');
});
it('can use queue first strategy', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this:queue first"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button>' +
' <button id="b3" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
b1.click();
var b2 = byId("b2");
b2.click();
var b3 = byId("b3");
b3.click();
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Initial');
b3.innerHTML.should.equal('Initial');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Click 1');
b3.innerHTML.should.equal('Initial');
this.server.respond();
b1.innerHTML.should.equal('Click 0');
b2.innerHTML.should.equal('Click 1');
b3.innerHTML.should.equal('Initial');
});
it('can use abort strategy to end existing abortable request', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this"><button hx-sync="closest div:abort" id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b1.click();
b2.click();
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Initial');
b2.innerHTML.should.equal('Click 0');
});
it('can use abort strategy to drop abortable request when one is in flight', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div hx-sync="this"><button hx-sync="closest div:abort" id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b2.click();
b1.click();
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Initial');
b2.innerHTML.should.equal('Click 0');
});
it('can abort a request programmatically', function()
{
var count = 0;
this.server.respondWith("GET", "/test", function(xhr){
xhr.respond(200, {}, "Click " + count++);
});
make('<div><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId("b1");
var b2 = byId("b2");
b1.click();
b2.click();
htmx.trigger(b1, "htmx:abort");
this.server.respond();
this.server.respond();
b1.innerHTML.should.equal('Initial');
b2.innerHTML.should.equal('Click 0');
});
})

View File

@ -73,6 +73,7 @@
<script src="attributes/hx-sse.js"></script>
<script src="attributes/hx-swap-oob.js"></script>
<script src="attributes/hx-swap.js"></script>
<script src="attributes/hx-sync.js"></script>
<script src="attributes/hx-target.js"></script>
<script src="attributes/hx-trigger.js"></script>
<script src="attributes/hx-vals.js"></script>