-
Notifications
You must be signed in to change notification settings - Fork 8
Component
Juris implements a sophisticated component system with multiple types of components and injection mechanisms. The framework supports both visual components for DOM rendering and headless components for business logic, with advanced dependency injection and service management.
Juris supports three primary component types:
- Purpose: DOM-rendering components with lifecycle management
-
Registration:
componentManager.register(name, componentFn) - Features: Reactive props, async rendering, lifecycle hooks
-
Instantiation: Created during DOM rendering via
create(name, props)
- Purpose: Business logic components without DOM presence
-
Registration:
headlessManager.register(name, componentFn, options) - Features: API exposure, auto-initialization, service injection
- Instantiation: Can be auto-initialized or manually triggered
- Purpose: Progressive enhancement of existing DOM elements
-
Registration:
enhance(selector, definition, options) - Features: Mutation observers, selector-based enhancement, reactive properties
// Services are injected into component contexts automatically const context = { getState, setState, subscribe, services: this.services, ...this.services, // Direct service injection ...this.headlessAPIs, // Headless component API injection headless: this.headlessManager.context }
-
Service Spread: All services from
this.servicesare spread into context - Headless API Injection: APIs from headless components are injected
- Context Enrichment: Additional utilities and framework methods added
Component Context
├── Core Framework Methods (getState, setState, subscribe)
├── Registered Services (this.services)
├── Headless Component APIs (this.headlessAPIs)
├── Component Management Utils
└── Framework Utilities
// Registration componentManager.register(name, componentFn) ↓ // Storage in components Map components.set(name, componentFn) ↓ // Creation during rendering create(name, props) ↓ // Context creation with service injection _createComponentContext(componentId, componentStates) ↓ // Component function execution componentFn(props, context)
// Registration with options headlessManager.register(name, componentFn, options) ↓ // Storage with metadata components.set(name, { fn: componentFn, options }) ↓ // Auto-initialization queue (if autoInit: true) initQueue.add(name) ↓ // Initialization initialize(name, props) ↓ // Context creation and API exposure createHeadlessContext() → instance.api → juris.headlessAPIs[name]
When headless components return an api object, Juris automatically:
- Stores the API in
this.headlessAPIs[name] - Spreads it into all component contexts
- Updates existing component contexts via
_updateComponentContexts()
if (instance.api) { this.context[name] = instance.api; if (!this.juris.headlessAPIs) this.juris.headlessAPIs = {}; this.juris.headlessAPIs[name] = instance.api; this.juris._updateComponentContexts(); }
createContext(element = null) { const context = { // State management getState: (path, defaultValue, track) => this.stateManager.getState(path, defaultValue, track), setState: (path, value, context) => this.stateManager.setState(path, value, context), subscribe: (path, callback) => this.stateManager.subscribe(path, callback), // Service injection services: this.services, ...this.services, // Spread all services ...this.headlessAPIs, // Spread all headless APIs // Component management components: { register: (name, component) => this.componentManager.register(name, component), registerHeadless: (name, component, options) => this.headlessManager.register(name, component, options), get: name => this.componentManager.components.get(name), // ... more component utilities }, // Framework utilities utils: { render: container => this.render(container), cleanup: () => this.cleanup(), // ... more utilities } }; if (element) context.element = element; return context; }
context.newState = (key, initialValue) => { const statePath = `__local.${componentId}.${key}`; if (this.juris.stateManager.getState(statePath, Symbol('not-found')) === Symbol('not-found')) { this.juris.stateManager.setState(statePath, initialValue); } componentStates.add(statePath); return [ () => this.juris.stateManager.getState(statePath, initialValue), value => this.juris.stateManager.setState(statePath, value) ]; };
Components can provide lifecycle hooks that are automatically managed:
const instance = { name, props, hooks: componentResult.hooks || {}, api: componentResult.api || {}, render: componentResult.render }; // Auto-execution of lifecycle hooks if (instance.hooks.onMount) { setTimeout(() => { const mountResult = instance.hooks.onMount(); if (mountResult?.then) { promisify(mountResult).catch(error => /* handle async errors */); } }, 0); }
When components are destroyed, their local state is automatically cleaned up:
cleanup(element) { const states = this.componentStates.get(element); if (states) { states.forEach(statePath => { const pathParts = statePath.split('.'); let current = this.juris.stateManager.state; for (let i = 0; i < pathParts.length - 1; i++) { if (current[pathParts[i]]) current = current[pathParts[i]]; else return; } delete current[pathParts[pathParts.length - 1]]; }); } }
Juris can handle components with asynchronous properties:
_createWithAsyncProps(name, componentFn, props) { const placeholder = this._createPlaceholder(`Loading ${name}...`, 'juris-async-props-loading'); this._resolveAsyncProps(props).then(resolvedProps => { const realElement = this._createSyncComponent(name, componentFn, resolvedProps); if (realElement && placeholder.parentNode) { placeholder.parentNode.replaceChild(realElement, placeholder); } }); return placeholder; }
Components that return promises are handled with placeholders:
_handleAsyncComponent(resultPromise, name, props, componentStates) { const placeholder = this._createPlaceholder(`Loading ${name}...`, 'juris-async-loading'); resultPromise.then(result => { const realElement = this._processComponentResult(result, name, props, componentStates); if (realElement && placeholder.parentNode) { placeholder.parentNode.replaceChild(realElement, placeholder); } }); return placeholder; }
- Full DOM rendering capabilities
- Complete service injection
- Component management utilities
- State management with reactivity
- No DOM rendering methods
- Service injection
- State management
- Component registration utilities
- Specialized for business logic
- Element-specific context
- Partial service injection
- Reactive property handling
- DOM enhancement utilities
Services are registered during Juris initialization:
constructor(config = {}) { this.services = config.services || {}; // Services are automatically injected into all component contexts }
Components can access services in multiple ways:
// Via services object context.services.myService // Via direct injection (spread) context.myService // Via headless API injection context.headlessComponentAPI
Components communicate through shared state:
// Component A context.setState('shared.data', value); // Component B context.subscribe('shared.data', callback);
Via injected services:
// Service provides communication channel context.eventBus.emit('event', data); context.eventBus.on('event', handler);
Via exposed headless component APIs:
// Headless component exposes API return { api: { doSomething: () => { /* implementation */ } } }; // Other components can use it context.headlessComponentName.doSomething();
- Element recycling for better performance
- Component instance reuse
- Async props caching with timestamps
- Components are only instantiated when needed
- Async components use placeholders
- Progressive enhancement for existing DOM
- WeakMap usage for component instances
- Automatic cleanup of subscriptions
- State path cleanup on component destruction
- Keep services stateless when possible
- Use dependency injection for testability
- Provide clear APIs for service interaction
- Separate business logic into headless components
- Use visual components primarily for rendering
- Leverage service injection for cross-cutting concerns
- Use local state for component-specific data
- Use global state for shared application state
- Clean up state subscriptions properly
Juris implements a comprehensive component system with sophisticated injection mechanisms that support:
- Multiple component types for different use cases
- Automatic service injection into component contexts
- Lifecycle management with cleanup
- Async component handling with placeholders
- Progressive enhancement capabilities
- Flexible communication patterns between components
The injection system is designed to be non-intrusive yet powerful, allowing components to declare their dependencies implicitly through the context object while maintaining clean separation of concerns.