Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix sse.js double element processing issue with expanded extension API #18

Merged
merged 2 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 22 additions & 60 deletions src/sse/sse.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
}
},

getSelectors: function() {
return ['[sse-connect]', '[data-sse-connect]', '[sse-swap]', '[data-sse-swap]']
},

/**
* onEvent handles all events passed to this extension.
*
Expand Down Expand Up @@ -77,9 +81,9 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/
function registerSSE(elt) {
// Add message handlers for every `sse-swap` attribute
queryAttributeOnThisOrChildren(elt, 'sse-swap').forEach(function(child) {
if (api.getAttributeValue(elt, 'sse-swap')) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(child, hasEventSource)
var sourceElement = api.getClosestMatch(elt, hasEventSource)
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null // no eventsource in parentage, orphaned element
Expand All @@ -89,7 +93,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
var internalData = api.getInternalData(sourceElement)
var source = internalData.sseEventSource

var sseSwapAttr = api.getAttributeValue(child, 'sse-swap')
var sseSwapAttr = api.getAttributeValue(elt, 'sse-swap')
var sseEventNames = sseSwapAttr.split(',')

for (var i = 0; i < sseEventNames.length; i++) {
Expand All @@ -101,7 +105,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
}

// If the body no longer contains the element, remove the listener
if (!api.bodyContains(child)) {
if (!api.bodyContains(elt)) {
source.removeEventListener(sseEventName, listener)
return
}
Expand All @@ -110,20 +114,20 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
if (!api.triggerEvent(elt, 'htmx:sseBeforeMessage', event)) {
return
}
swap(child, event.data)
swap(elt, event.data)
api.triggerEvent(elt, 'htmx:sseMessage', event)
}

// Register the new listener
api.getInternalData(child).sseEventListener = listener
api.getInternalData(elt).sseEventListener = listener
source.addEventListener(sseEventName, listener)
}
})
}

// Add message handlers for every `hx-trigger="sse:*"` attribute
queryAttributeOnThisOrChildren(elt, 'hx-trigger').forEach(function(child) {
if (api.getAttributeValue(elt, 'hx-trigger')) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(child, hasEventSource)
var sourceElement = api.getClosestMatch(elt, hasEventSource)
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null // no eventsource in parentage, orphaned element
Expand All @@ -133,7 +137,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
var internalData = api.getInternalData(sourceElement)
var source = internalData.sseEventSource

var sseEventName = api.getAttributeValue(child, 'hx-trigger')
var sseEventName = api.getAttributeValue(elt, 'hx-trigger')
if (sseEventName == null) {
return
}
Expand All @@ -148,19 +152,19 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
return
}

if (!api.bodyContains(child)) {
if (!api.bodyContains(elt)) {
source.removeEventListener(sseEventName, listener)
}

// Trigger events to be handled by the rest of htmx
htmx.trigger(child, sseEventName, event)
htmx.trigger(child, 'htmx:sseMessage', event)
htmx.trigger(elt, sseEventName, event)
htmx.trigger(elt, 'htmx:sseMessage', event)
}

// Register the new listener
api.getInternalData(elt).sseEventListener = listener
source.addEventListener(sseEventName.slice(4), listener)
})
}
}

/**
Expand All @@ -177,14 +181,14 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
}

// handle extension source creation attribute
queryAttributeOnThisOrChildren(elt, 'sse-connect').forEach(function(child) {
var sseURL = api.getAttributeValue(child, 'sse-connect')
if (api.getAttributeValue(elt, 'sse-connect')) {
var sseURL = api.getAttributeValue(elt, 'sse-connect')
if (sseURL == null) {
return
}

ensureEventSource(child, sseURL, retryCount)
})
ensureEventSource(elt, sseURL, retryCount)
}

registerSSE(elt)
}
Expand Down Expand Up @@ -246,27 +250,6 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
return false
}

/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
var result = []

// If the parent element also contains the requested attribute, then add it to the results too.
if (api.hasAttribute(elt, attributeName)) {
result.push(elt)
}

// Search all child nodes that match the requested attribute
elt.querySelectorAll('[' + attributeName + '], [data-' + attributeName + ']').forEach(function(node) {
result.push(node)
})

return result
}

/**
* @param {HTMLElement} elt
Expand All @@ -282,27 +265,6 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
api.swap(target, content, swapSpec)
}

/**
* doSettle mirrors much of the functionality in htmx that
* settles elements after their content has been swapped.
* TODO: this should be published by htmx, and not duplicated here
* @param {import("../htmx").HtmxSettleInfo} settleInfo
* @returns () => void
*/
function doSettle(settleInfo) {
return function() {
settleInfo.tasks.forEach(function(task) {
task.call()
})

settleInfo.elts.forEach(function(elt) {
if (elt.classList) {
elt.classList.remove(htmx.config.settlingClass)
}
api.triggerEvent(elt, 'htmx:afterSettle')
})
}
}

function hasEventSource(node) {
return api.getInternalData(node).sseEventSource != null
Expand Down
36 changes: 36 additions & 0 deletions src/sse/test/ext/sse.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,28 @@ describe('sse extension', function() {
clearWorkArea()
})

it('correctly subscribes to events', function() {
make('<div hx-ext="sse" sse-connect="/foo">' +
'<div sse-connect="/foo">' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
'</div>' +
'</div>')

this.eventSource.url.should.be.equal('/foo');
this.eventSource._listeners.e1.should.be.lengthOf(1)
})

it('correctly behaves when ignored', function() {
make('<div hx-ext="sse" sse-connect="/foo">' +
'<div hx-ext="ignore:sse" sse-connect="/foo">' +
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
'</div>' +
'</div>');

this.eventSource.url.should.be.equal('/foo');
(this.eventSource._listeners.e1 == undefined).should.be.true
})

it('handles basic sse triggering', function() {
this.server.respondWith('GET', '/d1', 'div1 updated')
this.server.respondWith('GET', '/d2', 'div2 updated')
Expand Down Expand Up @@ -231,6 +253,20 @@ describe('sse extension', function() {
(byId('d2')['htmx-internal-data'].sseEventSource == undefined).should.be.true
})

it('triggers events with naked hx-trigger', function() {
var div = make( '<div hx-ext="sse"><div sse-connect="/foo"><div id="d2" hx-trigger="sse:e2">div2</div></div></div>')

let triggerCounter = 0
div.addEventListener("htmx:trigger", () => triggerCounter++)
let sseMessageCounter = 0
div.addEventListener("htmx:sseMessage", () => sseMessageCounter++)

this.eventSource.sendEvent('e2')

triggerCounter.should.be.equal(1)
sseMessageCounter.should.be.equal(1)
})

it('initializes connections in swapped content', function() {
this.server.respondWith('GET', '/d1', '<div><div sse-connect="/foo"><div id="d2" hx-trigger="sse:e2" hx-get="/d2">div2</div></div></div>')
this.server.respondWith('GET', '/d2', 'div2 updated')
Expand Down