@ou-imdt/utils
Version:
Utility library for interactive media development
126 lines (106 loc) • 4.88 kB
JavaScript
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);