UNPKG

classy-solid

Version:

Solid.js reactivity patterns for classes, and class components.

161 lines (151 loc) 5.89 kB
import { getInheritedDescriptor } from 'lowclass/dist/getInheritedDescriptor.js'; import { $PROXY, untrack } from 'solid-js'; import { createSignalFunction } from './createSignalFunction.js'; import { isMemoGetter, isSignalGetter } from '../_state.js'; /** * Convert properties on an object into Solid signal-backed properties. * * There are two ways to use this: * * 1. Define which properties to convert to signal-backed properties by * providing property names as trailing arguments. Properties that are * function-valued (methods) are included as values of the signal properties. * 2. If no property names are provided, all non-function-valued properties on * the object will be automatically converted to signal-backed properties. * * If any property is already memoified with `memoify()`, or already signalified * with `signalify()`, it will be skipped. * * Example with a class: * * ```js * import {signalify} from 'classy-solid' * import {createEffect} from 'solid-js' * * class Counter { * count = 0 * * constructor() { * signalify(this, 'count') * setInterval(() => this.count++, 1000) * } * } * * const counter = new Counter * * createEffect(() => { * console.log('count:', counter.count) * }) * ``` * * Example with a plain object: * * ```js * import {signalify} from 'classy-solid' * import {createEffect} from 'solid-js' * * const counter = { * count: 0 * } * * signalify(counter, 'count') * setInterval(() => counter.count++, 1000) * * createEffect(() => { * console.log('count:', counter.count) * }) * ``` */ /** This overload is for initial value support for downstream use cases. */ export function signalify(obj, ...props) { // Special case for Solid proxies: if the object is already a solid proxy, // all properties are already reactive, no need to signalify. // @ts-expect-error special indexed access const proxy = obj[$PROXY]; if (proxy) return obj; const skipFunctionProperties = props.length === 0; const _props = props.length ? props : Object.keys(obj).concat(Object.getOwnPropertySymbols(obj)); // Use `untrack` here to be extra safe the initial value doesn't count as a // dependency and cause a reactivity loop. for (const prop of _props) { const isTuple = Array.isArray(prop); // We cast from PropertyKey to PropKey because keys can't actually be number, only string | symbol. const _prop = isTuple ? prop[0] : prop; const initialValue = isTuple ? prop[1] : untrack(() => obj[_prop]); createSignalAccessor__(obj, _prop, initialValue, skipFunctionProperties); } return obj; } // propsSetAtLeastOnce is a Set that tracks which reactive properties have been // set at least once. const propsSetAtLeastOnce = new WeakMap(); // @lume/element uses this to detect if a reactive prop has been set, and if so // will not overwrite the value with any pre-existing value from custom element // pre-upgrade. export function isPropSetAtLeastOnce__(instance, prop) { return !!propsSetAtLeastOnce.get(instance)?.has(prop); } export function trackPropSetAtLeastOnce__(instance, prop) { if (!propsSetAtLeastOnce.has(instance)) propsSetAtLeastOnce.set(instance, new Set()); propsSetAtLeastOnce.get(instance).add(prop); } export function createSignalAccessor__(obj, prop, initialVal, skipFunctionProperties = false) { let descriptor = getInheritedDescriptor(obj, prop); let originalGet; let originalSet; const isAccessor = !!(descriptor?.get || descriptor?.set); if (descriptor) { if (skipFunctionProperties && typeof descriptor.value === 'function') return; originalGet = descriptor.get; originalSet = descriptor.set; // If the original getter is already a signal getter, skip re-signalifying. if (originalGet && isSignalGetter.has(originalGet)) return; // If the original getter is already a memo getter, skip signalifying. if (originalGet && isMemoGetter.has(originalGet)) return; // Signals require both getter and setter to work properly. if (isAccessor && !(originalGet && originalSet)) return; if (!isAccessor) { // No need to make a signal that can't be written to. if (!descriptor.writable) return warnNotWritable(prop); // If there was a value descriptor, trust it as the source of truth // for initialVal. For example, if the user class modifies the value // after the initializer, it will have a different value than what // we tracked from the initializer. initialVal = descriptor.value; } } const signalStorage = new WeakMap(); const newDescriptor = { configurable: true, enumerable: descriptor?.enumerable, get: isAccessor ? function () { getSignal__(this, signalStorage, initialVal)(); return originalGet.call(this); } : function () { return getSignal__(this, signalStorage, initialVal)(); }, set: isAccessor ? function (newValue) { originalSet.call(this, newValue); trackPropSetAtLeastOnce__(this, prop); const s = getSignal__(this, signalStorage, initialVal); s(typeof newValue === 'function' ? () => newValue : newValue); } : function (newValue) { trackPropSetAtLeastOnce__(this, prop); const s = getSignal__(this, signalStorage, initialVal); s(typeof newValue === 'function' ? () => newValue : newValue); } }; isSignalGetter.add(newDescriptor.get); Object.defineProperty(obj, prop, newDescriptor); } export function getSignal__(obj, storage, initialVal) { let s = storage.get(obj); if (!s) storage.set(obj, s = createSignalFunction(initialVal, { equals: false })); return s; } function warnNotWritable(prop) { console.warn(`The \`@signal\` decorator was used on a property named "${String(prop)}" that is not writable. Reactivity is not enabled for non-writable properties.`); } //# sourceMappingURL=signalify.js.map