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

  1. Service Spread: All services from this.services are spread into context
  2. Headless API Injection: APIs from headless components are injected
  3. 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.