basic_as-view.js

import API from '../../api/ecosystem-base.js';
import {Component_A} from './as-component.js';

/**
 * @typedef {Map<Component_A, string>} TComponents.ViewChildProps
 * @description
 *   A map of view child components:
 *   - **Key**: an instance of `Component_A`
 *   - **Value**: the name of that child component as a string
 * @memberof TComponents
 */

/**
 * @ignore
 */
const logModule = 'as-view';

/**
 * @typedef {object} TComponents.ViewProps
 * @property {any} [id] - The ID of the view.
 * @property {string} [name=''] - Name of the view.
 * @property {string} [icon=''] - Icon associated with the view.
 * @property {string | HTMLElement | null} [content=null] - Content to display in the view.
 *  The value should be a string when passed via props, but null or an HTMLElement when used in component_a's views property.
 * @property {TComponents.Component_A[]} [children=[]] - Child components.
 * @memberof TComponents
 */

/**
 * @description Base view component extending Component_A.
 * Manages activation state, identification, icon, content, and child component mapping.
 * @class TComponents.View_A
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - The parent DOM element to which this view will attach.
 * @param {TComponents.ViewProps} [props={}] - Initial properties for the view.
 * @example
 * const view = new TComponents.View_A(document.body, {
 *   name: 'My View',
 *   icon: 'view-icon',
 *   content: 'This is the view content',
 *   children: [new TComponents.Component_A()]
 * });
 */
export class View_A extends Component_A {
  constructor(parent = null, props = {}) {
    super(parent, props);

    /** @type {TComponents.ViewProps} */
    this._props;

    /** @type {TComponents.ViewChildProps} */
    this._children = new Map();

    this._newChildren = new Map();
  }

  /**
   * @returns {string}
   */
  get icon() {
    return this._props.icon;
  }

  /**
   * @description Set the view's icon.
   * @member {string} TComponents.View_A#icon
   * @instance
   * @param {string} value - Icon name or URL.
   * @example
   * const view = new TComponents.View_A(document.body, {
   *   name: 'My View',
   *   icon: 'view-icon',
   *   content: 'This is the view content',
   *   children: [new TComponents.Component_A()]
   * });
   * view.icon = 'new-icon';
   */
  set icon(value) {
    this.commitProps({icon: value});
  }

  /**
   * @returns {HTMLElement|string|null}
   */
  get content() {
    return this._props.content;
  }

  /**
   * @description List of child components in the view.
   * The obtained value should be treated as a read-only property and should not be used for manipulation.
   * @member {TComponents.Component_A[]} TComponents.View_A#children
   * @instance
   */
  get children() {
    return Array.from(new Set([...this._props.children, ...this._newChildren.keys()]));
  }

  /**
   * @description Default property values for the view.
   * Overrides parent defaults; not including inherited props.
   * @member TComponents.View_A#defaultProps
   * @method
   * @protected
   * @returns {TComponents.ViewProps}
   */
  defaultProps() {
    return {
      name: '',
      icon: '',
      content: '',
      children: [],
    };
  }

  /**
   * @description Lifecycle method called when the component is initialized.
   * Override to perform async setup logic.
   * @member TComponents.View_A#onInit
   * @method
   * @async
   * @returns {Promise<void>}
   */
  async onInit() {
    this._children.clear();

    const childrenArray = Object.values(this._props.children || []);
    childrenArray.forEach((child, _) => {
      if (Component_A.isTComponent(child)) {
        this._children.set(child, child.compId);
      }
    });
    // Make sure the component container becomes the content of the view,
    // since the markup function returns nothing.
    if (Component_A._isHTMLElement(this._props.content)) {
      this._props.content.classList.add('t-component__container', 'flex-row', 'justify-stretch');
      this._props.content.dataset.nodeId = this._props.content.id;
    }
  }

  /**
   * @description Map and return child components when the view is active.
   * Iterates over `child` prop and returns Tcomponents or global instances.
   * @member TComponents.View_A#mapComponents
   * @method
   * @returns {TComponents.ViewChildProps|object} Mapped child components by key.
   */
  mapComponents() {
    // This flag indicates whether the view has been rendered.
    // For view components that have already been rendered, we shouldn't render them again or trigger re-initialization.
    // We shallow-cloned the HTMLElement instances during initialization via this.find, and stored those elements in this._props.
    if (this.initialized) {
      return {};
    }
    const allChildren = Array.from(new Set([...this._children.keys(), ...this._newChildren.keys()]));
    return {children: allChildren};
  }

  /**
   * @description Lifecycle method called when rendering the component.
   * Override to implement custom render behavior.
   * @member TComponents.View_A#onRender
   * @method
   * @returns {void}
   */
  onRender() {
    // The `render()` method resets this.container.innerHTML,
    // which would sever the node references cached by `this.find()`,
    // so we need to preserve those elements.
    if (this.initialized) {
      // Assign the actual value to the child.
      this.child = {children: Array.from(new Set([...this._children.keys(), ...this._newChildren.keys()]))};
    }

    const target = this._props.content;
    if (!Component_A._isHTMLElement(target)) return;

    // Attach all elements.
    this.child.children.forEach((child) => {
      if (Component_A.isTComponent(child)) {
        child.attachToElement(target);
      } else {
        if (child.parentNode) {
          child.parentNode.removeChild(child);
        }
        target.append(child);
      }
    });
  }

  /**
   * @description Appends a new child (component or HTMLElement) to the container.
   * @member TComponents.View_A#appendChild
   * @method
   * @param {TComponents.Component_A|HTMLElement} t - The child instance to append. Can be a TComponent or a DOM element.
   * @param {string} [name=''] - Optional identifier for the child. If omitted and `t` is a TComponent, `t.compId` is used.
   * @returns {void}
   * @example
   * const view = new TComponents.View_A(null, {});
   * const child = new TComponents.Button(null, {});
   * view.appendChild(child, 'test');
   */
  appendChild(t, name = '') {
    const target = this._props.content;

    if (Component_A.isTComponent(t)) {
      this._newChildren.set(t, name || t.compId);
      const allChildren = Array.from(new Set([...this._children.keys(), ...this._newChildren.keys()]));
      this.child.children = allChildren;
      t.render();
      t.attachToElement(target);
    } else if (Component_A._isHTMLElement(t)) {
      if (!target.contains(t)) {
        this._newChildren.set(t, name);
        const allChildren = Array.from(new Set([...this._children.keys(), ...this._newChildren.keys()]));
        this.child.children = allChildren;
        target.appendChild(t);
      }
    }
  }

  /**
   * @description Removes a previously added child from the internal `_newChildren` registry.
   * @member TComponents.View_A#removeChild
   * @method
   * @param {TComponents.Component_A|HTMLElement} value - The child instance to remove. Must match the reference used in `appendChild`.
   * @returns {void}
   * @example
   * const view = new TComponents.View_A(null, {});
   * const child = new TComponents.Button(null, {});
   * view.appendChild(child, 'test');
   * view.removeChild(child);
   */
  removeChild(value) {
    const destroyOrRemove = (child) => {
      if (Component_A.isTComponent(child)) {
        child.destroy();
      } else if (child.remove) {
        child.remove();
      }
    };

    if (typeof value === 'string') {
      for (const [key, val] of this._newChildren.entries()) {
        if (val === value) {
          destroyOrRemove(key);
          this._newChildren.delete(key);
        }
      }
    } else {
      destroyOrRemove(value);
      this._newChildren.delete(value);
    }

    const allChildren = [...this._children.keys(), ...this._newChildren.keys()];
    this.child.children = allChildren;
  }

  /**
   * @description Creates a default set of properties for a new view.
   * @member TComponents.View_A.createViewProps
   * @method
   * @static
   * @returns {TComponents.ViewProps} A fresh view props object.
   */
  static createViewProps() {
    return {
      name: 'Item 0',
      content: `view_${API.generateUUID()}`,
      icon: '',
      children: [],
    };
  }
}