Created
April 22, 2024 17:54
-
-
Save theacodes/a5ee193d7ca7a80d54a58114e1f5a6f7 to your computer and use it in GitHub Desktop.
Lit @change decorator
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
/** | |
* Helper for watching reactive properties (@property) or internal state (@state) | |
* and calling a callback. | |
* | |
* Adapted from Shoelace: | |
* - https://github.com/shoelace-style/shoelace/blob/64996b2d3512a13d2ec68146fb92164d03e07e6a/src/internal/watch.ts | |
*/ | |
import type { PropertyValues, ReactiveElement } from "lit"; | |
type MixinBase<ExpectedBase extends ReactiveElement = ReactiveElement> = new (...args: any[]) => ExpectedBase; | |
type MixinReturn<MixinBase, MixinClass = object> = (new (...args: any[]) => MixinClass) & MixinBase; | |
type UpdateHandler = (prev?: any, next?: any) => void; | |
type NonUndefined<A> = A extends undefined ? never : A; | |
type UpdateHandlerFunctionKeys<T extends object> = { | |
[K in keyof T]-?: NonUndefined<T[K]> extends UpdateHandler ? K : never; | |
}[keyof T]; | |
interface WatchOptions { | |
afterFirstUpdate?: boolean; | |
when?: "before" | "after"; | |
} | |
type Watcher = Required<WatchOptions> & { | |
callback: UpdateHandler; | |
}; | |
const watchers = Symbol("watchers"); | |
const addWatcher = Symbol("addWatcher"); | |
const notifyWatchers = Symbol("notifyWatchers"); | |
interface IIsWatchable { | |
[watchers]?: Map<string, Watcher>; | |
[addWatcher](propertyName: PropertyKey, watcher: Watcher): void; | |
[notifyWatchers](when: "before" | "after", changedProperties: PropertyValues): void; | |
} | |
/** | |
* Runs when observed properties (@property, @state) change. | |
* | |
* This runs *before* but before the component updates, so it's useful for | |
* *reacting* to property changes, for example, setting multiple attributes | |
* based on the value of a single one. | |
* | |
* The class must have the IsWatchable mixin | |
* | |
* To wait for an update to complete after a change occurs, pass in | |
* `{when: "after"}` | |
* | |
* To start watching after the initial render, pass in `{ afterFirstUpdate: true }` | |
* or use `this.hasUpdated` in the handler. | |
* | |
* Example: | |
* | |
* @watch('propName') | |
* handlePropChange(oldValue, newValue) { | |
* ... | |
* } | |
*/ | |
export function watch<T extends IIsWatchable & ReactiveElement, TKey extends keyof T>( | |
propertyName: TKey, | |
options?: WatchOptions, | |
) { | |
return (proto: T, callbackName: UpdateHandlerFunctionKeys<T>): any => { | |
(proto.constructor as typeof ReactiveElement).addInitializer((element: ReactiveElement): void => { | |
const e = element as T; | |
e[addWatcher](propertyName, { | |
callback: e[callbackName] as UpdateHandler, | |
afterFirstUpdate: false, | |
when: "before", | |
...options, | |
}); | |
}); | |
}; | |
} | |
export const IsWatchable = <T extends MixinBase>(superClass: T) => { | |
class IsWatchable extends superClass implements IIsWatchable { | |
[watchers]?: Map<string, Watcher>; | |
[addWatcher](propertyName: string, watcher: Watcher): void { | |
if (this[watchers] === undefined) { | |
this[watchers] = new Map(); | |
} | |
this[watchers].set(propertyName, watcher); | |
} | |
[notifyWatchers](when: "before" | "after", changedProperties: PropertyValues) { | |
if (!this[watchers]) { | |
return; | |
} | |
for (const [key, watcher] of this[watchers]) { | |
if (watcher.when !== when) { | |
continue; | |
} | |
if (watcher.afterFirstUpdate && !this.hasUpdated) { | |
continue; | |
} | |
if (!changedProperties.has(key)) { | |
continue; | |
} | |
const oldValue: unknown = changedProperties.get(key); | |
const newValue: unknown = (this as Record<string, unknown>)[key]; | |
watcher.callback.call(this, oldValue, newValue); | |
} | |
} | |
protected override update(changedProperties: PropertyValues): void { | |
this[notifyWatchers]("before", changedProperties); | |
super.update(changedProperties); | |
this[notifyWatchers]("after", changedProperties); | |
} | |
} | |
return IsWatchable as MixinReturn<T, IIsWatchable>; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment