Skip to content
Open
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
2 changes: 1 addition & 1 deletion dist/htmx.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export interface Htmx {
on(event: string, handler: (evt: Event) => void): void;
on(target: string | Element, event: string, handler: (evt: Event) => void): void;
onLoad(callback: (elt: Element) => void): void;
process(elt: Element): void;
process(elt: Element, force?: boolean): void;
registerExtension(name: string, ext: any): void;
trigger(elt: Element | string, event: string, detail?: any, bubbles?: boolean): boolean;
timeout(ms: number): Promise<void>;
Expand Down
20 changes: 20 additions & 0 deletions src/ext/hx-live.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,15 @@
}
}

function cleanupLive(elt) {
let prop = elt._htmx;
if (!prop?.liveRuns) return;
for (let run of prop.liveRuns) fns.delete(run);
delete prop.liveRuns;
delete prop.liveRegistered;
delete prop.liveAttrs;
}

function processElement(elt) {
if (elt.closest('[hx-ignore]')) return;
let prop = api.htmxProp(elt);
Expand All @@ -510,6 +519,8 @@
}
};
fns.add(run);
prop.liveRuns = prop.liveRuns || new Set();
prop.liveRuns.add(run);
run();
}
}
Expand Down Expand Up @@ -559,6 +570,9 @@
}
};
fns.add(run);
let prop = api.htmxProp(elt);
prop.liveRuns = prop.liveRuns || new Set();
prop.liveRuns.add(run);
run();
}

Expand Down Expand Up @@ -590,6 +604,12 @@
init: (internalAPI) => {
api = internalAPI;
},
htmx_before_cleanup: (elt) => {
cleanupLive(elt);
},
htmx_before_morph_attr: (elt, detail) => {
if (bindPrefixes.some(p => detail.attrName.startsWith(p))) cleanupLive(elt);
},
htmx_after_process: (elt) => {
processLive(elt);
},
Expand Down
66 changes: 39 additions & 27 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,10 @@ var htmx = (() => {
return elt._htmx;
}

__htmxState(elt) {
return elt._htmx_state ||= {};
}

__initializeElement(elt) {
if (this.__shouldInitialize(elt) && this.__trigger(elt, "htmx:before:init", {}, true)) {
let htmxProp = this.__htmxProp(elt);
Expand Down Expand Up @@ -652,7 +656,7 @@ var htmx = (() => {
: (/^(drop|abort|replace|queue)/.test(syncValue) ? null : syncValue);
if (selector) syncElt = this.__findOrWarn(elt, selector, "hx-sync") || elt;
}
return this.__htmxProp(syncElt).rq ||= new ReqQ()
return this.__htmxState(syncElt).rq ||= new ReqQ()
}

__isModifierKeyClick(evt) {
Expand Down Expand Up @@ -869,12 +873,14 @@ var htmx = (() => {
return func.call(thisArg, ...values);
}

process(elt) {
// when force is true: re-wires elt and all powered descendants from current attributes
process(elt, force) {
if (!elt) return;
if (!(elt instanceof Element)) {
for (let child of elt.children || []) this.process(child);
for (let child of elt.children || []) this.process(child, force);
return;
}
if (force) this.__cleanup(elt, true);
if (this.__ignore(elt)) return;
if (!this.__trigger(elt, "htmx:before:process")) return
let hxOnNodes = [elt];
Expand Down Expand Up @@ -936,24 +942,23 @@ var htmx = (() => {
return !elt._htmx?.initialized && !this.__ignore(elt);
}

__cleanup(elt) {
if (elt._htmx) {
this.__trigger(elt, "htmx:before:cleanup")
for (let spec of elt._htmx.triggerSpecs || []) {
__cleanup(elt, force) {
let elts = [elt, ...elt.querySelectorAll?.('[data-htmx-powered]') ?? []];
for (let e of elts) {
if (!e._htmx) continue;
this.__trigger(e, "htmx:before:cleanup")
for (let spec of e._htmx.triggerSpecs || []) {
if (spec.interval) clearInterval(spec.interval);
if (spec.timeout) clearTimeout(spec.timeout);
if (spec.throttleTimeout) clearTimeout(spec.throttleTimeout);
spec.observer?.disconnect()
}
for (let listenerInfo of elt._htmx.listeners || []) {
listenerInfo.fromElt.removeEventListener(listenerInfo.eventName, listenerInfo.handler, listenerInfo);
}
this.__trigger(elt, "htmx:after:cleanup")
}
if (elt.firstChild) {
for (let child of elt.querySelectorAll('[data-htmx-powered]')) {
this.__cleanup(child);
for (let info of e._htmx.listeners || []) {
info.fromElt.removeEventListener(info.eventName, info.handler, info);
}
e.removeAttribute('data-htmx-powered');
this.__trigger(e, "htmx:after:cleanup")
if (force) delete e._htmx;
}
}

Expand Down Expand Up @@ -1321,8 +1326,6 @@ var htmx = (() => {
}
} else if (swapStyle === 'outerSync') {
this.__copyAttributes(target, fragment.firstElementChild);
this.__cleanup(target);
delete target._htmx;
target.replaceChildren(...fragment.firstElementChild.childNodes);
newContent = [target];
} else if (swapStyle === 'innerMorph') {
Expand Down Expand Up @@ -1622,6 +1625,7 @@ var htmx = (() => {
// hx-on:<event> binds to <event> directly
// hx-on::<event> is shorthand for hx-on:htmx:<event> (htmx events)
__handleHxOnAttributes(node) {
if (node._htmx?.onInitialized) return;
let hxOnNames = this.__prefixes("hx-on");
let mc = this.config.metaCharacter || ':';
let handler = (code) => async (evt) => {
Expand All @@ -1635,6 +1639,7 @@ var htmx = (() => {
for (let attr of node.getAttributeNames()) {
let prefix = hxOnNames.find(p => attr.startsWith(p));
if (!prefix) continue;
this.__htmxProp(node).onInitialized = true;
let rest = attr.substring(prefix.length);
let value = node.getAttribute(attr);
// hx-on="click once -> doA(); blur -> doB()"
Expand Down Expand Up @@ -1662,19 +1667,19 @@ var htmx = (() => {
indicatorElements = this.__findAllExt(elt, indicatorsSelector, "hx-indicator");
}
for (const indicator of indicatorElements) {
let p = this.__htmxProp(indicator);
p.rc = (p.rc || 0) + 1;
let s = this.__htmxState(indicator);
s.rc = (s.rc || 0) + 1;
this.__addClass(indicator, this.config.requestClass)
}
return indicatorElements
}

__hideIndicators(indicatorElements) {
for (let indicator of indicatorElements) {
let p = this.__htmxProp(indicator);
if (p.rc && --p.rc <= 0) {
let s = this.__htmxState(indicator);
if (s.rc && --s.rc <= 0) {
this.__removeClass(indicator, this.config.requestClass);
delete p.rc;
delete s.rc;
}
}
}
Expand All @@ -1685,8 +1690,8 @@ var htmx = (() => {
if (disabledSelector) {
disabledElements = this.__findAllExt(elt, disabledSelector, "hx-disable");
for (let indicator of disabledElements) {
let p = this.__htmxProp(indicator);
p.dc = (p.dc || 0) + 1;
let s = this.__htmxState(indicator);
s.dc = (s.dc || 0) + 1;
indicator.disabled = true
}
}
Expand All @@ -1695,10 +1700,10 @@ var htmx = (() => {

__enableElements(disabledElements) {
for (const indicator of disabledElements) {
let p = this.__htmxProp(indicator);
if (p.dc && --p.dc <= 0) {
let s = this.__htmxState(indicator);
if (s.dc && --s.dc <= 0) {
indicator.disabled = false
delete p.dc;
delete s.dc;
}
}
}
Expand Down Expand Up @@ -2088,8 +2093,12 @@ var htmx = (() => {

__copyAttributes(destination, source) {
let attributesToIgnore = this.config.morphIgnore || [];
let needsReinit = false;
let isHxAttr = name => this.__prefixes('hx-').some(p => name.startsWith(p));
for (const attr of source.attributes) {
if (!attributesToIgnore.some(p => attr.name.startsWith(p)) && destination.getAttribute(attr.name) !== attr.value) {
if (isHxAttr(attr.name)) needsReinit = true;
if (!this.__triggerExtensions(destination, 'htmx:before:morph:attr', { attrName: attr.name, newValue: attr.value })) continue;
destination.setAttribute(attr.name, attr.value);
if (attr.name === "value" && destination instanceof HTMLInputElement && destination.type !== "file") {
destination.value = attr.value;
Expand All @@ -2099,9 +2108,12 @@ var htmx = (() => {
for (let i = destination.attributes.length - 1; i >= 0; i--) {
let attr = destination.attributes[i];
if (attr && !source.hasAttribute(attr.name) && !attributesToIgnore.some(p => attr.name.startsWith(p))) {
if (isHxAttr(attr.name)) needsReinit = true;
if (!this.__triggerExtensions(destination, 'htmx:before:morph:attr', { attrName: attr.name, newValue: null })) continue;
destination.removeAttribute(attr.name);
}
}
if (needsReinit) this.__cleanup(destination, true);
}

__populateIdMapWithTree(idMap, persistentIds, root, elements) {
Expand Down
22 changes: 22 additions & 0 deletions test/tests/attributes/hx-on.js
Original file line number Diff line number Diff line change
Expand Up @@ -618,4 +618,26 @@ describe('hx-on="eventSpec -> code" syntax', function() {
window.foo.should.equal(true);
delete window.foo;
});

it('calling process() multiple times does not duplicate hx-on listeners', function() {
window.fooCount = 0;
let btn = createProcessedHTML('<button hx-on:click="window.fooCount++">x</button>');
htmx.process(btn);
htmx.process(btn);
htmx.process(btn);
btn.click();
window.fooCount.should.equal(1);
delete window.fooCount;
});

it('calling process() multiple times does not duplicate hx-on= listeners', function() {
window.fooCount = 0;
let btn = createProcessedHTML('<button hx-on="click -> window.fooCount++">x</button>');
htmx.process(btn);
htmx.process(btn);
htmx.process(btn);
btn.click();
window.fooCount.should.equal(1);
delete window.fooCount;
});
})
Loading