Skip to content

Instantly share code, notes, and snippets.

@Neophen
Last active February 6, 2025 18:11
Show Gist options
  • Save Neophen/5faea87df7b881316ad2bb70bdd50aed to your computer and use it in GitHub Desktop.
Save Neophen/5faea87df7b881316ad2bb70bdd50aed to your computer and use it in GitHub Desktop.
Phoenix LiveViewHook Class
import { LiveViewHook } from './live_view_hook'
/*
* Tracks height of an element when viewport resizes and sets a variable accordingly.
*
* Used to set the header offset.
*/
class HeightTrackerHook extends LiveViewHook {
private resizeObserver: ResizeObserver | null = null
mounted() {
this.resizeObserver = new ResizeObserver(this.setHeightProperty)
this.resizeObserver.observe(this.el)
}
beforeDestroy() {
this.resizeObserver?.unobserve(this.el)
}
private setHeightProperty = ([entry]: ResizeObserverEntry[]) => {
document.documentElement.style.setProperty(this.requiredAttr('data-height-var'), `${entry.contentRect.height}px`)
}
}
export default HeightTrackerHook.createViewHook()
import { ViewHook } from 'phoenix_live_view'
import { requiredAttr, requiredEl } from '../../utils'
interface Hook<T> extends ViewHook {
instance: T | null
}
type OnReply = (reply: unknown, ref: number) => unknown
export abstract class LiveViewHook {
protected el: HTMLElement
protected hook: ViewHook
mounted(): void {}
beforeUpdate(): void {}
updated(): void {}
beforeDestroy(): void {}
destroyed(): void {}
disconnected(): void {}
reconnected(): void {}
constructor(hook: ViewHook) {
this.hook = hook
this.el = hook.el
}
protected pushToServer(event: string, payload: object = {}, onReply?: OnReply) {
const targetView = this.el.getAttribute('phx-target') ?? this.el.getAttribute('data-target')
if (targetView === null || targetView === '') {
this.hook.pushEvent(event, payload, onReply)
return
}
this.hook.pushEventTo(targetView, event, payload, onReply)
}
protected handleEvent<T extends object>(event: string, callback: (payload: T) => void): void {
this.hook.handleEvent(event, (payload: object) => callback(payload as T))
}
protected requiredAttr = <T = string>(attribute: string): T => {
return requiredAttr<T>(this.el, attribute)
}
protected requiredChild = <T extends HTMLElement>(
selector: string,
type: { new (): T } = HTMLElement as unknown as { new (): T }
): T => {
return requiredEl<T>(this.el.querySelector<T>(selector), type)
}
protected requiredChildren = <T extends HTMLElement>(
selector: string,
type: { new (): T } = HTMLElement as unknown as { new (): T }
): T[] => {
const childrenElements = Array.from(this.el.querySelectorAll<T>(selector))
if (childrenElements.every((element) => requiredEl<T>(element, type))) {
return childrenElements
}
throw new Error(`All child elements must be of type: ${type.name}`)
}
static createViewHook<T extends LiveViewHook>(this: new (hook: ViewHook) => T): Hook<T> {
const HookClass = this as unknown as new (hook: ViewHook) => T
return {
instance: null,
mounted() {
this.instance = new HookClass(this)
this.instance.mounted()
},
beforeUpdate() {
this.instance?.beforeUpdate()
},
updated() {
this.instance?.updated()
},
beforeDestroy() {
this.instance?.beforeDestroy()
},
destroyed() {
this.instance?.destroyed()
this.instance = null
},
disconnected() {
this.instance?.disconnected()
},
reconnected() {
this.instance?.reconnected()
}
} as Hook<T>
}
}
export const requiredEl = <T extends HTMLElement>(
element: unknown,
type: { new (): T } = HTMLElement as unknown as { new (): T }
): T => {
if (!element) {
throw new Error(`Element must exist ${type.name}`)
}
if (!(element instanceof type)) {
throw new Error(`Element must be of type ${type.name}`)
}
return element
}
export const requiredAttr = <T = string>(el: Element, attribute: string): T => {
const value = el.getAttribute(attribute)
if (!value) {
throw new Error(`Attribute must exist: ${attribute}`)
}
return value as unknown as T
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment