UNPKG

@kalxjs/core

Version:

A modern JavaScript framework for building user interfaces with reactive state, composition API, and built-in performance optimizations

1,463 lines (1,267 loc) 67.4 kB
// packages/core/src/renderer/custom-renderer.js /** * Custom Renderer for KalxJS * * This renderer provides a template-based rendering system that bypasses * the virtual DOM implementation while still leveraging KalxJS's state * management and routing capabilities. */ /** * Creates a new custom renderer instance * @param {Object} router - KalxJS router instance * @param {Object} store - KalxJS store instance * @returns {Object} Custom renderer instance */ export function createCustomRenderer(router, store) { return new CustomRenderer(router, store); } /** * Custom Renderer class to manage the rendering lifecycle */ class CustomRenderer { /** * Creates a new CustomRenderer instance * @param {Object} router - KalxJS router instance * @param {Object} store - KalxJS store instance */ constructor(router, store) { this.router = router; this.store = store; this.routerView = null; this.currentRoute = null; this.templates = new Map(); this.componentInstances = new Map(); this.eventListeners = new Map(); } /** * Initializes the renderer * @param {string|HTMLElement} routerViewSelector - Selector or element for the router view */ init(routerViewSelector) { // Get router view element this.routerView = typeof routerViewSelector === 'string' ? document.querySelector(routerViewSelector) : routerViewSelector; if (!this.routerView) { console.error(`Router view element "${routerViewSelector}" not found`); return; } // Set up router listeners if router is available if (this.router) { this.setupRouterListeners(); // Set up navigation this.setupNavigation(); } else { console.warn('Router not provided to custom renderer, using default welcome page'); } // Initial render this.renderCurrentRoute(); console.log('Custom renderer initialized'); } /** * Sets up router event listeners */ setupRouterListeners() { if (!this.router) { console.warn('Router not provided to custom renderer'); return; } // Listen for route changes if (typeof this.router.onChange === 'function') { this.router.onChange((route) => { console.log('Route changed in custom renderer:', route); this.currentRoute = route; // Force a complete re-render of the route view if (this.routerView) { // Clear the router view first this.routerView.innerHTML = ''; // Then render the new route component this.renderCurrentRoute(); // Update navigation active states this.updateNavigation(); console.log('Router view updated for path:', route.path); } else { console.warn('Router view element not found when trying to update route'); } }); } else if (typeof this.router.beforeEach === 'function' && typeof this.router.afterEach === 'function') { // Alternative router API this.router.beforeEach((to, from, next) => { console.log('Route changing from', from, 'to', to); next(); }); this.router.afterEach((to, from) => { console.log('Route changed to:', to); this.currentRoute = to; // Force a complete re-render if (this.routerView) { this.routerView.innerHTML = ''; this.renderCurrentRoute(); this.updateNavigation(); } }); } // Try to get the initial route if (!this.currentRoute) { if (this.router.currentRoute) { this.currentRoute = this.router.currentRoute; } else if (typeof this.router.getRoute === 'function') { this.currentRoute = this.router.getRoute(); } } // Set up popstate event listener for history mode if (this.router.mode === 'history') { window.addEventListener('popstate', () => { console.log('Popstate event detected'); this.router._onRouteChange(); }); } // Set up hashchange event listener for hash mode if (this.router.mode === 'hash') { window.addEventListener('hashchange', () => { console.log('Hashchange event detected'); this.router._onRouteChange(); }); } } /** * Sets up navigation elements */ setupNavigation() { // Find all navigation links const navLinks = document.querySelectorAll('[data-route]'); navLinks.forEach(link => { const route = link.getAttribute('data-route'); // Remove existing click listener if any const existingListener = this.eventListeners.get(link); if (existingListener) { link.removeEventListener('click', existingListener); } // Add click listener const clickHandler = (e) => { e.preventDefault(); if (this.router) { this.router.push(route); } }; link.addEventListener('click', clickHandler); this.eventListeners.set(link, clickHandler); }); } /** * Updates navigation elements based on current route */ updateNavigation() { if (!this.currentRoute) return; const navLinks = document.querySelectorAll('[data-route]'); navLinks.forEach(link => { const route = link.getAttribute('data-route'); if (route === this.currentRoute.path) { link.classList.add('active'); } else { link.classList.remove('active'); } }); } /** * Renders the current route */ renderCurrentRoute() { if (!this.routerView) { console.error('Router view element not found'); return; } // Clear current content this.routerView.innerHTML = ''; // If no router or current route, render the default welcome component if (!this.currentRoute) { console.log('No current route, rendering default welcome component'); this.renderNamedComponent('welcome'); return; } const path = this.currentRoute.path; // Get the matched component from the route const matchedRoute = this.currentRoute.matched && this.currentRoute.matched.length > 0 ? this.currentRoute.matched[0] : null; if (!matchedRoute) { console.warn(`No matched route found for path: ${path}`); this.renderNotFound(); return; } const component = matchedRoute.component; console.log(`Rendering route: ${path}, component:`, component); // Render the component based on its type if (typeof component === 'string') { // Component is a string identifier this.renderNamedComponent(component); } else if (typeof component === 'function') { // Component is a function (functional component) try { this.renderFunctionComponent(component); } catch (error) { console.error(`Error rendering function component for route ${path}:`, error); this.renderError(error); } } else if (component && typeof component === 'object') { // Component is an object (options API component) try { this.renderObjectComponent(component); } catch (error) { console.error(`Error rendering object component for route ${path}:`, error); this.renderError(error); } } else { console.warn(`Unknown component type for route ${path}:`, component); this.renderNotFound(); } console.log(`Route ${path} rendered successfully`); } /** * Renders an error message * @param {Error} error - The error that occurred */ renderError(error) { const errorContainer = document.createElement('div'); errorContainer.className = 'kal-router-error'; errorContainer.style.color = 'red'; errorContainer.style.padding = '20px'; errorContainer.style.border = '1px solid red'; errorContainer.style.margin = '10px 0'; const errorTitle = document.createElement('h3'); errorTitle.textContent = 'Rendering Error'; const errorMessage = document.createElement('p'); errorMessage.textContent = error.message; const errorStack = document.createElement('pre'); errorStack.textContent = error.stack; errorStack.style.fontSize = '12px'; errorStack.style.overflow = 'auto'; errorStack.style.maxHeight = '200px'; errorStack.style.backgroundColor = '#f5f5f5'; errorStack.style.padding = '10px'; errorContainer.appendChild(errorTitle); errorContainer.appendChild(errorMessage); errorContainer.appendChild(errorStack); this.routerView.appendChild(errorContainer); } /** * Renders a component by name * @param {string} name - Component name */ renderNamedComponent(name) { const templateId = `${name}-template`; const template = document.getElementById(templateId); if (!template) { console.warn(`Template not found for component: ${name}`); // Try to load the template from a file this.loadTemplateFromFile(name) .then(content => { // Set up component this.setupComponent(name, content); // Append to router view this.routerView.appendChild(content); }) .catch(error => { console.error(`Failed to load template for ${name}:`, error); // If this is the welcome component, use a default template if (name === 'welcome') { this.renderDefaultWelcome(); } else if (name === 'counter') { this.renderDefaultCounter(); } else { this.renderNotFound(); } }); return; } // Clone the template content const content = template.content.cloneNode(true); // Set up component this.setupComponent(name, content); // Append to router view this.routerView.appendChild(content); } /** * Loads a template from a file * @param {string} name - Template name * @returns {Promise<DocumentFragment>} Template content */ async loadTemplateFromFile(name) { try { // First try to load the .klx component try { const klxResponse = await fetch(`/src/components/${name}.klx`); if (klxResponse.ok) { const klxSource = await klxResponse.text(); return this.processKlxComponent(klxSource, name); } } catch (klxError) { console.warn(`Could not load .klx component: ${klxError.message}`); } // If .klx fails, try to load from views directory try { const viewResponse = await fetch(`/src/views/${name}.klx`); if (viewResponse.ok) { const viewSource = await viewResponse.text(); return this.processKlxComponent(viewSource, name); } } catch (viewError) { console.warn(`Could not load view component: ${viewError.message}`); } // If .klx fails, try to load the HTML template const htmlResponse = await fetch(`/src/templates/${name}.html`); if (!htmlResponse.ok) { throw new Error(`Failed to load template: ${htmlResponse.status} ${htmlResponse.statusText}`); } const html = await htmlResponse.text(); // Create a template element const template = document.createElement('template'); template.innerHTML = html.trim(); // Store the template for future use this.templates.set(name, template); // Return a clone of the content return template.content.cloneNode(true); } catch (error) { console.error(`Error loading template ${name}:`, error); throw error; } } /** * Parses a .klx file into its sections * @param {string} source - KLX component source * @param {string} name - Component name (optional) * @returns {Object} Parsed sections */ parseKlx(source, name = '') { const result = { template: null, script: null, style: null, vueStyleComponent: false }; // Find template section const templateMatch = /<template>([\s\S]*?)<\/template>/i.exec(source); if (templateMatch) { // Process template interpolation let template = templateMatch[1].trim(); // Process v-for directives (simplified implementation) template = this.processVForDirectives(template); // Process template interpolation {{ expression }} template = template.replace(/{{(.*?)}}/g, (match, expression) => { return `<span data-bind="${expression.trim()}"></span>`; }); result.template = template; } else { console.warn(`No <template> section found in .klx component${name ? ` ${name}` : ''}`); } // Find script section const scriptMatch = /<script>([\s\S]*?)<\/script>/i.exec(source); if (scriptMatch) { const scriptContent = scriptMatch[1].trim(); // Options API if (scriptContent.includes('data()') || scriptContent.includes('methods:') || scriptContent.includes('computed:')) { result.vueStyleComponent = true; result.script = `{ ${scriptContent .replace(/export\s+default\s+/, '') .replace(/defineComponent\(/, '') .replace(/\)\s*;?\s*$/, '') } }`; } else { // Regular script content result.script = scriptContent .replace(/export\s+default\s+/, '') .replace(/defineComponent\(/, '') .replace(/\)\s*;?\s*$/, ''); } } // Find style section const styleMatch = /<style(\s+scoped)?>([\s\S]*?)<\/style>/i.exec(source); if (styleMatch) { const scoped = !!styleMatch[1]; let styleContent = styleMatch[2].trim(); // Handle scoped styles if (scoped && name) { const scopeId = `data-klx-${name}`; styleContent = styleContent.replace(/([^{]*){/g, (match, selector) => { return `${selector}[${scopeId}] {`; }); } result.style = styleContent; } return result; } /** * Special processor for the welcome component * @param {string} source - KLX component source * @returns {DocumentFragment} Processed component content */ processWelcomeComponent(source) { console.log('Using special welcome component processor'); try { // Extract template const templateMatch = /<template>([\s\S]*?)<\/template>/i.exec(source); let templateContent = ''; if (templateMatch) { templateContent = templateMatch[1].trim(); } else { templateContent = ` <div class="welcome-component"> <h2>Welcome to KalxJS</h2> <p>This is a default welcome component.</p> </div> `; } // Extract style const styleMatch = /<style>([\s\S]*?)<\/style>/i.exec(source); if (styleMatch) { const styleContent = styleMatch[1].trim(); const styleId = `style-welcome-${Date.now()}`; const styleElement = document.createElement('style'); styleElement.id = styleId; styleElement.textContent = styleContent; document.head.appendChild(styleElement); } // Extract script content const scriptMatch = /<script>([\s\S]*?)<\/script>/i.exec(source); let welcomeComponent; if (scriptMatch) { const scriptContent = scriptMatch[1].trim(); // Options API if (scriptContent.includes('data()') && scriptContent.includes('methods')) { console.log('Detected Options API in welcome component'); // Create a component that adapts the Options API to our setup function welcomeComponent = { name: 'WelcomeComponent', setup() { // Create reactive state const count = { value: 0 }; const doubleCount = { value: 0 }; const isEven = { value: 'Yes' }; // Methods function increment() { count.value++; updateComputed(); updateUI(); } function decrement() { count.value--; updateComputed(); updateUI(); } function reset() { count.value = 0; updateComputed(); updateUI(); } // Update computed values function updateComputed() { doubleCount.value = count.value * 2; isEven.value = count.value % 2 === 0 ? 'Yes' : 'No'; } // Update UI elements function updateUI() { const counterValue = document.getElementById('counter-value'); const doubleCountEl = document.getElementById('double-count'); const isEvenEl = document.getElementById('is-even'); if (counterValue) counterValue.textContent = count.value; if (doubleCountEl) doubleCountEl.textContent = doubleCount.value; if (isEvenEl) isEvenEl.textContent = isEven.value; // Add animation class if (counterValue) { counterValue.classList.add('updated'); setTimeout(() => { counterValue.classList.remove('updated'); }, 300); } } // Setup event listeners when mounted setTimeout(() => { const incrementBtn = document.getElementById('increment-button'); const decrementBtn = document.getElementById('decrement-button'); const resetBtn = document.getElementById('reset-button'); if (incrementBtn) incrementBtn.addEventListener('click', increment); if (decrementBtn) decrementBtn.addEventListener('click', decrement); if (resetBtn) resetBtn.addEventListener('click', reset); // Initial UI update updateUI(); // Set up feature grid if needed const featureGrid = document.querySelector('.feature-grid'); if (featureGrid) { // We'll keep the existing feature grid from the template console.log('Feature grid found in template'); } }, 0); return { count: count.value, welcomeMessage: "Congratulations! You've successfully created a new KalxJS project with .klx components!", features: [ { icon: '📝', title: 'Template-Based Rendering', description: 'Use HTML templates directly with no virtual DOM overhead' }, { icon: '⚡', title: 'Reactive State', description: 'Powerful state management with automatic DOM updates' }, { icon: '🧩', title: 'Component System', description: 'Create reusable components with clean APIs' }, { icon: '🔄', title: 'Routing', description: 'Seamless navigation between different views' } ], doubleCount: doubleCount.value, isEven: isEven.value }; } }; } else { // Try to use the script as is try { // Look for export default const exportMatch = /export\s+default\s+(\{[\s\S]*\})/i.exec(scriptContent); if (exportMatch) { const componentDef = exportMatch[1]; welcomeComponent = new Function('return ' + componentDef)(); welcomeComponent.name = welcomeComponent.name || 'WelcomeComponent'; } else { // Fallback to default component welcomeComponent = { name: 'WelcomeComponent', setup() { return { count: 0, welcomeMessage: "Welcome to KalxJS!", features: [ { icon: '📝', title: 'Template-Based Rendering', description: 'Use HTML templates directly' }, { icon: '⚡', title: 'Reactive State', description: 'Powerful state management' } ] }; } }; } } catch (scriptError) { console.warn('Error evaluating welcome component script:', scriptError); // Fallback to default component welcomeComponent = { name: 'WelcomeComponent', setup() { return { count: 0, welcomeMessage: "Welcome to KalxJS!", features: [ { icon: '📝', title: 'Template-Based Rendering', description: 'Use HTML templates directly' }, { icon: '⚡', title: 'Reactive State', description: 'Powerful state management' } ] }; } }; } } } else { // No script section, use default component welcomeComponent = { name: 'WelcomeComponent', setup() { return { count: 0, welcomeMessage: "Welcome to KalxJS!", features: [ { icon: '📝', title: 'Template-Based Rendering', description: 'Use HTML templates directly' }, { icon: '⚡', title: 'Reactive State', description: 'Powerful state management' } ] }; } }; } // Store the component this.componentInstances.set('welcome', welcomeComponent); // Create a template element const template = document.createElement('template'); template.innerHTML = templateContent; // Store the template this.templates.set('welcome', template); // Return the template content return template.content.cloneNode(true); } catch (error) { console.error('Error processing welcome component:', error); // Create a fallback template const errorTemplate = document.createElement('template'); errorTemplate.innerHTML = ` <div class="welcome-error" style="color: red; padding: 20px; border: 1px solid red;"> <h3>Error Processing Welcome Component</h3> <p>${error.message}</p> </div> `; return errorTemplate.content.cloneNode(true); } } /** * Parses a .klx file into its sections * @param {string} source - KLX component source * @param {string} name - Component name (optional) * @returns {Object} Parsed sections */ parseKlx(source, name = '') { const result = { template: null, script: null, style: null }; // Find template section const templateMatch = /<template>([\s\S]*?)<\/template>/i.exec(source); if (templateMatch) { // Process template interpolation let template = templateMatch[1].trim(); // Process v-for directives (simplified implementation) template = this.processVForDirectives(template); // Process template interpolation {{ expression }} template = template.replace(/{{(.*?)}}/g, (match, expression) => { return `<span data-bind="${expression.trim()}"></span>`; }); result.template = template; } else { console.warn(`No <template> section found in .klx component${name ? ` ${name}` : ''}`); } // Find script section const scriptMatch = /<script>([\s\S]*?)<\/script>/i.exec(source); if (scriptMatch) { // Extract the component definition const scriptContent = scriptMatch[1].trim(); // Store the raw script content for later processing // This allows us to handle the script more flexibly in processKlxComponent result.rawScript = scriptContent; // Options API if (scriptContent.includes('data()') && (scriptContent.includes('methods') || scriptContent.includes('computed'))) { console.log(`Detected Options API in ${name} component`); // simplified setup function result.script = `{ name: '${name || 'VueStyleComponent'}', setup() { // This is a placeholder that will be replaced with actual component processing return {}; } }`; // Store the original component for later processing result.vueStyleComponent = true; } else { // Try to extract the component definition try { // Look for defineComponent call if (scriptContent.includes('defineComponent')) { const defineComponentMatch = /defineComponent\s*\(\s*(\{[\s\S]*?\})\s*\)/i.exec(scriptContent); if (defineComponentMatch) { result.script = defineComponentMatch[1]; } else { // If we can't extract the options directly, create a simple object result.script = `{ name: '${name || 'DefaultComponent'}', setup() { return {}; } }`; } } // Special handling for welcome component else if (name === 'welcome' && scriptContent.includes('export default defineComponent')) { console.log('Processing welcome component with special handling'); // For welcome.klx, create a hardcoded component definition that matches the expected structure result.script = `{ name: 'WelcomeComponent', setup() { return { count: 0, welcomeMessage: 'Welcome to KalxJS!', features: [ { icon: '📝', title: 'Template-Based Rendering', description: 'Use HTML templates directly' }, { icon: '⚡', title: 'Reactive State', description: 'Powerful state management' }, { icon: '🧩', title: 'Component System', description: 'Create reusable components' }, { icon: '🔄', title: 'Routing', description: 'Seamless navigation' } ], doubleCount: 0, isEven: 'Yes' }; } }`; } // Look for export default with defineComponent (for other components) else if (scriptContent.includes('export default defineComponent')) { const exportDefineMatch = /export\s+default\s+defineComponent\s*\(\s*(\{[\s\S]*?\})\s*\)/i.exec(scriptContent); if (exportDefineMatch) { result.script = exportDefineMatch[1]; } else { result.script = `{ name: '${name || 'DefaultComponent'}', setup() { return {}; } }`; } } // Look for export default with object literal else if (scriptContent.includes('export default {')) { const exportObjectMatch = /export\s+default\s+(\{[\s\S]*?\})/i.exec(scriptContent); if (exportObjectMatch) { result.script = exportObjectMatch[1]; } else { result.script = `{ name: '${name || 'DefaultComponent'}', setup() { return {}; } }`; } } // Look for export default with variable else if (scriptContent.includes('export default')) { const exportVarMatch = /export\s+default\s+(\w+)/i.exec(scriptContent); if (exportVarMatch) { const varName = exportVarMatch[1]; const varDefMatch = new RegExp(`const\\s+${varName}\\s*=\\s*(\\{[\\s\\S]*?\\})`, 'i').exec(scriptContent); if (varDefMatch) { result.script = varDefMatch[1]; } else { result.script = `{ name: '${name || 'DefaultComponent'}', setup() { return {}; } }`; } } else { result.script = `{ name: '${name || 'DefaultComponent'}', setup() { return {}; } }`; } } // Other component definition else { result.script = `{ name: '${name || 'DefaultComponent'}', setup() { return {}; } }`; } } catch (error) { console.warn(`Error parsing script for ${name}:`, error.message); result.script = `{ name: '${name || 'ErrorComponent'}', setup() { return { error: '${error.message.replace(/'/g, "\\'")}' }; } }`; } } } else { console.warn(`No <script> section found in .klx component${name ? ` ${name}` : ''}`); // Create a default script if none is found result.script = `{ name: '${name || 'DefaultComponent'}', setup() { return {}; } }`; } // Find style section const styleMatch = /<style(\s+scoped)?>([\s\S]*?)<\/style>/i.exec(source); if (styleMatch) { const scoped = !!styleMatch[1]; let styleContent = styleMatch[2].trim(); // Handle scoped styles if (scoped && name) { const scopeId = `data-klx-${name}`; styleContent = styleContent.replace(/([^{]*){/g, (match, selector) => { return `${selector}[${scopeId}] {`; }); } result.style = styleContent; } return result; } /** * Processes v-for directives in a template * @param {string} template - Template HTML * @returns {string} Processed template */ processVForDirectives(template) { // This is a simplified implementation // In a real implementation, we would use a proper HTML parser // Match elements with v-for directive const vForRegex = /<([a-z0-9-]+)([^>]*?)v-for="([^"]*?)"([^>]*?)>([\s\S]*?)<\/\1>/gi; return template.replace(vForRegex, (match, tag, attrs1, vForExpr, attrs2, content) => { // Extract the v-for expression parts: "item in items" const [itemName, arrayName] = vForExpr.split(' in ').map(s => s.trim()); // Replace with a data-for attribute for runtime processing return `<div data-for="${arrayName}" data-for-item="${itemName}" data-for-template="${encodeURIComponent( `<${tag}${attrs1}${attrs2}>${content}</${tag}>` )}"></div>`; }); } /** * Renders a function component * @param {Function} component - Component function */ renderFunctionComponent(component) { try { // Create a container for the component const container = document.createElement('div'); container.className = 'component-container'; // Create component instance with route params as props const props = this.currentRoute ? { params: this.currentRoute.params || {}, query: this.currentRoute.query || {}, path: this.currentRoute.path || '/' } : {}; console.log('Rendering function component with props:', props); // Call the component function with props const instance = component(props); // Store the instance const id = `func-${Date.now()}`; this.componentInstances.set(id, instance); // Handle different return types from the component function if (instance && typeof instance === 'object') { if (instance.$mount) { // If it's a KalxJS component instance with $mount method console.log('Mounting component instance with $mount'); instance.$mount(container); } else if (instance.tag) { // If it's a virtual DOM node console.log('Rendering virtual DOM node:', instance); // Import the createElement function if not already available if (typeof createElement !== 'function') { // Use a simple implementation if the actual one is not available const createElement = (vnode) => { if (typeof vnode === 'string') { return document.createTextNode(vnode); } if (!vnode || !vnode.tag) { return document.createComment('empty or invalid node'); } const el = document.createElement(vnode.tag); // Set attributes/props for (const [key, value] of Object.entries(vnode.props || {})) { if (key.startsWith('on') && typeof value === 'function') { // Event handler const eventName = key.slice(2).toLowerCase(); el.addEventListener(eventName, value); } else { // Regular attribute el.setAttribute(key, value); } } // Create children (vnode.children || []).forEach(child => { if (child != null) { el.appendChild(createElement(child)); } }); return el; }; // Create DOM element from virtual DOM const domElement = createElement(instance); container.appendChild(domElement); } else { // Use the imported createElement function const domElement = createElement(instance); container.appendChild(domElement); } } else if (instance.render && typeof instance.render === 'function') { // If it has a render method, call it and handle the result console.log('Component has render method, calling it'); const renderResult = instance.render(); if (typeof renderResult === 'string') { container.innerHTML = renderResult; } else if (renderResult && renderResult.tag) { // It's a virtual DOM node, render it const domElement = createElement(renderResult); container.appendChild(domElement); } else { console.warn('Render method returned invalid result:', renderResult); container.innerHTML = 'Invalid render result'; } } else { // Unknown object type console.warn('Unknown component instance type:', instance); container.innerHTML = JSON.stringify(instance, null, 2); } } else if (typeof instance === 'string') { // If it's a string, render it directly container.innerHTML = instance; } else if (instance === null || instance === undefined) { // Handle null/undefined return console.warn('Component function returned null or undefined'); container.innerHTML = '<div class="empty-component">Empty component</div>'; } else { // Handle other return types console.warn('Unexpected component return type:', typeof instance); container.innerHTML = String(instance); } // Append to router view this.routerView.appendChild(container); console.log('Function component rendered successfully'); } catch (error) { console.error('Error rendering function component:', error); this.renderError(error); } } /** * Renders an object component * @param {Object} component - Component object */ renderObjectComponent(component) { try { // Create a container for the component const container = document.createElement('div'); container.className = 'component-container'; // Store the component const id = `obj-${Date.now()}`; this.componentInstances.set(id, component); // Get route params to pass as props const props = this.currentRoute ? { params: this.currentRoute.params || {}, query: this.currentRoute.query || {}, path: this.currentRoute.path || '/' } : {}; console.log('Rendering object component with props:', props); // Check if it's a KalxJS component definition if (component.setup && typeof component.setup === 'function') { console.log('Component has setup method, creating instance'); // Create a component instance using the component definition const instance = { ...component, props: props }; // Call setup with props const setupResult = component.setup(props); // Handle setup result if (typeof setupResult === 'function') { // Setup returned a render function console.log('Setup returned a render function'); const renderResult = setupResult(); // Import createElement if needed if (typeof createElement !== 'function') { // Simple implementation const createElement = (vnode) => { if (typeof vnode === 'string') { return document.createTextNode(vnode); } if (!vnode || !vnode.tag) { return document.createComment('empty or invalid node'); } const el = document.createElement(vnode.tag); // Set attributes/props for (const [key, value] of Object.entries(vnode.props || {})) { if (key.startsWith('on') && typeof value === 'function') { // Event handler const eventName = key.slice(2).toLowerCase(); el.addEventListener(eventName, value); } else { // Regular attribute el.setAttribute(key, value); } } // Create children (vnode.children || []).forEach(child => { if (child != null) { el.appendChild(createElement(child)); } }); return el; }; if (renderResult && renderResult.tag) { const domElement = createElement(renderResult); container.appendChild(domElement); } else { console.warn('Render function returned invalid result:', renderResult); container.innerHTML = 'Invalid render result'; } } else { // Use imported createElement const domElement = createElement(renderResult); container.appendChild(domElement); } } else if (setupResult && typeof setupResult === 'object') { // Setup returned an object with state/methods console.log('Setup returned an object with state/methods'); // Merge setup result with instance Object.assign(instance, setupResult); // If instance has a render method, use it if (instance.render && typeof instance.render === 'function') { const renderResult = instance.render(); if (typeof renderResult === 'string') { container.innerHTML = renderResult; } else if (renderResult && renderResult.tag) { // It's a virtual DOM node const domElement = createElement(renderResult); container.appendChild(domElement); } else { console.warn('Render method returned invalid result:', renderResult); container.innerHTML = 'Invalid render result'; } } else if (instance.template) { container.innerHTML = instance.template; } else { console.warn('Component instance has no render method or template'); container.innerHTML = '<div>Component Error: No render method or template</div>'; } } else { console.warn('Setup returned unexpected result:', setupResult); container.innerHTML = 'Invalid setup result'; } } else if (component.render && typeof component.render === 'function') { // Component has a render method console.log('Component has render method'); const result = component.render(props); if (typeof result === 'string') { container.innerHTML = result; } else if (result && result.tag) { // It's a virtual DOM node const domElement = createElement(result); container.appendChild(domElement); } else if (result instanceof Node) { // It's a DOM node container.appendChild(result); } else { console.warn('Render method returned unexpected result:', result); container.innerHTML = 'Invalid render result'; } } else if (component.template) { // Component has a template console.log('Component has template'); container.innerHTML = component.template; } else if (component.name) { // Component only has a name, try to render by name console.log('Component only has name, trying to render by name:', component.name); container.innerHTML = `<div class="${component.name}-component">${component.name} Component</div>`; } else { console.warn('Component has no render method, template, or name'); container.innerHTML = '<div>Component Error: No render method or template</div>'; } // Append to router view this.routerView.appendChild(container); console.log('Object component rendered successfully'); } catch (error) { console.error('Error rendering object component:', error); this.renderError(error); } } /** * Sets up a component * @param {string} name - Component name * @param {DocumentFragment} content - Component content */ setupComponent(name, content) { // Check if we have a component instance for this name const componentInstance = this.componentInstances.get(name); if (componentInstance) { // Set up the component using its instance this.setupComponentInstance(componentInstance, content); return; } // Otherwise, set up based on component type switch (name) { case 'home': this.setupHomeComponent(content); break; case 'welcome': this.setupWelcomeComponent(content); break; case 'counter': this.setupCounterComponent(content); break; case 'todos': this.setupTodosComponent(content); break; case 'about': this.setupAboutComponent(content); break; default: console.log(`No specific setup for component: ${name}`); } } /** * Sets up a component using its instance * @param {Object} component - Component instance * @param {DocumentFragment} content - Component content */ setupComponentInstance(component, content) { // Initialize component data let data = {}; if (component.data && typeof component.data === 'function') { data = component.data(); // Store the data on the component component._data = data; } // Initialize computed properties let computed = {}; if (component.computed) { for (const [key, fn] of Object.entries(component.computed)) { Object.defineProperty(component, key, { get: typeof fn === 'function' ? fn.bind(component) : () => fn, configurable: true }); // Initialize computed value computed[key] = component[key]; } } // Process v-for directives const forElements = content.querySelectorAll('[data-for]'); forElements.forEach(element => { const arrayName = element.getAttribute('data-for'); const itemName = element.getAttribute('data-for-item'); const templateStr = decodeURIComponent(element.getAttribute('data-for-template')); // Get the array from data const array = this.getNestedValue(data, arrayName); if (Array.isArray(array)) { // Create a document fragment to hold the generated elements const fragment = document.createDocumentFragment(); // Generate elements for each item in the array array.forEach((item, index) => { // Create a template element to parse the template string const template = document.createElement('template'); template.innerHTML = templateStr; // Clone the template content const itemContent = template.content.cloneNode(true); // Replace item interpolation in the content this.processItemInterpolation(itemContent, itemName, item, index); // Add to the fragment fragment.appendChild(itemContent); }); // Replace the v-for element with the generated elements element.parentNode.replaceChild(fragment, element); } }); // Set up data bindings const bindingElements = content.querySelectorAll('[data-bind]'); bindingElements.forEach(element => { const binding = element.getAttribute('data-bind'); // Try to get the value from data, computed, or component methods let value; if (binding.includes('.')) { // Handle nested properties value = this.getNestedValue(data, binding); } else if (data[binding] !== undefined) { value = data[binding]; } else if (component[binding] !== undefined) { value = component[binding]; } if (value !== undefined) { element.textContent = value; } }); // Set up event handlers if (component.methods) { for (const [methodName, method] of Object.entries(component.methods)) { // Find elements with this method as an event handler const eventElements = content.querySelectorAll(`[data-event-${methodName}]`); eventElements.forEach(element => { const eventType = element.getAttribute(`data-event-${methodName}`); const boundMethod = method.bind(component); element.addEventListener(eventType, boundMethod); this.eventListeners.set(element, boundMethod); }); } } // Call mounted hook if available if (component.mounted && typeof component.mounted === 'function') { // Delay the mounted call to ensure the DOM is ready setTimeout(() => { component.mounted.call(component); }, 0); } } /** * Gets a nested value from an object using a dot-notation path * @param {Object} obj - Object to get value from * @param {string} path - Dot-notation path * @returns {*} Value at the path */ getNestedValue(obj, path) { return path.split('.').reduce((value, key) => { return value && value[key] !== undefined ? value[key] : undefined; }, obj); } /** * Processes item interpolation in v-for templates * @param {DocumentFragment} content - Content to process * @param {string} itemName - Name of the item variable * @param {*} item - Current item * @param {number} index - Current index */ processItemInterpolation(content, itemName, item, index) { // Process text nodes const walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT, null, false ); const textNodes = []; let currentNode; while ((currentNode = walker.nextNode())) { textNodes.push(currentNode); } textNodes.forEach(node => { // Replace {{ item.property }} with the actual value node.textContent = node.textContent.replace( new RegExp(`{{\\s*${itemName}\\.(.*?)\\s*}}`, 'g'), (match, property) => { return item[property] !== undefined ? item[property] : ''; } ); // Replace {{ index }} with the current index node.textContent = node.textContent.replace( /{{\\s*index\\s*}}/g, index ); }); // Process attributes const elements = content.querySelectorAll('*'); elements.forEach(element => { Array.from(element.attributes).forEach(attr => { if (attr.value.includes(`{{${itemName}.`)) { // Replace {{ item.property }} with the actual value attr.value = attr.value.replace( new RegExp(`{{\\s*${itemName}\\.(.*?)\\s*}}`, 'g'), (match, property) => { return item[property] !== undefined ? item[property] : ''; } ); } }); }); } /** * Sets up the home component * @param {DocumentFragment} content - Component content */ setupHomeComponent(content) { // Example: Set up welcome message const welcomeEl = content.querySelector('.welcome-message'); if (welcomeEl && this.store && this.store.state && this.store.state.user) { welcomeEl.textContent = `Welcome, ${this.store.state.user.name || 'Guest'}!`; } } /** * Sets up the welcome component * @param {DocumentFragment} content - Component content */ setupWelcomeComponent(content) { if (!this.store) { console.warn('Store not available for welcome component'); return; } // Set up user name if available const userNameEl = content.querySelector('.user-name'); if (userNameEl && this.store.state.user) { userNameEl.textContent = this.store.state.user.name || 'Developer'; } // Set up counter in the welcome page const counterValue = content.querySelector('#counter-value'); const doubleCount = content.querySelector('#double-count'); if (counterValue) { counterValue.textContent = this.store.state.count || 0; } if (doubleCount) { // Handle both function and value getters const doubled = typeof this.store.getters?.doubleCount === 'function' ? this.store.getters.doubleCount() : this.store.getters?.doubleCount; doubleCount.textContent = doubled !== undefined ? doubled : ((this.store.state.count || 0) * 2); } // Set up event listeners const incrementBtn = content.querySelector('#increment-button'); if (incrementBtn) { const listener = () => { this.store.commit('increment'); this.updateWelcomeCounter(); }; incrementBtn.addEventListener('click', listener); this.eventListeners.set(incrementBtn, listener); } const dec