Skip to content

Commit 5190f0b

Browse files
committed
feat(#47): SelectorObserver addListener() unsubscribe fn return
1 parent ca6ff58 commit 5190f0b

File tree

3 files changed

+31
-11
lines changed

3 files changed

+31
-11
lines changed

.changeset/fluffy-comics-think.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sv443-network/userutils": minor
3+
---
4+
5+
SelectorObserver's `addListener()` now returns an unsubscribe function to more easily remove a listener

README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ document.addEventListener("DOMContentLoaded", () => {
298298
const bazObserver = new SelectorObserver(document.body);
299299

300300
// for TypeScript, specify that input elements are returned by the listener:
301-
bazObserver.addListener<HTMLInputElement>("input", {
301+
const unsubscribe = bazObserver.addListener<HTMLInputElement>("input", {
302302
all: true, // use querySelectorAll() instead of querySelector()
303303
continuous: true, // don't remove the listener after it was called once
304304
debounce: 50, // debounce the listener by 50ms
@@ -310,6 +310,11 @@ document.addEventListener("DOMContentLoaded", () => {
310310

311311
bazObserver.enable();
312312

313+
window.addEventListener("something", () => {
314+
// remove the listener after the event "something" was dispatched:
315+
unsubscribe();
316+
});
317+
313318

314319
// use a different element as the base:
315320

lib/SelectorObserver.ts

+20-10
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ type SelectorOptionsCommon = {
2929
debounceEdge?: "rising" | "falling";
3030
};
3131

32+
type UnsubscribeFunction = () => void;
33+
3234
export type SelectorObserverOptions = {
3335
/** If set, applies this debounce in milliseconds to all listeners that don't have their own debounce set */
3436
defaultDebounce?: number;
@@ -104,15 +106,17 @@ export class SelectorObserver {
104106
}
105107
}
106108

107-
private checkAllSelectors(): void {
109+
/** Call to check all selectors in the {@linkcode listenerMap} using {@linkcode checkSelector()} */
110+
protected checkAllSelectors(): void {
108111
if(!this.enabled || !domLoaded)
109112
return;
110113

111114
for(const [selector, listeners] of this.listenerMap.entries())
112115
this.checkSelector(selector, listeners);
113116
}
114117

115-
private checkSelector(selector: string, listeners: SelectorListenerOptions[]): void {
118+
/** Checks if the element(s) with the given {@linkcode selector} exist in the DOM and calls the respective {@linkcode listeners} accordingly */
119+
protected checkSelector(selector: string, listeners: SelectorListenerOptions[]): void {
116120
if(!this.enabled)
117121
return;
118122

@@ -149,10 +153,6 @@ export class SelectorObserver {
149153
}
150154
}
151155

152-
private debounce<TArgs>(func: (...args: TArgs[]) => void, time: number, edge: "falling" | "rising" = "falling"): (...args: TArgs[]) => void {
153-
return debounce(func, time, edge);
154-
}
155-
156156
/**
157157
* Starts observing the children of the base element for changes to the given {@linkcode selector} according to the set {@linkcode options}
158158
* @param selector The selector to observe
@@ -161,16 +161,24 @@ export class SelectorObserver {
161161
* @param [options.all] Whether to use `querySelectorAll()` instead - default is false
162162
* @param [options.continuous] Whether to call the listener continuously instead of just once - default is false
163163
* @param [options.debounce] Whether to debounce the listener to reduce calls to `querySelector` or `querySelectorAll` - set undefined or <=0 to disable (default)
164+
* @returns Returns a function that can be called to remove this listener more easily
164165
*/
165-
public addListener<TElem extends Element = HTMLElement>(selector: string, options: SelectorListenerOptions<TElem>): void {
166-
options = { all: false, continuous: false, debounce: 0, ...options };
166+
public addListener<TElem extends Element = HTMLElement>(selector: string, options: SelectorListenerOptions<TElem>): UnsubscribeFunction {
167+
options = {
168+
all: false,
169+
continuous: false,
170+
debounce: 0,
171+
...options,
172+
};
173+
167174
if((options.debounce && options.debounce > 0) || (this.customOptions.defaultDebounce && this.customOptions.defaultDebounce > 0)) {
168-
options.listener = this.debounce(
175+
options.listener = debounce(
169176
options.listener as ((arg: NodeListOf<Element> | Element) => void),
170177
(options.debounce || this.customOptions.defaultDebounce)!,
171178
(options.debounceEdge || this.customOptions.defaultDebounceEdge),
172-
);
179+
) as (arg: NodeListOf<Element> | Element) => void;
173180
}
181+
174182
if(this.listenerMap.has(selector))
175183
this.listenerMap.get(selector)!.push(options as SelectorListenerOptions<Element>);
176184
else
@@ -180,6 +188,8 @@ export class SelectorObserver {
180188
this.enable();
181189

182190
this.checkSelector(selector, [options as SelectorListenerOptions<Element>]);
191+
192+
return () => this.removeListener(selector, options as SelectorListenerOptions<Element>);
183193
}
184194

185195
/** Disables the observation of the child elements */

0 commit comments

Comments
 (0)