@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
421 lines • 13.4 kB
JavaScript
/**
* @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