UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

421 lines 13.4 kB
/** * @fileoverview OrdoJS Component System - Modern component architecture * @author OrdoJS Framework Team */ import { signal } from './reactivity.js'; /** * Component lifecycle phases */ export var ComponentLifecycle; (function (ComponentLifecycle) { ComponentLifecycle["CREATED"] = "created"; ComponentLifecycle["MOUNTED"] = "mounted"; ComponentLifecycle["UPDATED"] = "updated"; ComponentLifecycle["UNMOUNTED"] = "unmounted"; ComponentLifecycle["ERROR"] = "error"; })(ComponentLifecycle || (ComponentLifecycle = {})); /** * Component registry implementation */ class ComponentRegistryImpl { components = new Map(); register(definition) { if (this.components.has(definition.name)) { console.warn(`Component "${definition.name}" is already registered`); } this.components.set(definition.name, definition); } get(name) { return this.components.get(name); } unregister(name) { return this.components.delete(name); } list() { return Array.from(this.components.values()); } } /** * Component instance implementation */ class ComponentInstanceImpl { definition; id; props; state; context; lifecycle; element = null; children = []; exposed = {}; cleanups = []; eventListeners = new Map(); constructor(definition, initialProps, parentContext) { this.definition = definition; this.id = generateComponentId(); this.props = signal(initialProps); this.lifecycle = signal(ComponentLifecycle.CREATED); this.context = { parent: parentContext?.parent, injected: new Map(parentContext?.injected), provided: new Map(), registry: parentContext?.registry || new ComponentRegistryImpl() }; this.state = {}; this.initializeComponent(); } emit = (event, payload) => { const listeners = this.eventListeners.get(event); if (listeners) { for (const listener of listeners) { try { listener(payload); } catch (error) { console.error(`Error in event listener for "${event}":`, error); } } } // Bubble to parent if (this.context.parent) { this.context.parent.emit(`child:${event}`, { source: this, payload }); } }; async mount(target) { if (this.element) { throw new Error('Component is already mounted'); } try { this.runLifecycleHook('beforeMount'); // Create element this.element = await this.render(); target.appendChild(this.element); this.lifecycle.set(ComponentLifecycle.MOUNTED); this.runLifecycleHook('mounted'); } catch (error) { this.lifecycle.set(ComponentLifecycle.ERROR); this.handleError(error); } } async unmount() { if (!this.element) { return; } try { this.runLifecycleHook('beforeUnmount'); // Unmount children for (const child of this.children) { await child.unmount(); } // Remove element if (this.element.parentNode) { this.element.parentNode.removeChild(this.element); } this.element = null; // Cleanup effects for (const cleanup of this.cleanups) { cleanup(); } this.cleanups = []; this.lifecycle.set(ComponentLifecycle.UNMOUNTED); this.runLifecycleHook('unmounted'); } catch (error) { this.handleError(error); } } async update(newProps) { if (newProps) { this.props.update(current => ({ ...current, ...newProps })); } try { this.runLifecycleHook('beforeUpdate'); if (this.element) { const newElement = await this.render(); if (this.element.parentNode) { this.element.parentNode.replaceChild(newElement, this.element); } this.element = newElement; } this.lifecycle.set(ComponentLifecycle.UPDATED); this.runLifecycleHook('updated'); } catch (error) { this.handleError(error); } } async forceUpdate() { await this.update(); } async initializeComponent() { try { this.runLifecycleHook('beforeCreate'); // Validate props if (this.definition.props) { this.validateProps(this.props.value, this.definition.props); } // Setup component if (this.definition.setup) { const setupContext = this.createSetupContext(); const result = await this.definition.setup(this.props.value, setupContext); if (result) { this.state = result; } } this.runLifecycleHook('created'); } catch (error) { this.handleError(error); } } createSetupContext() { return { props: this.props, emit: this.emit, expose: (api) => { Object.assign(this.exposed, api); }, parent: this.context.parent, inject: (key, defaultValue) => { return this.context.injected.get(key) ?? defaultValue; }, provide: (key, value) => { this.context.provided.set(key, value); // Propagate to children for (const child of this.children) { child.context.injected.set(key, value); } }, slots: {} // TODO: Implement slots }; } async render() { if (this.definition.render) { const vnode = this.definition.render(this.state, this.props.value); return this.renderVNode(vnode); } // Default render const div = document.createElement('div'); div.className = `ordojs-component ordojs-${this.definition.name}`; div.textContent = `Component: ${this.definition.name}`; return div; } renderVNode(vnode) { if (typeof vnode === 'string') { const div = document.createElement('div'); div.textContent = vnode; return div; } if (typeof vnode.type === 'string') { // HTML element const element = document.createElement(vnode.type); // Set props as attributes if (vnode.props) { for (const [key, value] of Object.entries(vnode.props)) { if (key.startsWith('on') && typeof value === 'function') { // Event listener const eventName = key.slice(2).toLowerCase(); element.addEventListener(eventName, value); } else if (key === 'className' || key === 'class') { element.className = String(value); } else if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value); } else { element.setAttribute(key, String(value)); } } } // Render children if (vnode.children) { for (const child of vnode.children) { element.appendChild(this.renderVNode(child)); } } // Set innerHTML if provided if (vnode.innerHTML) { element.innerHTML = vnode.innerHTML; } return element; } else { // Component const childInstance = createComponent(vnode.type, vnode.props || {}, this.context); this.children.push(childInstance); const childElement = document.createElement('div'); childInstance.mount(childElement).then(() => { // Component mounted }); return childElement; } } validateProps(props, schema) { for (const [key, definition] of Object.entries(schema)) { const value = props[key]; // Check required if (definition.required && (value === undefined || value === null)) { throw new Error(`Required prop "${key}" is missing`); } // Check type if (value !== undefined && !this.validatePropType(value, definition.type)) { throw new Error(`Prop "${key}" has invalid type`); } // Custom validator if (value !== undefined && definition.validator && !definition.validator(value)) { throw new Error(`Prop "${key}" failed validation`); } } } validatePropType(value, type) { if (typeof type === 'function') { return type(value); } switch (type) { case 'string': return typeof value === 'string'; case 'number': return typeof value === 'number'; case 'boolean': return typeof value === 'boolean'; case 'object': return typeof value === 'object' && value !== null; case 'array': return Array.isArray(value); case 'function': return typeof value === 'function'; default: return true; } } runLifecycleHook(hook) { if (hook === 'errorCaptured') { return; // errorCaptured is handled separately } const hookFn = this.definition.lifecycle?.[hook]; if (hookFn) { try { hookFn.call(this); } catch (error) { this.handleError(error); } } } handleError(error) { console.error(`Error in component "${this.definition.name}":`, error); // Try error boundary if (this.definition.lifecycle?.errorCaptured) { const handled = this.definition.lifecycle.errorCaptured(error, this); if (handled) return; } // Bubble to parent if (this.context.parent?.definition.lifecycle?.errorCaptured) { const handled = this.context.parent.definition.lifecycle.errorCaptured(error, this); if (handled) return; } // Global error handler would be called here throw error; } } /** * Generate unique component ID */ function generateComponentId() { return `ordojs-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Create component instance */ export function createComponent(definition, props, context) { return new ComponentInstanceImpl(definition, props, context); } /** * Define a component */ export function defineComponent(definition) { return definition; } /** * Create virtual DOM node */ export function h(type, props, ...children) { return { type, props, children: children.flat().map(child => typeof child === 'string' ? { type: 'text', props: { textContent: child } } : child) }; } /** * Fragment component for multiple root nodes */ export const Fragment = defineComponent({ name: 'Fragment', render: (_, props) => { return h('div', { style: { display: 'contents' } }, ...(props.children || [])); } }); /** * Component factory */ export class ComponentFactory { registry; options; constructor(options = {}) { this.registry = options.registry || new ComponentRegistryImpl(); this.options = { devMode: false, ...options }; } /** * Register component */ register(definition) { this.registry.register(definition); } /** * Create component instance */ create(nameOrDefinition, props) { const definition = typeof nameOrDefinition === 'string' ? this.registry.get(nameOrDefinition) : nameOrDefinition; if (!definition) { throw new Error(`Component "${nameOrDefinition}" not found`); } const context = { injected: new Map(), provided: new Map(), registry: this.registry }; return createComponent(definition, props, context); } /** * Get registry */ getRegistry() { return this.registry; } } /** * Default component factory */ export const componentFactory = new ComponentFactory(); /** * Global component registration */ export function registerComponent(definition) { componentFactory.register(definition); } /** * Create component from name */ export function createComponentByName(name, props) { return componentFactory.create(name, props); } //# sourceMappingURL=component-system.js.map