Created
February 26, 2025 17:54
-
-
Save MagnusThor/cc1bc2e09ef4291032896ff7e72667d3 to your computer and use it in GitHub Desktop.
Utils functions that i used regularly, i not that keen to use massive framework for tiny web app, less is more!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class DOMUtils { | |
static value<T>(selector: string, value?: any) { | |
const element = DOMUtils.get<HTMLInputElement>(selector); | |
if (value && element) element!.value = value; | |
return element ? element.value as T : null; | |
} | |
static get<T extends HTMLElement>(selector: string, parent?: Element | DocumentFragment | null): T | null { // Allow null | |
if (!parent) { | |
return document.querySelector(selector) as T | null; // Handle null parent | |
} | |
return parent.querySelector(selector) as T | null; // Handle null parent | |
} | |
static getAll(selector: string, parent?: Element | DocumentFragment): Element[] { | |
const queryResult = parent ? parent.querySelectorAll(selector) : document.querySelectorAll(selector); | |
return Array.from(queryResult); | |
} | |
static replace(oldElement: HTMLElement, newElement: HTMLElement) { | |
oldElement.replaceWith(newElement); | |
} | |
static onAll<T extends HTMLElement>( | |
event: string, | |
selectorOrElements: string | NodeListOf<T> | T[], | |
fn: (event?: Event, el?: T) => void, | |
options?: AddEventListenerOptions, | |
parentEl?: HTMLElement | DocumentFragment | null | |
): void { | |
let elements: NodeListOf<T> | T[]; | |
if (typeof selectorOrElements === "string") { | |
elements = parentEl | |
? parentEl.querySelectorAll(selectorOrElements) as NodeListOf<T> | |
: document.querySelectorAll(selectorOrElements) as NodeListOf<T>; | |
} else { | |
elements = selectorOrElements; | |
} | |
elements.forEach((el: T) => { | |
el.addEventListener(event, (e: Event) => { | |
fn(e, el); | |
}, options); | |
}); | |
} | |
static debounce(func: Function, delay: number): Function { | |
let timeoutId: ReturnType<typeof setTimeout>; | |
return function (...args: any[]) { | |
clearTimeout(timeoutId); | |
timeoutId = setTimeout(() => func.apply(this, args), delay); | |
}; | |
} | |
static getFormData(form: HTMLFormElement | string): FormData | null { | |
const formElement = typeof form === "string" ? $D.get(form) as HTMLFormElement : form; | |
if (!formElement) return null; | |
return new FormData(formElement); | |
} | |
static serializeForm<T extends object, K extends keyof T = never>( | |
form: HTMLFormElement, | |
omit?: K[], | |
pick?: (keyof T)[] | |
): Omit<T, K> | Pick<T, keyof Pick<T, keyof T>> { | |
const formData = new FormData(form); | |
const serialized: { [key: string]: any } = {}; | |
formData.forEach((value, key) => { | |
if (serialized[key]) { | |
if (Array.isArray(serialized[key])) { | |
serialized[key].push(value); | |
} else { | |
serialized[key] = [serialized[key], value]; | |
} | |
} else { | |
serialized[key] = value; | |
} | |
}); | |
let result: any = serialized; | |
if (pick) { | |
result = pick.reduce((acc: any, key) => { | |
if (result[key]) { | |
acc[key] = result[key]; | |
} | |
return acc; | |
}, {}); | |
} | |
if (omit) { | |
result = Object.keys(result).reduce((acc: any, key) => { | |
if (!omit.includes(key as K)) { | |
acc[key] = result[key]; | |
} | |
return acc; | |
}, {}); | |
} | |
return result as Omit<T, K> | Pick<T, keyof Pick<T, keyof T>>; | |
} | |
static throttle(func: Function, limit: number): Function { | |
let inThrottle: boolean; | |
return function (...args: any[]) { | |
if (!inThrottle) { | |
func.apply(this, args); | |
inThrottle = true; | |
setTimeout(() => inThrottle = false, limit); | |
} | |
}; | |
} | |
static css(element: HTMLElement | string, property: string, value?: string): string | void { | |
const el = typeof element === "string" ? $D.get(element) : element; | |
if (!el) return; | |
if (value === undefined) { | |
return getComputedStyle(el).getPropertyValue(property); | |
} else { | |
el.style.setProperty(property, value); | |
} | |
} | |
static awaitAll(tasks: (() => any | Promise<any>)[]): Promise<any[]> { | |
const promises = tasks.map(task => { | |
try { | |
const result = task(); | |
if (result instanceof Promise) { | |
return result; // If it's a promise, return it | |
} else { | |
return Promise.resolve(result); // If it's sync, wrap it in a promise | |
} | |
} catch (error) { | |
return Promise.reject(error); // Handle errors | |
} | |
}); | |
return Promise.all(promises); | |
} | |
static live<T extends HTMLElement>( | |
event: string, | |
selector: string, | |
fn: (event?: Event, el?: T) => void, | |
options?: AddEventListenerOptions, | |
parentEl: HTMLElement | DocumentFragment | null = document.body // Default to document.body | |
): void { | |
const applyListeners = () => { | |
$D.onAll(event, selector, fn, options, parentEl); | |
}; | |
applyListeners(); // Apply listeners initially | |
const observer = new MutationObserver((mutationsList) => { | |
for (const mutation of mutationsList) { | |
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { | |
applyListeners(); // Reapply listeners on DOM changes | |
break; // No need to check other mutations if childList changed | |
} | |
} | |
}); | |
observer.observe(parentEl instanceof DocumentFragment ? parentEl : parentEl as HTMLElement, { | |
childList: true, | |
subtree: true, | |
}); | |
} | |
static on<T extends HTMLElement>( | |
event: string, | |
selector: string | HTMLElement | Element | DocumentFragment, | |
fn: (event?: Event, el?: T) => void, | |
options?: AddEventListenerOptions, | |
parentEl?: HTMLElement | DocumentFragment | null | |
): T | null { | |
if (typeof selector === "string") { | |
const elements = parentEl | |
? parentEl.querySelectorAll(selector) as NodeListOf<T> | |
: document.querySelectorAll(selector) as NodeListOf<T>; | |
if (elements.length > 1) { | |
// Delegate to onAll if multiple elements match | |
DOMUtils.onAll(event, elements, fn, options); | |
return null; // Return null since multiple elements were handled | |
} | |
const target = elements[0]; | |
if (target) { | |
target.addEventListener(event, (e: Event) => { | |
fn(e, target); | |
}, options); | |
return target; | |
} else { | |
return null; | |
} | |
} else if (selector instanceof Element || selector instanceof HTMLElement) { | |
selector.addEventListener(event, (e: Event) => { | |
fn(e, selector as T); | |
}, options); | |
return selector as T; | |
} else if (selector instanceof DocumentFragment) { | |
// Handle DocumentFragment: Find the first matching element within. | |
const target = selector.querySelector(selector as unknown as string) as T | null; // Query inside it | |
if (target) { | |
target.addEventListener(event, (e: Event) => { | |
fn(e, target as T); | |
}, options); | |
} | |
return target; | |
} | |
return null; | |
} | |
static removeChilds(selector: string | HTMLElement): void { | |
const parent = typeof (selector) === "string" ? DOMUtils.get(selector) : selector; | |
if (!parent) return; | |
while (parent.firstChild) { | |
parent.firstChild.remove() | |
} | |
} | |
static create<T extends HTMLElement>(p: string | T, textContent?: string): T { | |
let node: T; | |
typeof (p) === "string" ? node = document.createElement(p) as T : node = p; | |
if (textContent) | |
node.textContent = textContent; | |
return node; | |
} | |
static toDOM(html: string): DocumentFragment { | |
const template = document.createElement('template'); | |
template.innerHTML = html.trim(); | |
return template.content; | |
} | |
static parentUntil(element: HTMLElement, selector: string) { | |
let currentElement = element; | |
while (currentElement) { | |
if (currentElement.matches(selector)) { | |
return currentElement; | |
} | |
currentElement = currentElement.parentElement as HTMLElement; | |
} | |
return null; | |
} | |
static ElementToSelector(el: HTMLElement | Element): string { | |
if (!(el instanceof HTMLElement)) { | |
throw new Error('Invalid argument: el must be an HTMLElement'); | |
} | |
let path = []; | |
while (el.nodeType === Node.ELEMENT_NODE) { | |
let selector = el.nodeName.toLowerCase(); | |
if (el.id) { | |
selector += '#' + el.id; | |
path.unshift(selector); | |
break; | |
} else { | |
let sib = el, nth = 1; | |
while (sib.previousElementSibling) { | |
sib = sib.previousElementSibling; | |
nth++; | |
} | |
if (nth !== 1) { | |
selector += ':nth-child(' + nth + ')'; | |
} | |
path.unshift(selector); | |
} | |
el = el.parentNode as HTMLElement; | |
} | |
return path.join(' > '); | |
} | |
static nextSibling(el: HTMLElement | string): HTMLElement | null { | |
const element = typeof el === "string" ? DOMUtils.get(el) : el; | |
return element && element.nextElementSibling instanceof HTMLElement ? element.nextElementSibling : null; | |
} | |
static previousSibling(el: HTMLElement | string): HTMLElement | null { | |
const element = typeof el === "string" ? DOMUtils.get(el) : el; | |
return element && element.previousElementSibling instanceof HTMLElement ? element.previousElementSibling as HTMLElement : null; | |
} | |
static data(el: HTMLElement | string, key: string, value?: string | null): string | null | undefined { | |
const element = typeof el === "string" ? DOMUtils.get(el) : el; | |
if (!element) return undefined; | |
if (value === undefined) { | |
return element.dataset[key]; | |
} else if (value === null) { | |
delete element.dataset[key]; | |
} else { | |
element.dataset[key] = value; | |
} | |
return value; | |
} | |
static closest<T extends HTMLElement>(el: HTMLElement | string, selector: string): T | null { | |
const element = typeof el === "string" ? DOMUtils.get(el) : el; | |
return element ? element.closest(selector) as T | null : null; | |
} | |
static parent(el: HTMLElement | string): HTMLElement | null { | |
const element = typeof el === "string" ? DOMUtils.get(el) : el; | |
return element ? element.parentElement : null; | |
} | |
static insertBefore(newElement: HTMLElement, referenceElement: HTMLElement | string): void { | |
const refEl = typeof referenceElement === "string" ? DOMUtils.get(referenceElement) : referenceElement; | |
if (refEl && refEl.parentNode) { | |
refEl.parentNode.insertBefore(newElement, refEl); | |
} | |
} | |
static insertAfter(newElement: HTMLElement, referenceElement: HTMLElement | string): void { | |
const refEl = typeof referenceElement === "string" ? DOMUtils.get(referenceElement) : referenceElement; | |
if (refEl && refEl.parentNode) { | |
refEl.parentNode.insertBefore(newElement, refEl.nextSibling); | |
} | |
} | |
static hasClass(element: HTMLElement | string, classNames: string | string[]): boolean { | |
const el = typeof element === "string" ? DOMUtils.get(element) : element; | |
if (!el) return false; | |
if (typeof classNames === "string") { | |
return el.classList.contains(classNames); | |
} else if (Array.isArray(classNames)) { | |
return classNames.every(className => el.classList.contains(className)); | |
} | |
return false; // Handle invalid input (e.g., non-string, non-array) | |
} | |
static addClass(element: HTMLElement | string, classNames: string | string[]): void { | |
const el = typeof element === "string" ? DOMUtils.get(element) : element; | |
if (!el) return; | |
if (typeof classNames === "string") { | |
el.classList.add(classNames); | |
} else if (Array.isArray(classNames)) { | |
classNames.forEach(className => el.classList.add(className)); | |
} // No else needed: handles invalid input gracefully (does nothing) | |
} | |
static removeClass(element: HTMLElement | string, classNames: string | string[]): void { | |
const el = typeof element === "string" ? DOMUtils.get(element) : element; | |
if (!el) return; | |
if (typeof classNames === "string") { | |
el.classList.remove(classNames); | |
} else if (Array.isArray(classNames)) { | |
classNames.forEach(className => el.classList.remove(className)); | |
} // No else needed: handles invalid input gracefully (does nothing) | |
} | |
static toggleClass(element: Element | HTMLElement | string, classNames: string | string[]): void { | |
const el = typeof element === "string" ? DOMUtils.get(element) : element; | |
if (!el) return; | |
if (typeof classNames === "string") { | |
el.classList.toggle(classNames); | |
} else if (Array.isArray(classNames)) { | |
classNames.forEach(className => el.classList.toggle(className)); | |
} // No else needed: handles invalid input gracefully (does nothing) | |
} | |
static html(el: HTMLElement | string, htmlContent?: string | null): string | null | undefined { | |
const element = typeof el === "string" ? DOMUtils.get(el) : el; | |
if (!element) return undefined; | |
if (htmlContent === undefined) { | |
return element.innerHTML; | |
} else if (htmlContent === null) { | |
element.innerHTML = ''; | |
} else { | |
element.innerHTML = htmlContent; | |
} | |
return htmlContent; | |
} | |
static repeat<T>( | |
items: T[], | |
itemTemplate: (item: T, index: number) => string, | |
container: HTMLElement | string | null = null, | |
joinString: string = '' | |
): void | string { | |
const html = items.map((item, index) => itemTemplate(item, index)).join(joinString); | |
if (container) { | |
const targetContainer = typeof container === "string" ? DOMUtils.get(container) : container; | |
if (targetContainer) { | |
DOMUtils.html(targetContainer, html); | |
return; | |
} else { | |
return html; | |
} | |
} else { | |
return html; | |
} | |
} | |
static wrappedList<T>( | |
items: T[], | |
itemTemplate: (item: T, index: number) => string, | |
wrapperTag: string, | |
wrapperClass?: string, | |
itemClass?: string | |
): string { | |
let listItems = items.map((item, index) => { | |
const itemContent = itemTemplate(item, index); | |
if (itemClass) { | |
return `<${wrapperTag === 'ul' || wrapperTag === 'ol' ? 'li' : 'div'} class="${itemClass}">${itemContent}</${wrapperTag === 'ul' || wrapperTag === 'ol' ? 'li' : 'div'}>`; | |
} else { | |
return itemContent; | |
} | |
}).join(''); | |
const wrapperClasses = wrapperClass ? ` class="${wrapperClass}"` : ''; | |
return `<${wrapperTag}${wrapperClasses}>${listItems}</${wrapperTag}>`; | |
} | |
static appendToRepeat<T>( | |
items: T[], | |
itemTemplate: (item: T, index: number) => string, | |
container: HTMLElement | string, | |
joinString: string = '' | |
): void { | |
const targetContainer = typeof container === "string" ? DOMUtils.get(container) : container; | |
if (!targetContainer) return; | |
const html = DOMUtils.repeat(items, itemTemplate, null, joinString); | |
DOMUtils.html(targetContainer, (targetContainer.innerHTML || '') + html); | |
} | |
static appendToWrappedList<T>( | |
items: T[], | |
itemTemplate: (item: T, index: number) => string, | |
container: HTMLElement | string, | |
wrapperTag: string, | |
wrapperClass?: string, | |
itemClass?: string | |
): void { | |
const targetContainer = typeof container === "string" ? DOMUtils.get(container) : container; | |
if (!targetContainer) return; | |
const html = DOMUtils.wrappedList(items, itemTemplate, wrapperTag, wrapperClass, itemClass); | |
DOMUtils.html(targetContainer, (targetContainer.innerHTML || '') + html); | |
} | |
static observe<T extends object | any[]>( // Allow arrays | |
data: T, | |
updateCallback: (newData: T) => void | |
): T { | |
const handler: ProxyHandler<T> = { | |
set: (target, property: string | symbol, value) => { | |
(target as any)[property] = value; | |
updateCallback(target); | |
return true; | |
}, | |
}; | |
return new Proxy(data, handler); | |
} | |
static bindText( | |
data: any, | |
property: string, | |
element: HTMLElement | string, | |
joinString: string = ', ' // Optional join string for arrays | |
): void { | |
const targetElement = typeof element === "string" ? DOMUtils.get(element) : element; | |
if (!targetElement) return; | |
const updateText = (newData: any) => { | |
const value = newData[property]; | |
if (Array.isArray(value)) { | |
targetElement.textContent = value.join(joinString); | |
} else { | |
targetElement.textContent = value; | |
} | |
}; | |
const observedData = DOMUtils.observe(data, updateText); | |
updateText(observedData); // Initial render | |
} | |
static bindAttribute( | |
data: any, | |
property: string, | |
element: HTMLElement | string, | |
attribute: string | |
): void { | |
const targetElement = typeof element === "string" ? DOMUtils.get(element) : element; | |
if (!targetElement) return; | |
const updateAttribute = (newData: any) => { | |
targetElement.setAttribute(attribute, newData[property]); | |
}; | |
const observedData = DOMUtils.observe(data, updateAttribute); | |
updateAttribute(observedData); // Initial render | |
} | |
static bindTemplate( | |
data: any, | |
template: string, | |
element: HTMLElement | string, | |
updateCallback: (newData: any) => string = (data) => JSON.stringify(data) | |
): void { | |
const targetElement = typeof element === "string" ? DOMUtils.get(element) : element; | |
if (!targetElement) return; | |
const render = (newData: any) => { | |
const renderedTemplate = template.replace(/{{(.*?)}}/g, (match, prop) => { | |
return newData[prop.trim()]; | |
}); | |
targetElement.innerHTML = renderedTemplate; | |
}; | |
const observedData = DOMUtils.observe(data, render); | |
render(observedData); // Initial render | |
} | |
static observeAll<T extends object>( | |
data: T, | |
updateCallback: (newData: T) => void | |
): T { | |
const observedData = this.observe(data, updateCallback); | |
const observeNested = (obj: any) => { | |
if (typeof obj === 'object' && obj !== null) { | |
for (const key in obj) { | |
if (obj.hasOwnProperty(key)) { | |
if (typeof obj[key] === 'object' && obj[key] !== null) { | |
obj[key] = DOMUtils.observe(obj[key], updateCallback); | |
observeNested(obj[key]); | |
} | |
} | |
} | |
} | |
} | |
observeNested(observedData); | |
return observedData; | |
} | |
} | |
export const $D = DOMUtils; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment