Skip to content

Instantly share code, notes, and snippets.

@MagnusThor
Created February 26, 2025 17:54
Show Gist options
  • Save MagnusThor/cc1bc2e09ef4291032896ff7e72667d3 to your computer and use it in GitHub Desktop.
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!
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