revert settle attribute swapping so internals work + hx-ws tests (ugh)

This commit is contained in:
carson 2020-05-25 19:46:10 -07:00
parent 7036557cba
commit 51697abff6
15 changed files with 185 additions and 124 deletions

47
dist/htmx.js vendored
View File

@ -346,13 +346,11 @@ return (function () {
var target = getDocument().getElementById(child.id);
if (target) {
var fragment;
if (isInlineSwap(oobValue, target)) {
fragment = getDocument().createDocumentFragment();
fragment.appendChild(child);
} else {
fragment = child;
fragment = getDocument().createDocumentFragment();
fragment.appendChild(child); // pulls the child out of the existing fragment
if (!isInlineSwap(oobValue, target)) {
fragment = child; // if this is not an inline swap, we use the content of the node, not the node itself
}
fragment.appendChild(child);
swap(oobValue, target, target, fragment, settleInfo);
} else {
child.parentNode.removeChild(child);
@ -370,27 +368,26 @@ return (function () {
});
}
function handleAttributes(parentNode, fragment, settleInfo) {
forEach(fragment.querySelectorAll("[id]"), function (newNode) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]")
if (oldNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
}
});
}
function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
handleAttributes(parentNode, fragment, settleInfo);
while(fragment.childNodes.length > 0){
var child = fragment.firstChild;
parentNode.insertBefore(child, insertBefore);
if (child.nodeType !== Node.TEXT_NODE) {
triggerEvent(child, 'load.htmx', {});
processNode(child);
var newAttributes = null;
if (child.id) {
var originalNode = parentNode.querySelector(child.tagName + "[id=" + child.id + "]");
if (originalNode && originalNode !== parentNode) {
newAttributes = child.cloneNode();
cloneAttributes(child, originalNode);
}
}
settleInfo.tasks.push(function(){
if (newAttributes) {
cloneAttributes(child, newAttributes);
}
processNode(child);
triggerEvent(child, 'load.htmx', {});
});
}
}
}
@ -757,10 +754,6 @@ return (function () {
}
function processSSESource(elt, sseSrc) {
var detail = {
config:{withCredentials: true}
};
triggerEvent(elt, "initSSE.htmx", detail);
var source = new EventSource(sseSrc, detail.config);
source.onerror = function (e) {
triggerErrorEvent(elt, "sseError.htmx", {error:e, source:source});
@ -1494,7 +1487,7 @@ return (function () {
function getMetaConfig() {
var element = getDocument().querySelector('meta[name="htmx-config"]');
if (element) {
return JSON.parse(element.content);
return eval(element.content);
} else {
return null;
}

2
dist/htmx.min.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/htmx.min.js.gz vendored

Binary file not shown.

View File

@ -368,23 +368,26 @@ return (function () {
});
}
function handleAttributes(parentNode, fragment, settleInfo) {
forEach(fragment.querySelectorAll("[id]"), function (newNode) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]")
if (oldNode && oldNode !== parentNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
}
});
}
function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
handleAttributes(parentNode, fragment, settleInfo);
while(fragment.childNodes.length > 0){
var child = fragment.firstChild;
parentNode.insertBefore(child, insertBefore);
if (child.nodeType !== Node.TEXT_NODE) {
var newAttributes = null;
if (child.id) {
var originalNode = parentNode.querySelector(child.tagName + "[id=" + child.id + "]");
if (originalNode && originalNode !== parentNode) {
newAttributes = child.cloneNode();
cloneAttributes(child, originalNode);
}
}
settleInfo.tasks.push(function(){
if (newAttributes) {
cloneAttributes(child, newAttributes);
}
processNode(child);
triggerEvent(child, 'load.htmx', {});
});
@ -663,21 +666,17 @@ return (function () {
var values = info.split(",");
for (var i = 0; i < values.length; i++) {
var value = removeWhiteSpace(values[i]);
if (value.indexOf("source:") === 0) {
if (value.indexOf("source ") === 0) {
processWebSocketSource(elt, value.substr(7));
}
if (value.indexOf("send:") === 0) {
processWebSocketSend(elt, value.substr(5));
if (value === "send") {
processWebSocketSend(elt);
}
}
}
function processWebSocketSource(elt, wssSource) {
var detail = {
protocols:[]
};
triggerEvent(elt, "initWebSocket.htmx", detail);
var socket = new WebSocket("wss:" + wssSource, detail.protocols);
var socket = htmx.createWebSocket(wssSource);
socket.onerror = function (e) {
triggerErrorEvent(elt, "wsError.htmx", {error:e, socket:socket});
maybeCloseWebSocketSource(elt);
@ -712,13 +711,13 @@ return (function () {
}
}
function processWebSocketSend(elt, eventName) {
function processWebSocketSend(elt) {
var webSocketSourceElt = getClosestMatch(elt, function (parent) {
return getInternalData(parent).webSocket != null;
});
if (webSocketSourceElt) {
var webSocket = getInternalData(webSocketSourceElt).webSocket;
elt.addEventListener(eventName, function (evt) {
elt.addEventListener(getTriggerSpecs(elt)[0].trigger, function (evt) {
var headers = getHeaders(elt, webSocketSourceElt, null, elt);
var rawParameters = getInputValues(elt, 'post');
var filteredParameters = filterValues(rawParameters, elt);
@ -754,11 +753,7 @@ return (function () {
}
function processSSESource(elt, sseSrc) {
var detail = {
config:{withCredentials: true}
};
triggerEvent(elt, "initSSE.htmx", detail);
var source = new EventSource(sseSrc, detail.config);
var source = htmx.createEventSource(sseSrc);
source.onerror = function (e) {
triggerErrorEvent(elt, "sseError.htmx", {error:e, source:source});
maybeCloseSSESource(elt);
@ -1543,7 +1538,13 @@ return (function () {
includeIndicatorStyles:true
},
parseInterval:parseInterval,
_:internalEval
_:internalEval,
createEventSource: function(url){
return new EventSource(url, {withCredentials:true})
},
createWebSocket: function(url){
return new WebSocket(url, []);
}
}
}
)()

51
test/attributes/hx-ws.js Normal file
View File

@ -0,0 +1,51 @@
describe("hx-ws attribute", function() {
function mockWebsocket() {
var listener;
var lastSent;
var mockSocket = {
addEventListener : function(message, l) {
listener = l;
},
write : function(content) {
return listener({data:content});
},
send : function(data) {
lastSent = data;
},
getLastSent : function() {
return lastSent;
}
};
return mockSocket;
}
beforeEach(function () {
this.server = makeServer();
var socket = mockWebsocket();
this.socket = socket;
clearWorkArea();
htmx.createWebSocket = function(){ return socket };
});
afterEach(function () {
this.server.restore();
clearWorkArea();
});
it('handles a basic call back', function () {
var div = make('<div hx-ws="source:wss:/foo"><div id="d1">div1</div><div id="d2">div2</div></div>');
this.socket.write("<div id=\"d1\">replaced</div>")
byId("d1").innerHTML.should.equal("replaced");
byId("d2").innerHTML.should.equal("div2");
})
it('handles a basic send', function () {
var div = make('<div hx-ws="source:wss:/foo"><div hx-ws="send" id="d1">div1</div></div>');
byId("d1").click();
var lastSent = this.socket.getLastSent();
var data = JSON.parse(lastSent);
data.HEADERS["X-HX-Request"].should.equal("true");
})
});

View File

@ -305,6 +305,19 @@ describe("Core htmx AJAX Tests", function(){
div.innerHTML.should.equal("<div id=\"d1\">foo</div>");
});
it('properly settles attributes on interior elements', function(done)
{
this.server.respondWith("GET", "/test", "<div hx-get='/test'><div foo='bar' id='d1'></div></div>");
var div = make("<div hx-get='/test' hx-swap='outerHTML settle:10ms'><div id='d1'></div></div>");
div.click();
this.server.respond();
should.equal(byId("d1").getAttribute("foo"), null);
setTimeout(function () {
should.equal(byId("d1").getAttribute("foo"), "bar");
done();
}, 20);
});
it('properly handles checkbox inputs', function()
{
var values;

View File

@ -78,6 +78,7 @@
<script src="attributes/hx-swap.js"></script>
<script src="attributes/hx-target.js"></script>
<script src="attributes/hx-trigger.js"></script>
<script src="attributes/hx-ws.js"></script>
<!-- extension tests -->
<script src="../src/ext/rails-method.js"></script>

View File

@ -8,24 +8,25 @@ title: </> htmx - hx-ws
The `hx-ws` allows you to work with [Web Sockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications)
directly from HTML. The value of the attribute can be one or more of the following, separated by commas:
* `source:<url>` - A URL to establish an `WebSocket` against (NB: do not include the `wss:` protocol prefix)
* `send:<event_name>` - Sends a message to the nearest websocket
* `source <url>` - A URL to establish an `WebSocket` against (NB: include the `wss:` protocol prefix)
* `send` - Sends a message to the nearest websocket based on the trigger value for the element (either the natural event
of the event specified by [`hx-trigger`])
Here is an example:
```html
<div hx-ws="source:/chatroom">
<div hx-ws="source wss:/chatroom">
<div id="chat_room">
...
</div>
<form hx-ws="send:submit">
<form hx-ws="send">
<input name="chat_message">
</form>
</div>
```
This example establishes a WebSocket to the `chatroom` end point. Content that is send down from the websocket will
be parsed as HTML and swapped in by the `id` property, similar to [Out of Band Swaps](/attributes/hx-swap-oob).
be parsed as HTML and swapped in by the `id` property, using the same logis as [Out of Band Swaps](/attributes/hx-swap-oob).
The form uses the `send:` syntax to indicate that when it is submitted, the form values should be serialized as JSON
and send to the nearest enclosing `WebSocket`.

View File

@ -118,13 +118,6 @@ This event is triggered when htmx handles a history restoration action
* `detail.path` - the path and query of the page being restored
* `detail.historyElt` - the history element being restored into
### <a name="initSSE.htmx"></a> Event - [`initSSE.htmx`](#initSSE.htmx)
This event is triggered when htmx initializes a new SSE source. It can be used
to [configure the source](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource).
Note that by default `withCredentials` will be set to `true` in the configuration.
##### Details
* `detail.config` - the config that will be passed to the `EventSource` contstructor

View File

@ -346,13 +346,11 @@ return (function () {
var target = getDocument().getElementById(child.id);
if (target) {
var fragment;
if (isInlineSwap(oobValue, target)) {
fragment = getDocument().createDocumentFragment();
fragment.appendChild(child);
} else {
fragment = child;
fragment = getDocument().createDocumentFragment();
fragment.appendChild(child); // pulls the child out of the existing fragment
if (!isInlineSwap(oobValue, target)) {
fragment = child; // if this is not an inline swap, we use the content of the node, not the node itself
}
fragment.appendChild(child);
swap(oobValue, target, target, fragment, settleInfo);
} else {
child.parentNode.removeChild(child);
@ -370,27 +368,26 @@ return (function () {
});
}
function handleAttributes(parentNode, fragment, settleInfo) {
forEach(fragment.querySelectorAll("[id]"), function (newNode) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]")
if (oldNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
}
});
}
function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
handleAttributes(parentNode, fragment, settleInfo);
while(fragment.childNodes.length > 0){
var child = fragment.firstChild;
parentNode.insertBefore(child, insertBefore);
if (child.nodeType !== Node.TEXT_NODE) {
triggerEvent(child, 'load.htmx', {});
processNode(child);
var newAttributes = null;
if (child.id) {
var originalNode = parentNode.querySelector(child.tagName + "[id=" + child.id + "]");
if (originalNode && originalNode !== parentNode) {
newAttributes = child.cloneNode();
cloneAttributes(child, originalNode);
}
}
settleInfo.tasks.push(function(){
if (newAttributes) {
cloneAttributes(child, newAttributes);
}
processNode(child);
triggerEvent(child, 'load.htmx', {});
});
}
}
}
@ -757,10 +754,6 @@ return (function () {
}
function processSSESource(elt, sseSrc) {
var detail = {
config:{withCredentials: true}
};
triggerEvent(elt, "initSSE.htmx", detail);
var source = new EventSource(sseSrc, detail.config);
source.onerror = function (e) {
triggerErrorEvent(elt, "sseError.htmx", {error:e, source:source});
@ -1494,7 +1487,7 @@ return (function () {
function getMetaConfig() {
var element = getDocument().querySelector('meta[name="htmx-config"]');
if (element) {
return JSON.parse(element.content);
return eval(element.content);
} else {
return null;
}

View File

@ -82,7 +82,6 @@ title: </> htmx - Attributes
| [`historyCacheMissLoad.htmx`](/events#historyCacheMissLoad.htmx) | triggered on a succesful remote retrieval
| [`historyRestore.htmx`](/events#historyRestore.htmx) | triggered when htmx handles a history restoration action
| [`beforeHistorySave.htmx`](/events#beforeHistorySave.htmx) | triggered before content is saved to the history cache
| [`initSSE.htmx`](/events#initSSE.htmx) | triggered when a new Server Sent Event source is created
| [`load.htmx`](/events#load.htmx) | triggered when new content is added to the DOM
| [`noSSESourceError.htmx`](/events#noSSESourceError.htmx) | triggered when an element refers to a SSE event in its trigger, but no parent SSE source has been defined
| [`onLoadError.htmx`](/events#onLoadError.htmx) | triggered when an exception occurs during the onLoad handling in htmx

View File

@ -346,13 +346,11 @@ return (function () {
var target = getDocument().getElementById(child.id);
if (target) {
var fragment;
if (isInlineSwap(oobValue, target)) {
fragment = getDocument().createDocumentFragment();
fragment.appendChild(child);
} else {
fragment = child;
fragment = getDocument().createDocumentFragment();
fragment.appendChild(child); // pulls the child out of the existing fragment
if (!isInlineSwap(oobValue, target)) {
fragment = child; // if this is not an inline swap, we use the content of the node, not the node itself
}
fragment.appendChild(child);
swap(oobValue, target, target, fragment, settleInfo);
} else {
child.parentNode.removeChild(child);
@ -370,27 +368,26 @@ return (function () {
});
}
function handleAttributes(parentNode, fragment, settleInfo) {
forEach(fragment.querySelectorAll("[id]"), function (newNode) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]")
if (oldNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
}
});
}
function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
handleAttributes(parentNode, fragment, settleInfo);
while(fragment.childNodes.length > 0){
var child = fragment.firstChild;
parentNode.insertBefore(child, insertBefore);
if (child.nodeType !== Node.TEXT_NODE) {
triggerEvent(child, 'load.htmx', {});
processNode(child);
var newAttributes = null;
if (child.id) {
var originalNode = parentNode.querySelector(child.tagName + "[id=" + child.id + "]");
if (originalNode && originalNode !== parentNode) {
newAttributes = child.cloneNode();
cloneAttributes(child, originalNode);
}
}
settleInfo.tasks.push(function(){
if (newAttributes) {
cloneAttributes(child, newAttributes);
}
processNode(child);
triggerEvent(child, 'load.htmx', {});
});
}
}
}
@ -757,10 +754,6 @@ return (function () {
}
function processSSESource(elt, sseSrc) {
var detail = {
config:{withCredentials: true}
};
triggerEvent(elt, "initSSE.htmx", detail);
var source = new EventSource(sseSrc, detail.config);
source.onerror = function (e) {
triggerErrorEvent(elt, "sseError.htmx", {error:e, source:source});
@ -1494,7 +1487,7 @@ return (function () {
function getMetaConfig() {
var element = getDocument().querySelector('meta[name="htmx-config"]');
if (element) {
return JSON.parse(element.content);
return eval(element.content);
} else {
return null;
}

View File

@ -48,5 +48,28 @@ describe("hx-swap-oob attribute", function () {
byId("d1").innerHTML.should.equal("Swapped");
})
it('handles outerHTML response properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' foo='bar' hx-swap-oob='outerHTML'>Swapped</div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"></div>');
div.click();
this.server.respond();
byId("d1").getAttribute("foo").should.equal("bar");
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped");
})
it('handles innerHTML response properly', function () {
this.server.respondWith("GET", "/test", "Clicked<div id='d1' foo='bar' hx-swap-oob='innerHTML'>Swapped</div>");
var div = make('<div hx-get="/test">click me</div>');
make('<div id="d1"></div>');
div.click();
this.server.respond();
should.equal(byId("d1").getAttribute("foo"), null);
div.innerHTML.should.equal("Clicked");
byId("d1").innerHTML.should.equal("Swapped");
})
});

View File

@ -211,7 +211,7 @@ describe("hx-swap attribute", function(){
swapSpec(make("<div/>")).swapStyle.should.equal("innerHTML")
swapSpec(make("<div hx-swap='innerHTML'/>")).swapStyle.should.equal("innerHTML")
swapSpec(make("<div hx-swap='innerHTML'/>")).swapDelay.should.equal(0)
swapSpec(make("<div hx-swap='innerHTML'/>")).settleDelay.should.equal(100)
swapSpec(make("<div hx-swap='innerHTML'/>")).settleDelay.should.equal(0) // set to 0 in tests
swapSpec(make("<div hx-swap='innerHTML swap:10'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='innerHTML settle:10'/>")).settleDelay.should.equal(10)
swapSpec(make("<div hx-swap='innerHTML swap:10 settle:11'/>")).swapDelay.should.equal(10)

View File

@ -9,7 +9,7 @@
<meta http-equiv="expires" content="0" />
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
<meta http-equiv="pragma" content="no-cache" />
<meta name="htmx-config" content='{"historyEnabled":false}'>
<meta name="htmx-config" content='{"historyEnabled":false,"defaultSettleDelay":0}'>
</head>
<body style="padding:20px;font-family: sans-serif">