@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
294 lines • 11 kB
JavaScript
/**
* @fileoverview OrdoJS Runtime - Minimal runtime components
*/
/**
* Minimal runtime for hydration and reactivity
*/
export class OrdoJSRuntime {
static instance;
hydratedComponents = new Map();
componentConstructors = new Map();
static getInstance() {
if (!OrdoJSRuntime.instance) {
OrdoJSRuntime.instance = new OrdoJSRuntime();
}
return OrdoJSRuntime.instance;
}
/**
* Initialize the runtime
*/
init() {
// Auto-hydrate components on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.autoHydrate());
}
else {
this.autoHydrate();
}
}
/**
* Register a component constructor for hydration
*/
registerComponent(name, constructor) {
this.componentConstructors.set(name, constructor);
}
/**
* Auto-hydrate all components found in the DOM
*/
autoHydrate() {
// Find all elements with hydration data
const hydrationScript = document.getElementById('ordojs-hydration-data');
if (hydrationScript && hydrationScript.textContent) {
try {
const hydrationData = JSON.parse(hydrationScript.textContent);
this.hydrateFromData(hydrationData);
}
catch (error) {
console.error('Failed to parse hydration data:', error);
}
}
// Find all components marked for hydration
const componentElements = document.querySelectorAll('[data-ordojs-component]');
componentElements.forEach(element => {
const componentName = element.getAttribute('data-ordojs-component');
const componentId = element.getAttribute('data-component-id');
if (componentName && componentId && !this.hydratedComponents.has(componentId)) {
this.hydrateComponent(element, componentName, componentId);
}
});
}
/**
* Hydrate a component from hydration data
*/
hydrateFromData(hydrationData) {
const element = document.querySelector(`[data-component-id="${hydrationData.componentId}"]`);
if (element) {
this.hydrateComponent(element, hydrationData.componentName, hydrationData.componentId, hydrationData);
}
}
/**
* Hydrate a specific component
*/
hydrateComponent(element, componentName, componentId, hydrationData) {
const constructor = this.componentConstructors.get(componentName);
if (!constructor) {
console.warn(`Component constructor for "${componentName}" not found`);
return null;
}
// Extract props from element or hydration data
const props = this.extractProps(element, hydrationData);
// Create component instance
const componentInstance = constructor(props);
componentInstance.id = componentId;
componentInstance.name = componentName;
componentInstance.element = element;
// Initialize reactive state from hydration data
if (hydrationData?.initialState) {
this.initializeReactiveState(componentInstance, hydrationData.initialState);
}
// Attach event listeners
this.attachEventListeners(element, componentInstance);
// Store the hydrated component
this.hydratedComponents.set(componentId, componentInstance);
// Mark as hydrated
element.setAttribute('data-ordojs-hydrated', 'true');
return componentInstance;
}
/**
* Extract props from element attributes or hydration data
*/
extractProps(element, hydrationData) {
let props = {};
// Extract from hydration data first
if (hydrationData?.props) {
props = { ...hydrationData.props };
}
// Extract from data-props attribute
const propsAttr = element.getAttribute('data-props');
if (propsAttr) {
try {
const parsedProps = JSON.parse(propsAttr);
props = { ...props, ...parsedProps };
}
catch (error) {
console.warn('Failed to parse props from data-props attribute:', error);
}
}
return props;
}
/**
* Initialize reactive state from server data
*/
initializeReactiveState(component, initialState) {
// Initialize state properties with reactive setters
for (const [key, value] of Object.entries(initialState)) {
if (component.state && typeof component.state === 'object') {
// Set initial value
component.state[key] = value;
// Make it reactive if the component has an update method
if (component.update && typeof component.update === 'function') {
let currentValue = value;
Object.defineProperty(component.state, key, {
get: () => currentValue,
set: (newValue) => {
if (currentValue !== newValue) {
currentValue = newValue;
component.update();
}
},
enumerable: true,
configurable: true
});
}
}
}
}
/**
* Attach event listeners to hydrated components
*/
attachEventListeners(element, component) {
// Find all elements with event handlers
const eventElements = element.querySelectorAll('[data-event]');
if (eventElements) {
eventElements.forEach(eventElement => {
const eventName = eventElement.getAttribute('data-event');
if (!eventName)
return;
// Create event handler
const handler = (event) => {
// Look for handler function in component
const handlerName = `handle${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`;
if (component && typeof component[handlerName] === 'function') {
component[handlerName](event);
}
};
// Attach event listener
eventElement.addEventListener(eventName, handler);
// Store for cleanup
if (!component.eventListeners) {
component.eventListeners = new Map();
}
component.eventListeners.set(`${eventElement.id || 'element'}-${eventName}`, handler);
});
}
// Handle interpolation updates
this.setupInterpolationUpdates(element, component);
}
/**
* Set up updates for interpolated content
*/
setupInterpolationUpdates(element, component) {
const interpolationElements = element.querySelectorAll('[data-interpolation]');
interpolationElements.forEach(interpElement => {
const varName = interpElement.getAttribute('data-interpolation');
if (!varName || !component.state)
return;
// Update interpolation when state changes
const originalUpdate = component.update;
component.update = () => {
// Update interpolation content
if (component.state[varName] !== undefined) {
interpElement.textContent = String(component.state[varName]);
}
// Call original update if it exists
if (originalUpdate && typeof originalUpdate === 'function') {
originalUpdate.call(component);
}
};
});
}
/**
* Get a hydrated component by ID
*/
getComponent(componentId) {
return this.hydratedComponents.get(componentId);
}
/**
* Get all hydrated components
*/
getAllComponents() {
return Array.from(this.hydratedComponents.values());
}
/**
* Unmount a component
*/
unmountComponent(componentId) {
const component = this.hydratedComponents.get(componentId);
if (!component)
return;
// Remove event listeners
if (component.eventListeners) {
component.eventListeners.forEach((listener, key) => {
const [elementId, eventName] = key.split('-');
const element = elementId === 'element'
? component.element
: document.getElementById(elementId);
if (element) {
element.removeEventListener(eventName, listener);
}
});
component.eventListeners.clear();
}
// Call component unmount if available
if (component.unmount && typeof component.unmount === 'function') {
component.unmount();
}
// Remove from registry
this.hydratedComponents.delete(componentId);
// Remove hydration marker
component.element.removeAttribute('data-ordojs-hydrated');
}
/**
* Unmount all components
*/
unmountAll() {
const componentIds = Array.from(this.hydratedComponents.keys());
componentIds.forEach(id => this.unmountComponent(id));
}
}
/**
* Hydrator class for mounting to pre-rendered HTML
*/
export class OrdoJSHydrator {
runtime;
constructor() {
this.runtime = OrdoJSRuntime.getInstance();
}
/**
* Hydrate a specific component
*/
hydrateComponent(element, componentData) {
const componentName = element.getAttribute('data-ordojs-component');
const componentId = element.getAttribute('data-component-id');
if (!componentName || !componentId) {
console.warn('Element missing required hydration attributes');
return null;
}
return this.runtime.hydrateComponent(element, componentName, componentId, {
componentName,
componentId,
props: componentData.props,
initialState: componentData.state || {},
version: '1.0'
});
}
/**
* Attach event listeners to hydrated components
*/
attachEventListeners(element, handlers) {
Object.entries(handlers).forEach(([eventName, handler]) => {
const eventElements = element.querySelectorAll(`[data-event="${eventName}"]`);
eventElements.forEach(eventElement => {
eventElement.addEventListener(eventName, handler);
});
});
}
/**
* Initialize reactive state
*/
initializeReactiveState(component) {
// This is handled by the runtime's initializeReactiveState method
// This method is kept for API compatibility
}
}
//# sourceMappingURL=index.js.map