import {Eventing_A} from './as-event.js';
import state from '../services/processing-queue.js';
import {ErrorCode} from '../../exception/exceptionDesc.js';
/**
* @ignore
*/
const logModule = 'as-base';
/**
* @description Base class for handling objects
* @class TComponents.Base_A
* @extends TComponents.Eventing_A
* @memberof TComponents
* @param {object} [props={}] - Initial properties for the object
* @throws Will throw an error if props is not an object
* @example
* const baseInstance = new TComponents.Base_A({prop1: 'value1', prop2: 42});
* baseInstance.on('event1', (data) => {
* console.log('Event 1 triggered with data:', data);
* });
*/
export class Base_A extends Eventing_A {
constructor(props = {}) {
super();
if (typeof props !== 'object') {
Logger.e(logModule, ErrorCode.InvalidComponentProp, 'The props should be an object.', props);
throw ErrorCode.InvalidComponentProp;
}
this.initialized = false;
this.noCheck = [];
this._initPropsDependencies = [];
this._props = this._getAllProps(props);
this._prevProps = Object.assign({}, this._props);
}
/**
* @description Returns an object with expected input properties together with their initial value.
* Every child class shall have a {@link TComponents.Base_A#defaultProps} to register its corresponding input properties.
* @member TComponents.Base_A#defaultProps
* @method
* @protected
* @returns {object}
* @example
* class MyComponent extends TComponents.Component_A {
* constructor(parent, props){}
* defaultProps(){
* return {
* myProp1: '',
* myProp2: 0,
* myProp3: false,
* myProp4: { a: 'A', b: 'B'}
* }
* }
* }
*/
defaultProps() {
return {options: {async: false}};
}
/**
* @description Register the properties that trigger an onInit when changed with setProps().
* Input value is a string or array of strings with the name of the corresponding props
* @member TComponents.Base_A#initPropsDep
* @method
* @protected
* @param {string|string[]} props - The properties that trigger an onInit when changed
* @throws Will throw an error if the new value is not a string or an array of strings
* @example
* this.initPropsDep(['module', 'variable']);
*/
initPropsDep(props) {
if (typeof props === 'string') {
props = [props];
} else if (!Array.isArray(props)) {
Logger.e(logModule, ErrorCode.InvalidNewPropName, props);
throw ErrorCode.InvalidNewPropName;
}
this._initPropsDependencies = [...this._initPropsDependencies, ...props];
}
/**
* @description Method used to update one or multiple component input properties. A change of property using this method
* will trigger at least a {@link TComponent.Base_A#render()} call. If at least one of the given properties is listed in
* the {@link TComponent.Base_A#initPropsDep} array, then a {@link TComponent.Base_A#init()} is called before the {@link TComponent.Base_A#render()}.
* @member TComponents.Base_A#initPropsDep
* @method
* @param {object} newProps - Object including the property or properties to be updated.
* @param {Function | null} [onRender=null] - Function to be executed once after the component has been rendered.
* @param {boolean} [sync=false] - Whether to update synchronously or not
* @param {boolean} [force=false] - Whether to force the update or not
* @returns {Promise<boolean>} - true if the component has been updated, false otherwise
*/
async setProps(newProps, onRender = null, sync = false, force = false) {
const {props, modified} = Base_A._updateProps(newProps, this._props, false, this.noCheck);
/**
* Internal element containing the component properties. A copy of it can be obtained
* outside the component with {@link TComponent.Base_A#getProps} method. To modify the props from outside, the method
* {@link TComponent.Base_A#setProps} can be used.
* @private
*/
this._props = props;
// if onRender is a function, register event listener to be executed after render
if (onRender && typeof onRender === 'function') this.once('render', onRender);
if ((force || modified) && this.initialized) {
if (sync) {
await this._componentDidUpdate();
} else {
// Put the update in the queue so that every rendering is done synchronously
// one after the other
state.q.push(this._componentDidUpdate.bind(this));
}
return true;
}
return false;
}
/**
* @description Updates the component properties using the internal merge logic
* and synchronizes the previous properties snapshot.
*
* This method applies the given properties directly to the internal `_props`
* object without triggering the component update lifecycle
* (`init()` or `render()`).
*
* After the update, `_prevProps` is synchronized with the new `_props`,
* establishing a new baseline state for future change detection.
*
* ⚠️ This method should be used only for internal or controlled updates.
* To trigger a normal component update flow, use {@link TComponents.Base_A#setProps}.
*
* @member TComponents.Base_A#commitProps
* @method
* @param {object} newProps - Object containing the properties to update.
* @returns {object} An object containing the updated properties and a flag indicating
* whether any property value changed.
*/
commitProps(newProps) {
const result = Base_A._updateProps(newProps, this._props, false, this.noCheck);
this._props = result.props;
this._prevProps = Object.assign({}, this._props);
return result;
}
/**
* @description Returns a copy of the component properties. Notice that the returning value does not have a
* reference to the internal properties of the component. i.e. changing a value in that object
* does not affect the component itself. To change the properties of the component use the {@link TComponent.Base_A#setProps} method.
* @member TComponents.Base_A#getProps
* @method
* @returns {object}
*/
getProps() {
return Base_A._deepClone(this._props);
}
/**
* @description Abstract function for asynchronous initialization of the component. This function is overwritten in {@link TComponents.Component_A}
* @member TComponents.Base_A#init
* @method
* @async
* @abstract
* @protected
* @returns {Promise<object>} The TComponents instance on which this method was called.
*/
async init() {}
/**
* @description Abstract function for DOM rendering. This function is overwritten in {@link TComponents.Component_A}
* @member TComponents.Base_A#render
* @method
* @abstract
* @protected
* @async
*/
async render() {}
/**
* @return {object}
*/
get props() {
return this.getProps();
}
/**
* @description The properties of the component.
* @member {object} TComponents.Base_A#props
* @instance
* @example
* const base = new TComponents.Base_A();
* base.props = { key: 'value' };
*/
set props(props) {
this.setProps(props);
}
/**
* @description Get all properties, including default properties from the prototype chain.
* @member TComponents.Base_A~_getAllProps
* @method
* @private
* @param {object} p - Initial properties
* @param {boolean} [restError=true] - Whether to throw error for unexpected props
* @returns {object}
*/
_getAllProps(p, restError = true) {
const {props} = Base_A._updateProps(p, this._getAllDefaultProps(), restError, this.noCheck);
return props;
}
/**
* @description Get all default properties from the prototype chain.
* @member TComponents.Base_A~_getAllDefaultProps
* @method
* @private
* @returns {object}
*/
_getAllDefaultProps() {
let props = {};
let proto = this;
const noCheck = [];
// Traverse up the prototype chain and merge all defaultProps
while (proto) {
if (proto.defaultProps) {
props = Object.assign({}, proto.defaultProps(), props);
if (proto.noCheck) {
proto.noCheck.forEach((element) => {
if (!noCheck.includes(element)) {
noCheck.push(element);
}
});
}
}
proto = Object.getPrototypeOf(proto);
}
this.noCheck = noCheck;
return props;
}
/**
* @description Update properties with new values and determine if any properties were modified.
* @member TComponents.Base_A._updateProps
* @method
* @static
* @private
* @param {object} [newProps={}] - New properties to update
* @param {object} [prevProps={}] - Previous properties
* @param {boolean} [restError=false] - Whether to throw error for unexpected props
* @param {string[]} [noCheck=[]] - Properties to exclude from modification check
* @returns {object} An object containing the updated properties and a boolean indicating if any properties were modified
*/
static _updateProps(newProps = {}, prevProps = {}, restError = false, noCheck = []) {
let modified = false;
let props = Object.keys(prevProps).reduce((acc, key) => {
if (Object.prototype.hasOwnProperty.call(newProps, key)) {
if (
!Array.isArray(prevProps[key]) &&
!noCheck.includes(key) &&
typeof prevProps[key] === 'object' &&
prevProps[key] !== null &&
!(prevProps[key] instanceof HTMLElement)
) {
if (newProps[key] && newProps[key].constructor !== Object) {
// If the key in newProps is a class instance, replace it entirely
acc[key] = newProps[key];
modified = modified || newProps[key] !== prevProps[key];
} else {
const nestedProps = Base_A._updateProps(newProps[key], prevProps[key], restError, noCheck);
modified = modified || nestedProps.modified;
acc[key] = nestedProps.props;
}
} else {
acc[key] = newProps[key];
modified = modified || newProps[key] !== prevProps[key];
}
} else {
// key not existing in new prop, so the value is the same as before
acc[key] = prevProps[key];
}
return acc;
}, {});
const rest = Object.keys(newProps).reduce((acc, key) => {
if (!Object.prototype.hasOwnProperty.call(prevProps, key)) {
acc[key] = newProps[key];
}
return acc;
}, {});
if (restError && Object.keys(rest).length !== 0) {
// Logger.w('TComponents.Base_A', `Unexpected props: ${JSON.stringify(rest)}`);
// throw new Error(`Unexpected props: ${JSON.stringify(rest)}`);
}
return {props, modified};
}
/**
* @description Deeply checks if two values are equal.
* Works with objects and arrays by recursively comparing keys and values.
*
* Limitations:
* - Does not handle special objects like Date, Map, Set, RegExp properly.
* - Does not handle circular references.
*
* @member TComponents.Base_A._deepEqual
* @method
* @static
* @private
* @param {any} a - First value to compare
* @param {any} b - Second value to compare
* @returns {boolean} - True if values are deeply equal, otherwise false
*/
static _deepEqual(a, b) {
// If both values are strictly equal (covers primitives and identical references)
if (a === b) return true;
// If either value is not an object (null included), they are not equal
if (typeof a !== 'object' || typeof b !== 'object' || a == null || b == null) {
return false;
}
// Compare the number of keys in both objects
var keysA = Object.keys(a);
var keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
// Recursively compare each key's value
for (var i = 0; i < keysA.length; i++) {
var key = keysA[i];
// Use indexOf instead of includes (better ES5 compatibility)
if (keysB.indexOf(key) === -1 || !Base_A._deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
/**
* @description Handle component updates and determine if dependencies have changed.
* @member TComponents.Base_A~_componentDidUpdate
* @method
* @private
* @async
* @returns {Promise<void>}
*/
async _componentDidUpdate() {
function checkDepsDiff(deps, props, prevProps) {
for (let i = 0; i < deps.length; i++) {
const dep = deps[i];
if (!Base_A._deepEqual(props[dep], prevProps[dep])) {
return true;
}
}
return false;
}
const isDepsDiff =
this._initPropsDependencies && checkDepsDiff(this._initPropsDependencies, this._props, this._prevProps);
// Update previous props
this._prevProps = Object.assign({}, this._props);
// Trigger update of the component
if (isDepsDiff) {
await this.init();
} else {
await this.render();
}
}
/**
* @description Creates a clone of an object, including objects with circular references,
* functions, and non-enumerable properties.
* @member TComponents.Base_A._deepClone
* @method
* @static
* @private
* @param {object} obj - The object to clone
* @returns {object} The cloned object
*/
static _deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
let clone;
// ⚠️ Note: directly returns the TComponent instance.
// The reason for this change can be found in Section `View Type Component Design.2.2`.
if (typeof obj === 'object' && obj._isTComponent) {
return obj;
} else if (obj instanceof Date) {
clone = new Date(obj.getTime());
} else if (obj instanceof RegExp) {
clone = new RegExp(obj.source, obj.flags);
} else if (obj instanceof Set) {
clone = new Set(obj);
} else if (obj instanceof Map) {
clone = new Map(obj);
} else if (obj instanceof Error) {
clone = new Error(obj.message);
} else if (obj instanceof Array) {
clone = [];
for (let i = 0; i < obj.length; i++) {
clone[i] = Base_A._deepClone(obj[i]);
}
} else if (obj instanceof HTMLElement) {
// clone = obj.cloneNode(true);
// if (obj.id && clone.id) {
// clone.id = API.generateUUID(); // Replace generateUniqueID() with your own logic to generate a unique ID
// }
// The reason for this change can be found in Section `View Type Component Design.2.2`.
return obj;
} else {
// clone = Object.create(Object.getPrototypeOf(obj));
clone = {};
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
clone[key] = Base_A._deepClone(obj[key]);
}
// clone[key] = Base_A._deepClone(obj[key]);
}
}
return clone;
}
}