Component - jurisjs/juris GitHub Wiki
Deep Dive: Juris Component System & Injection Architecture
Overview
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.
Component Architecture
1. Component Types
Juris supports three primary component types:
Visual Components (ComponentManager)
- 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)
Headless Components (HeadlessManager)
- 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
Enhanced Components (DOMEnhancer)
- Purpose: Progressive enhancement of existing DOM elements
- Registration:
enhance(selector, definition, options)
- Features: Mutation observers, selector-based enhancement, reactive properties
2. Component Injection Mechanisms
Service Injection
// 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
}
Dependency Resolution Process
- Service Spread: All services from
this.services
are spread into context - Headless API Injection: APIs from headless components are injected
- Context Enrichment: Additional utilities and framework methods added
Injection Hierarchy
Component Context
├── Core Framework Methods (getState, setState, subscribe)
├── Registered Services (this.services)
├── Headless Component APIs (this.headlessAPIs)
├── Component Management Utils
└── Framework Utilities
Component Registration & Creation
Visual Component Flow
// 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)
Headless Component Flow
// 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]
Advanced Injection Features
1. Automatic API Exposure
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();
}
2. Context Enrichment Patterns
Base Context Creation
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;
}
3. Component State Management
Local State Injection
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)
];
};
Component Lifecycle & Cleanup
1. Lifecycle Hook Management
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);
}
2. State Cleanup
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]];
});
}
}
Async Component Handling
1. Async Props Resolution
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;
}
2. Async Component Rendering
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;
}
Component Context Types
1. Regular Component Context
- Full DOM rendering capabilities
- Complete service injection
- Component management utilities
- State management with reactivity
2. Headless Component Context
- No DOM rendering methods
- Service injection
- State management
- Component registration utilities
- Specialized for business logic
3. Enhancement Context
- Element-specific context
- Partial service injection
- Reactive property handling
- DOM enhancement utilities
Service Registration & Discovery
1. Service Registration
Services are registered during Juris initialization:
constructor(config = {}) {
this.services = config.services || {};
// Services are automatically injected into all component contexts
}
2. Service Discovery
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
Component Communication Patterns
1. State-Based Communication
Components communicate through shared state:
// Component A
context.setState('shared.data', value);
// Component B
context.subscribe('shared.data', callback);
2. Service-Based Communication
Via injected services:
// Service provides communication channel
context.eventBus.emit('event', data);
context.eventBus.on('event', handler);
3. Headless API Communication
Via exposed headless component APIs:
// Headless component exposes API
return {
api: {
doSomething: () => { /* implementation */ }
}
};
// Other components can use it
context.headlessComponentName.doSomething();
Performance Considerations
1. Component Caching
- Element recycling for better performance
- Component instance reuse
- Async props caching with timestamps
2. Lazy Evaluation
- Components are only instantiated when needed
- Async components use placeholders
- Progressive enhancement for existing DOM
3. Memory Management
- WeakMap usage for component instances
- Automatic cleanup of subscriptions
- State path cleanup on component destruction
Best Practices for Component Injection
1. Service Design
- Keep services stateless when possible
- Use dependency injection for testability
- Provide clear APIs for service interaction
2. Component Architecture
- Separate business logic into headless components
- Use visual components primarily for rendering
- Leverage service injection for cross-cutting concerns
3. State Management
- Use local state for component-specific data
- Use global state for shared application state
- Clean up state subscriptions properly
Conclusion
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.