UNPKG

@ou-imdt/utils

Version:

Utility library for interactive media development

126 lines (106 loc) 4.88 kB
import toSlugCase from '../toSlugCase.js'; // symbol for defining the library namespace (i.e. imdt) export const namespace = Symbol('namespace'); // symbol for defining a unique ID export const id = Symbol('id'); // symbol for defining the formatted component/constructor name export const name = Symbol('name'); // symbol for defining the instance index (used for creating unique IDs) export const index = Symbol('index'); // symbol for defining default state export const defaultState = Symbol('defaultState'); // symbol for defining initial state (e.g. state post first render) export const initialState = Symbol('initialState'); // symbol for exposing component state, including any internal/protected (symbol-keyed) state export const state = Symbol('state'); // symbol for exposing component state in a normalised way, with symbol-keyed props replaced with string-keyed equivalents export const plainState = Symbol('plainState'); /** * a base class written as a mixin for use with other base classes, e.g. LitElement */ export const BaseMixin = (superClass) => class Base extends superClass { /** * stores default state and is used to apply initial state, including internal state (symbols) * currently defined here but could be removed if required by components, catch any errors below if so * should always be extended, e.g. static [defaultState] = { ...super[defaultState], foo, bar, ... } */ static [defaultState] = {}; /** * lib namespace... used for defining custom elements */ static [namespace] = 'imdt'; constructor() { super(); // track instances created on constructor and store current index here for use in id this[index] = this.constructor[index] = ++this.constructor[index] || 0; // lowercase constructor/class name this[name] = toSlugCase(this.constructor.name); // an general purpose internal id (in addition to this.id, although could be exposed there too) this[id] = `${this[name]}-${this[index]}`; // long ids! // this[id] = `${this[name].match(/(^.)|(-.)?/g).join('')}-${this[index]}`; // too general // set initial state from default this[state] = this.constructor[defaultState]; } /** * gets current state relative to [defaultState] including any (internal) symbol-keyed props */ get [state]() { return this.#getState(Reflect.ownKeys(this.constructor[defaultState])); } /** * updates a components state with any values including internal symbol-keyed props * N.B. prevents* use of setters that ref private class fields, as class fields not yet defined * (same reason you can't use them with Lit reactive props) * *technically they can still be used but the initial assignment must be handled/prevented * @param values - */ set [state](values) { // console.log(values); Reflect.ownKeys(values).forEach((key) => { const value = values[key]; const { get, set } = Object.getOwnPropertyDescriptor(this.constructor.prototype, key) ?? {}; // allows values for accessor props without setters to be included in defaultState, although any value obviously not set // TODO check this^... what's the point?!! if (typeof get === 'function' && typeof set !== 'function') return; this[key] = value; // if (key === 'disabled') console.log('set state', key, value, this[key]); }); } /** * returns an object representing current component state with any symbol-keyed props * replaced with their string-keyed equivalents, e.g. 'Symbol('foo'):' becomes 'foo:' * here as a convenience for internally accessing all state in a normalised way */ get [plainState]() { return Object.fromEntries(Object.entries(this[state]).map(([key, value]) => { return [(typeof key === 'symbol') ? key.description : key, value]; })); } /** * gets current state relative to [defaultState] excluding any (internal) symbol-keyed props */ get state() { return this.#getState(Object.keys(this.constructor[defaultState])); } /** * updates a components state with values limited to [defaultState] minus internal symbol-keyed props */ set state(values) { const defaultKeys = Object.keys(this.constructor[defaultState]); // string-keyed only (no symbols) Object.entries(values).forEach(([key, value]) => { // ensure defaultState has key if (!defaultKeys.includes(key)) return; // quick check to see if type matches (may cause issues - props may allow multiple types) // if (typeof value !== typeof this.constructor[defaultState][key]) return; this[key] = value; }); } #getState(keys) { const entries = keys.map(key => [key, this[key]]); return Object.fromEntries(entries); } } /** * default export as a class for extending directly (modules, native etc.) */ export default BaseMixin(Object);