UNPKG

lume

Version:

Build next-level interactive web applications.

136 lines 5.02 kB
import { observe, unobserve } from 'james-bond'; // We use this to enforce that the @receiver decorator is used on PropReceiver // classes. const isPropReceiverClass = Symbol(); /** * @class PropReceiver * * `mixin` * * Forwards properties of a specified `observedObject` onto properties of * `this` object. The properties to be received from `observedObject` are those * listed in the `receivedProperties` array, or the ones decorated with * `@receiver`. * * In particular, LUME uses this to forward properties of a behavior's host * element onto the behavior. * * Example: * * ```js * class SomeBehavior extends PropReceiver(BaseClass) { * receivedProperties = ['foo', 'bar'] * get observedObject() { * return this.element * } * } * * const behavior = new SomeBehavior() * const el = document.querySelector('.some-element-with-some-behavior') * el.foo = 123 * console.log(behavior.foo) // 123 * ``` */ export function PropReceiver(Base = Object) { return class PropReceiver extends Base { // @ts-ignore Make this unknown to the type system, otherwise we get "has or is using private name" errors due to declaration emit. :( // (https://github.com/microsoft/TypeScript/issues/35822) static [isPropReceiverClass] = true; connectedCallback() { super.connectedCallback?.(); this.receiveProps(); } disconnectedCallback() { super.disconnectedCallback?.(); this.unreceiveProps(); } /** * @abstract * @property {object} observedObject * * `abstract` `protected` `readonly` * * A subclass should specify the object to observe by defining a `get observedObject` getter. */ get observedObject() { throw new TypeError(`implement 'observedObject' in subclass`); } #propChangedCallback = (propName, value) => { ; this[propName] = value; }; receiveProps() { // Make it unique, before we pass it to observe(), just in case. if (this.receivedProperties) this.receivedProperties = Array.from(new Set(this.receivedProperties)); this.receiveInitialValues(); observe(this.observedObject, this.#getReceivedProps(), this.#propChangedCallback, { // inherited: true, // XXX the 'inherited' option doesn't work in this case. Why? }); } unreceiveProps() { unobserve(this.observedObject, this.#getReceivedProps(), this.#propChangedCallback); } /** * @property {string[]} receivedProperties * * `static` * * An array of strings, the properties of observedObject to observe. */ receivedProperties = []; #getReceivedProps() { const props = this.receivedProperties || []; // @prod-prune if (!Array.isArray(props)) throw new TypeError('Expected receivedProperties to be an array.'); return props; } receiveInitialValues() { const observed = this.observedObject; for (const prop of this.#getReceivedProps()) { if (prop in observed) { const value = observed[prop]; // @ts-expect-error indexed access of this this.#propChangedCallback(prop, value !== undefined ? value : this[prop]); } } } }; } function checkIsObject(o) { if (typeof o !== 'object') throw new Error('cannot use @receiver on class returning a non-object instance'); return true; } export function receiver(_, context) { const { kind, name } = context; if (kind === 'field') { return function (initialValue) { checkIsObject(this); trackReceiverProperty(this, name); return initialValue; }; } else if (kind === 'getter' || kind === 'setter' || kind === 'accessor') { context.addInitializer(function () { checkIsObject(this); trackReceiverProperty(this, name); }); } else { throw new TypeError('@receiver is for use only on class fields, getters/setters, and auto accessors. Also make sure your class extends from PropReceiver.'); } } function trackReceiverProperty(obj, name) { const ctor = obj.constructor; if (!ctor[isPropReceiverClass]) throw new TypeError('@receiver must be used on a property of a class that extends PropReceiver'); // Ensure we modify the own property if any, and not an inherited property which would break other inheriting objects. if (!obj.hasOwnProperty('receivedProperties')) obj.receivedProperties = [...(obj.receivedProperties || [])]; if (!obj.receivedProperties) debugger; obj.receivedProperties.push(name); } //# sourceMappingURL=PropReceiver.js.map