Developer's Guide - jurisjs/juris GitHub Wiki

Juris Developer Reference Guide

Version 0.82.0 - The only Non-Blocking Reactive Framework for JavaScript

Developers are now enjoying coding with Juris with VSCode Snippets and IntelliSense: VSCode-Snippets | JSDoc-TypeScript-Definitions

⚠️ Important: Follow This Reference Guide

This guide contains the canonical patterns and conventions for Juris development. Following these patterns is essential to:

  • Prevent spaghetti code - Juris's flexibility can lead to inconsistent patterns if not properly structured
  • Maintain code readability - Consistent VDOM syntax, component structure, and state organization
  • Ensure optimal performance - Proper use of reactivity, batching, and the "ignore" pattern
  • Enable team collaboration - Standardized approaches that all developers can understand and maintain
  • Leverage framework features - Correct usage of headless components, DOM enhancement, and state propagation

Key Rules:

  1. Use labeled closing brackets for nested structures (}//div, }//button)
  2. Prefer semantic HTML tags over unnecessary CSS classes (div, button, ul not div.wrapper unless styling needed)
  3. Use either text OR children - the last defined property wins
  4. Structure state paths logically (user.profile.name or userName - both are fine, use nesting when it makes sense)
  5. Use services for stateless utilities, headless components for stateful business logic
  6. Leverage headless components for business logic, regular components for UI
  7. Use DOM enhancement when integrating with existing HTML/libraries

Anti-patterns to avoid:

  • Mixing business logic in UI components
  • Deeply nested state objects
  • Unnecessary re-renders (use "ignore" pattern)
  • Using re-usable code into reactive attributes, props and children. Use headless or component body your reusable codes
  • Manual DOM manipulation outside Juris

⚠️ Security Best Practice

  • Never inject Juris instance and setState function into window and globals in production.
  • Use innerHTML with caution by sanitizing user provided data.

Follow these patterns religiously to build maintainable, performant Juris applications.

Quick Start

Basic Setup

<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/[email protected]/juris.js"></script>
</head>
<body>
  <div id="app"></div>
  <script>
    const app = new Juris({
      states: { count: 0 },
      layout: {
        div: {
          text: () => app.getState('count', 0),
          children: [
            { button: { text: '+', onclick: () => app.setState('count', app.getState('count') + 1) } },
            { button: { text: '-', onclick: () => app.setState('count', app.getState('count') - 1) } }
          ]
        }//div
      }
    });
    app.render();
  </script>
</body>
</html>

Component-Based App

const app = new Juris({
  states: { todos: [] },
  components: {
    TodoApp: (props, { getState, setState }) => ({
      div: {
        children: [
          { TodoInput: {} },
          { TodoList: {} }
        ]
      }//div
    }),
    
    TodoInput: (props, { getState, setState }) => ({
      input: {
        type: 'text',
        placeholder: 'Add todo...',
        onkeypress: (e) => {
          if (e.key === 'Enter' && e.target.value.trim()) {
            const todos = getState('todos', []);
            setState('todos', [...todos, { id: Date.now(), text: e.target.value.trim() }]);
            e.target.value = '';
          }
        }
      }//input
    }),
    
    TodoList: (props, { getState }) => ({
      ul: {
        children: () => getState('todos', []).map(todo => ({
          li: { text: todo.text, key: todo.id }
        }))
      }//ul
    })
  },
  layout: { TodoApp: {} }
});

app.render();

Core Concepts

Reactivity

Juris uses automatic dependency detection. When you call getState() inside reactive functions, it automatically subscribes to changes:

// Reactive text - updates when 'user.name' changes
text: () => getState('user.name', 'Anonymous')

// Reactive children - updates when 'items' changes  
children: () => getState('items', []).map(item => ({ li: { text: item.name } }))

// Reactive attributes
className: () => getState('isActive') ? 'active' : 'inactive'

State Change Propagation

Juris implements hierarchical state propagation - when you change a state path, it automatically notifies all related subscribers:

// Given this state structure:
const state = {
  user: {
    profile: {
      name: 'John',
      email: '[email protected]'
    },
    settings: {
      theme: 'dark'
    }
  }
};

// When you call:
setState('user.profile.name', 'Jane');

// Juris automatically triggers updates for subscribers to:
// 1. 'user.profile.name' (exact match)
// 2. 'user.profile' (parent path) 
// 3. 'user' (grandparent path)
// 4. Any child paths (if they existed)

Dependency Re-discovery: Reactive functions can discover new dependencies as they run:

const ConditionalComponent = (props, { getState }) => ({
  div: {
    text: () => {
      const showDetails = getState('ui.showDetails', false);
      
      if (showDetails) {
        // When showDetails becomes true, Juris automatically 
        // subscribes to 'user.name' for future updates
        return getState('user.name', 'No name');
      }
      
      return 'Click to show details';
    }
  }//div
});

// Initially subscribed to: ['ui.showDetails']
// After showDetails becomes true: ['ui.showDetails', 'user.name']
// Juris handles this subscription change automatically

Propagation in Action:

// Multiple components can subscribe to different levels
const UserProfile = (props, { getState }) => ({
  div: {
    // Subscribes to 'user.profile.name'
    text: () => `Name: ${getState('user.profile.name', '')}`
  }//div
});

const UserCard = (props, { getState }) => ({
  div: {
    // Subscribes to entire 'user.profile' object
    text: () => {
      const profile = getState('user.profile', {});
      return `${profile.name} (${profile.email})`;
    }
  }//div
});

const UserSection = (props, { getState }) => ({
  div: {
    // Subscribes to entire 'user' object
    children: () => {
      const user = getState('user', {});
      return [
        { UserProfile: {} },
        { UserCard: {} },
        { div: { text: `Theme: ${user.settings?.theme}` } }
      ];
    }
  }//div
});

// When you call setState('user.profile.name', 'Alice'):
// - UserProfile updates (direct subscription)
// - UserCard updates (parent subscription) 
// - UserSection updates (grandparent subscription)
// All automatically, no manual event handling needed!

VDOM Structure

Juris uses a lean object syntax for virtual DOM:

// Single element
{ tagName: { prop: value } }

// With children
{
  div: {
    children: [
      { h1: { text: 'Title' } },
      { p: { text: 'Content' } }
    ]
  }//div
}

// Use CSS selectors only when you need specific classes/IDs
{
  'div.container': {
    children: [
      { 'h1#main-title': { text: 'Main Title' } },
      { 'p.highlight': { text: 'Important content' } }
    ]
  }//div.container
}

// Reactive properties use functions
{
  div: {
    text: () => getState('message'),
    style: () => ({ color: getState('theme.color') }),
    children: () => getState('items', []).map(item => ({ span: { text: item.name } }))
  }//div
}

Text vs Children Precedence

Important: When both text and children are defined on the same element, the last one defined wins:

// Text wins (defined last)
{
  div: {
    children: [{ button: { text: 'Click me' } }],
    text: 'Override text' // This wins - shows "Override text"
  }//div
}

// Children win (defined last)  
{
  div: {
    text: 'Will be replaced',
    children: [{ button: { text: 'Click me' } }] // This wins - shows button
  }//div
}

// Reactive example - dynamic switching
{
  div: {
    children: [
      { p: { text: 'Default content' } },
      { button: { text: 'Action' } }
    ],
    text: () => {
      const loading = getState('isLoading', false);
      if (loading) {
        return 'Loading...'; // Replaces children when loading
      }
      // Return undefined to keep children
      return undefined;
    }
  }//div
}

// Best practice: Use either text OR children consistently
{
  div: {
    children: () => {
      const loading = getState('isLoading', false);
      if (loading) {
        return [{ span: { text: 'Loading...' } }];
      }
      return [
        { p: { text: 'Content loaded' } },
        { button: { text: 'Action' } }
      ];
    }
  }//div
}

State Management

Basic State Operations

// Initialize with default state
const app = new Juris({
  states: {
    user: { name: 'John', age: 30 },
    settings: { theme: 'dark' },
    items: []
  }
});

// Get state with default fallback
const name = app.getState('user.name', 'Unknown');
const theme = app.getState('settings.theme', 'light');
//accessing getState via app means you are outside the component scope.

// Set state (triggers reactivity)
app.setState('user.name', 'Jane');
app.setState('settings.theme', 'light');
app.setState('items', [...app.getState('items', []), newItem]);

State Subscriptions

// Subscribe to specific path
const unsubscribe = app.subscribe('user.name', (newValue, oldValue, path) => {
  console.log(`${path} changed from ${oldValue} to ${newValue}`);
});

// Subscribe to exact path only (no children)
const unsubscribeExact = app.subscribeExact('user', (newValue, oldValue) => {
  console.log('User object changed', newValue);
});

// Unsubscribe
unsubscribe();

Batched Updates

// Manual batching for performance
app.executeBatch(()=>{
  app.setState('user.name', 'John');
  app.setState('user.age', 31);
  app.setState('user.email', '[email protected]');
})

// Check if batching is active
if (app.stateManager.isBatchingActive()) {
  console.log('Currently batching updates');
}

Non-Reactive State Access

// Skip reactivity subscription (3rd parameter = false)
const value = getState('some.path', defaultValue, false);

Component System

Component Registration

// Register individual component
app.registerComponent('MyButton', (props, context) => ({
  button: {
    text: props.label || 'Click me',
    className: props.variant || 'default',
    onclick: props.onClick || (() => {})
  }//button
}));

// Register multiple components
const app = new Juris({
  components: {
    Header: (props, { getState }) => ({
      header: {
        h1: { text: () => getState('app.title', 'My App') }
      }//header
    }),
    
    Counter: (props, { getState, setState }) => {
      const count = () => getState('counter.value', 0);
      return {
        div: {
          children: [
            { span: { text: () => `Count: ${count()}` } },
            { button: { text: '+', onclick: () => setState('counter.value', count() + 1) } },
            { button: { text: '-', onclick: () => setState('counter.value', count() - 1) } }
          ]
        }//div
      };
    }
  }
});

Component Props

// Component with props
const UserCard = (props, { getState }) => ({
  div: {
    children: [
      { img: { src: props.avatar, alt: 'Avatar' } },
      { h3: { text: props.name } },
      { p: { text: props.email } },
      { button: { 
        text: 'Follow', 
        onclick: props.onFollow,
        disabled: props.isFollowing 
      }}//button
    ]
  }//div
});

// Usage with props
{
  UserCard: {
    name: 'John Doe',
    email: '[email protected]', 
    avatar: '/avatar.jpg',
    isFollowing: () => getState('following.includes', false),
    onFollow: () => setState('following', [...getState('following', []), userId])
  }//UserCard
}

Component Lifecycle

const LifecycleComponent = (props, context) => {
  return {
    // Component lifecycle hooks
    hooks: {
      onMount: () => {
        console.log('Component mounted');
        // Setup event listeners, timers, etc.
      },
      
      onUpdate: (oldProps, newProps) => {
        console.log('Props changed', { oldProps, newProps });
      },
      
      onUnmount: () => {
        console.log('Component unmounting');
        // Cleanup resources
      }
    },
    
    // Component API (accessible to parent)
    api: {
      focus: () => element.querySelector('input')?.focus(),
      getValue: () => getState('local.value', '')
    },
    
    // Render function
    render: () => ({
      div: { text: 'Lifecycle component' }
    })
  };
};

Local Component State

const StatefulComponent = (props, { newState }) => {
  const [getCount, setCount] = newState('count', 0);
  const [getText, setText] = newState('text', '');
  
  return {
    div: {
      children: [
        { input: { 
          value: () => getText(),
          oninput: (e) => setText(e.target.value)
        }},//input
        { button: { 
          text: () => `Clicked ${getCount()} times`,
          onclick: () => setCount(getCount() + 1)
        }}//button
      ]
    }//div
  };
};

DOM Enhancement

Basic Enhancement

// Enhance existing DOM elements
app.enhance('.my-button', {
  className: () => getState('theme') === 'dark' ? 'btn-dark' : 'btn-light',
  onclick: () => setState('clicks', getState('clicks', 0) + 1),
  text: () => `Clicked ${getState('clicks', 0)} times`
});

// Enhance with function-based definition
app.enhance('.counter', (context) => {
  const { getState, setState, element } = context;
  
  return {
    text: () => getState('counter.value', 0),
    style: () => ({
      color: getState('counter.value', 0) > 10 ? 'red' : 'blue'
    }),
    onclick: () => setState('counter.value', getState('counter.value', 0) + 1)
  };
});

// Enhancement with services access
app.enhance('.api-button', (context) => {
  const { api, storage, setState } = context; // Services from config
  
  return {
    text: 'Load Data',
    onclick: async () => {
      setState('loading', true);
      try {
        const data = await api.get('/api/users');
        storage.save('users', data);
        setState('users', data);
      } catch (error) {
        setState('error', error.message);
      } finally {
        setState('loading', false);
      }
    },
    disabled: () => getState('loading', false)
  };
});

// Enhancement with headless component access (direct from context)
app.enhance('.notification-trigger', (context) => {
  // Headless APIs are available directly from context
  const { NotificationManager } = context;
  
  return {
    text: 'Show Notification',
    onclick: () => {
      NotificationManager.show({
        type: 'success',
        message: 'Enhancement triggered!',
        duration: 3000
      });
    }
  };
});

Selector-Based Enhancement

// Enhance containers with multiple selectors
app.enhance('.dashboard', {
  selectors: {
    '.metric': {
      text: () => getState('metrics.revenue', '$0'),
      className: () => getState('metrics.trend') === 'up' ? 'positive' : 'negative'
    },
    
    '.chart': (context) => ({
      innerHTML: () => `<canvas data-value="${getState('metrics.data', [])}"></canvas>`
    }),
    
    '.refresh-btn': {
      onclick: () => {
        setState('loading', true);
        fetchMetrics().then(data => {
          setState('metrics', data);
          setState('loading', false);
        });
      },
      disabled: () => getState('loading', false)
    }
  }
});

// Selector enhancement with services and headless components
app.enhance('.user-dashboard', {
  // Container-level enhancement
  className: () => getState('user.role', 'guest'),
  
  selectors: {
    '.user-avatar': (context) => {
      const { api, storage } = context; // Services
      
      return {
        src: () => getState('user.avatar', '/default-avatar.png'),
        onclick: async () => {
          const newAvatar = await api.uploadAvatar();
          storage.save('userAvatar', newAvatar);
          setState('user.avatar', newAvatar);
        }
      };
    },
    
    '.notification-bell': (context) => {
      // Direct access to headless component API
      const { NotificationManager } = context;
      
      return {
        text: () => {
          const count = NotificationManager.getUnreadCount();
          return count > 0 ? count.toString() : '';
        },
        className: () => NotificationManager.hasUnread() ? 'has-notifications' : '',
        onclick: () => NotificationManager.markAllAsRead()
      };
    },
    
    '.sync-status': (context) => {
      const { SyncManager, api } = context;
      
      return {
        text: () => {
          const status = SyncManager.getStatus();
          return status === 'syncing' ? 'Syncing...' : 
                 status === 'error' ? 'Sync Failed' : 'Synced';
        },
        className: () => `sync-${SyncManager.getStatus()}`,
        onclick: () => SyncManager.forceSync()
      };
    },
    
    '.data-export': (context) => {
      const { api, storage, DataManager } = context;
      
      return {
        text: 'Export Data',
        onclick: async () => {
          setState('exporting', true);
          try {
            const data = DataManager.getAllData();
            const blob = await api.exportToCSV(data);
            const url = URL.createObjectURL(blob);
            
            // Create download link
            const a = document.createElement('a');
            a.href = url;
            a.download = 'data-export.csv';
            a.click();
            
            storage.save('lastExport', Date.now());
          } finally {
            setState('exporting', false);
          }
        },
        disabled: () => getState('exporting', false)
      };
    }
  }
});

Enhancement Options

app.enhance('.auto-update', definition, {
  debounceMs: 100,           // Debounce DOM mutations
  batchUpdates: true,        // Batch multiple updates
  observeSubtree: true,      // Watch for nested changes
  observeChildList: true,    // Watch for added/removed elements
  observeNewElements: true,  // Auto-enhance new elements
  onEnhanced: (element, context) => {
    console.log('Enhanced:', element);
  }
});

Template System

Template Syntax

<template data-component="UserProfile" data-context="setState, getState">
  <script>
    const user = () => getState('user', {});
    const updateUser = (field, value) => setState(`user.${field}`, value);
  </script>
  
  <div class="profile">
    <img src={()=>user().avatar} alt="Avatar" />
    <h2>{()=>user().name}</h2>
    <input 
      value={()=>user().email} 
      oninput={(e)=>updateUser('email', e.target.value)} 
    />
    <div class={()=>user().isOnline ? 'online' : 'offline'}>
      {text: ()=>user().isOnline ? 'Online' : 'Offline'}
    </div>
  </div>
</template>

Reactive Template Elements

<template data-component="TodoList" data-context="getState, setState">
  <script>
    const todos = () => getState('todos', []);
    const addTodo = (text) => setState('todos', [...todos(), { id: Date.now(), text }]);
  </script>
  
  <div>
    <input onkeypress={(e)=>{
      if(e.key==='Enter') {
        addTodo(e.target.value);
        e.target.value = '';
      }
    }} />
    
    <ul>
      {children: ()=>todos().map(todo => ({
        li: { text: todo.text, key: todo.id }
      }))}
    </ul>
  </div>
</template>

Auto-Compilation

// Templates auto-compile by default
const app = new Juris({
  autoCompileTemplates: true, // default
  states: { user: { name: 'John' } }
});

// Manual compilation
app.compileTemplates();

// Disable auto-compilation
const app = new Juris({
  autoCompileTemplates: false
});

Headless Components

Basic Headless Component

// Register headless component (no DOM)
app.registerHeadlessComponent('DataManager', (props, context) => {
  const { getState, setState, subscribe } = context;
  
  // Background logic
  const fetchData = async () => {
    setState('loading', true);
    try {
      const data = await fetch('/api/data').then(r => r.json());
      setState('data', data);
    } finally {
      setState('loading', false);
    }
  };
  
  return {
    // Lifecycle hooks
    hooks: {
      onRegister: () => {
        console.log('DataManager registered');
        fetchData(); // Initial load
        
        // Auto-refresh every 30 seconds
        setInterval(fetchData, 30000);
      },
      
      onUnregister: () => {
        console.log('DataManager cleanup');
      }
    },
    
    // Public API
    api: {
      refresh: fetchData,
      getData: () => getState('data', []),
      isLoading: () => getState('loading', false)
    }
  };
});

// Initialize headless component
app.initializeHeadlessComponent('DataManager');

// Access headless API in regular components
const MyComponent = (props, { components }) => ({
  div: {
    children: [
      { button: { 
        text: 'Refresh Data',
        onclick: () => components.getHeadlessAPI('DataManager').refresh()
      } },
      { div: { 
        text: () => components.getHeadlessAPI('DataManager').isLoading() ? 'Loading...' : 'Ready'
      } }
    ]
  }
});

Auto-Init Headless Components

const app = new Juris({
  headlessComponents: {
    // Auto-initialize on startup
    AuthManager: {
      fn: (props, context) => ({
        api: {
          login: (credentials) => { /* login logic */ },
          logout: () => { /* logout logic */ },
          isAuthenticated: () => context.getState('auth.isLoggedIn', false)
        }
      }),
      options: { autoInit: true }
    },
    
    // Simple function (no auto-init)
    CacheManager: (props, context) => ({
      api: {
        set: (key, value) => context.setState(`cache.${key}`, value),
        get: (key) => context.getState(`cache.${key}`)
      }
    })
  }
});

Async Handling

Async Props

// Components handle async props automatically
const AsyncComponent = (props, context) => ({
  div: {
    // Async text - shows loading state automatically
    text: fetch('/api/message').then(r => r.text()),
    
    // Async children
    children: fetch('/api/items').then(r => r.json()).then(items =>
      items.map(item => ({ li: { text: item.name } }))
    ),
    
    // Async styles
    style: fetch('/api/theme').then(r => r.json())
  }
});

// Mixed sync/async props
{
  div: {
    className: 'container', // sync
    text: () => getState('title'), // reactive
    style: fetchUserTheme(), // async promise
    children: [
      { span: { text: 'Static content' } },
      { span: { text: fetchDynamicContent() } } // async
    ]
  }
}

Async State Updates

// Async setState
setState('user', fetchUserData()); // Promise resolves automatically

// Manual async handling
const loadUser = async (userId) => {
  setState('loading', true);
  try {
    const user = await fetch(`/api/users/${userId}`).then(r => r.json());
    setState('user', user);
  } catch (error) {
    setState('error', error.message);
  } finally {
    setState('loading', false);
  }
};

Promise Tracking

// Track all promises for SSR/hydration
startTracking();

// Render with async content
app.render();

// Wait for all promises to resolve
onAllComplete(() => {
  console.log('All async operations completed');
  stopTracking();
});

Advanced Features

Server-Side Rendering (SSR)

// SSR-ready configuration
const app = new Juris({
  states: { isHydration: true },
  layout: { App: {} }
});

// Render with hydration mode
app.render(); // Automatically handles SSR hydration

Render Modes

// Fine-grained reactivity (default) - immediate updates
app.setRenderMode('fine-grained');

// Batch mode - batched updates for performance
app.setRenderMode('batch');

// Check current mode
if (app.isFineGrained()) {
  console.log('Using fine-grained rendering');
}

// Set mode in constructor
const app = new Juris({
  renderMode: 'batch' // or 'fine-grained'
});

Middleware

const app = new Juris({
  middleware: [
    // Logging middleware
    ({ path, oldValue, newValue, context }) => {
      console.log(`State change: ${path}`, { oldValue, newValue });
      return newValue; // Return undefined to use original value
    },
    
    // Validation middleware
    ({ path, newValue }) => {
      if (path === 'user.age' && newValue < 0) {
        console.warn('Age cannot be negative');
        return 0; // Override with valid value
      }
      return newValue;
    },
    
    // Persistence middleware
    ({ path, newValue }) => {
      if (path.startsWith('user.')) {
        localStorage.setItem('user', JSON.stringify(newValue));
      }
      return newValue;
    }
  ]
});

Service Injection

const app = new Juris({
  services: {
    api: {
      get: (url) => fetch(url).then(r => r.json()),
      post: (url, data) => fetch(url, { 
        method: 'POST', 
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' }
      })
    },
    
    storage: {
      save: (key, value) => localStorage.setItem(key, JSON.stringify(value)),
      load: (key) => JSON.parse(localStorage.getItem(key) || 'null')
    }
  }
});

// Use services in components
const MyComponent = (props, { api, storage }) => ({
  button: {
    text: 'Save Data',
    onclick: async () => {
      const data = await api.get('/api/data');
      storage.save('backup', data);
    }
  }
});

Performance Optimization

Element Recycling

// DOM renderer automatically recycles elements
// No configuration needed - handles pool management

// Clear caches when needed
app.domRenderer.clearAsyncCache();
app.componentManager.clearAsyncPropsCache();

Batched DOM Updates

// Manual batching for multiple state changes
app.stateManager.beginBatch();

// Multiple state updates
setState('user.name', 'John');
setState('user.email', '[email protected]');
setState('user.age', 30);

// Single DOM update
app.stateManager.endBatch();

Efficient List Rendering

// Use keys for efficient list updates
children: () => getState('items', []).map(item => ({
  li: { 
    text: item.name,
    key: item.id // Important for performance
  }
}))

// Avoid recreating objects in render functions
const renderItem = (item) => ({ li: { text: item.name, key: item.id } });
children: () => getState('items', []).map(renderItem)

// Use "ignore" pattern for structural optimization
app.registerComponent('ListItem', (props, { getState }) => ({
  li: {
    className: () => getState(`items.${props.itemId}.status`, 'active'),
    children: [
      { span: { text: () => getState(`items.${props.itemId}.name`, '') } },
      { small: { text: () => getState(`items.${props.itemId}.updatedAt`, '') } }
    ]
  }
}));

const OptimizedList = (props, { getState }) => {
  let lastItemIds = [];
  
  return {
    ul: {
      children: () => {
        const items = getState('itemsList', []); // Just the list of IDs
        const currentItemIds = items.map(item => item.id);
        
        // If the list structure (IDs) hasn't changed, skip re-rendering
        // Individual ListItem components will still update when their data changes
        if (JSON.stringify(currentItemIds) === JSON.stringify(lastItemIds)) {
          return "ignore";
        }
        
        lastItemIds = currentItemIds;
        return items.map(item => ({ 
          ListItem: { 
            itemId: item.id,
            key: item.id 
          } 
        }));
      }
    }
  };
};

API Reference

Core Instance Methods

const app = new Juris(config);

// State Management
app.getState(path, defaultValue, track = true)
app.setState(path, value, context = {})
app.subscribe(path, callback, hierarchical = true)
app.subscribeExact(path, callback)

// Component Management  
app.registerComponent(name, componentFn)
app.registerHeadlessComponent(name, componentFn, options = {})
app.getComponent(name)
app.getHeadlessComponent(name)
app.initializeHeadlessComponent(name, props = {})

// Rendering
app.render(container = '#app')
app.setRenderMode('fine-grained' | 'batch')
app.getRenderMode()
app.isFineGrained()
app.isBatchMode()

// DOM Enhancement
app.enhance(selector, definition, options = {})
app.configureEnhancement(options)
app.getEnhancementStats()

// Template System
app.compileTemplates()

// Utilities
app.cleanup()
app.destroy()
app.getHeadlessStatus()

Context Object

// Available in all components and enhancement functions
const context = {
  // State operations
  getState(path, defaultValue, track = true),
  setState(path, value, context = {}),
  subscribe(path, callback),
  
  // Local state (components only)
  newState(key, initialValue), // Returns [getter, setter]
  
  // Services (if configured)
  ...services,
  
  // Headless APIs
  ...headlessAPIs,
  
  // Component utilities
  components: {
    register(name, component),
    registerHeadless(name, component, options),
    get(name),
    getHeadless(name),
    initHeadless(name, props),
    reinitHeadless(name, props),
    getHeadlessAPI(name),
    getAllHeadlessAPIs()
  },
  
  // Utilities
  utils: {
    render(container),
    cleanup(),
    forceRender(),
    setRenderMode(mode),
    getRenderMode(),
    isFineGrained(),
    isBatchMode(),
    getHeadlessStatus()
  },
  
  // Framework access
  juris: app,
  
  // Current element (enhancement only)
  element: domElement,
  
  // Environment
  isSSR: boolean,
  
  // Logging
  logger: { log, warn, error, info, debug, subscribe, unsubscribe }
};

Configuration Options

const config = {
  // Initial state
  states: {},
  
  // State middleware
  middleware: [],
  
  // Root layout component
  layout: {},
  
  // Component definitions
  components: {},
  
  // Headless components
  headlessComponents: {},
  
  // Services for dependency injection
  services: {},
  
  // Render mode
  renderMode: 'auto' | 'fine-grained' | 'batch',
  
  // Template compilation
  autoCompileTemplates: true,
  
  // Logging level
  logLevel: 'debug' | 'info' | 'warn' | 'error'
};

Common Patterns

Form Handling

const LoginForm = (props, { getState, setState }) => {
  const [getEmail, setEmail] = newState('email', '');
  const [getPassword, setPassword] = newState('password', '');
  
  const handleSubmit = (e) => {
    e.preventDefault();
    setState('auth.isLoading', true);
    
    fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: getEmail(),
        password: getPassword()
      })
    })
    .then(r => r.json())
    .then(data => {
      setState('auth.user', data.user);
      setState('auth.token', data.token);
    })
    .finally(() => {
      setState('auth.isLoading', false);
    });
  };
  
  return {
    form: {
      onsubmit: handleSubmit,
      children: [
        { input: {
          type: 'email',
          placeholder: 'Email',
          value: () => getEmail(),
          oninput: (e) => setEmail(e.target.value)
        } },
        { input: {
          type: 'password', 
          placeholder: 'Password',
          value: () => getPassword(),
          oninput: (e) => setPassword(e.target.value)
        } },
        { button: {
          type: 'submit',
          text: () => getState('auth.isLoading') ? 'Logging in...' : 'Login',
          disabled: () => getState('auth.isLoading')
        } }
      ]
    }
  };
};

Modal Management

// Headless modal manager
app.registerHeadlessComponent('ModalManager', (props, { getState, setState }) => ({
  api: {
    open: (id, props = {}) => setState(`modals.${id}`, { open: true, ...props }),
    close: (id) => setState(`modals.${id}.open`, false),
    isOpen: (id) => getState(`modals.${id}.open`, false),
    getProps: (id) => getState(`modals.${id}`, {})
  }
}));

// Modal component
const Modal = (props, { components }) => {
  const modalManager = components.getHeadlessAPI('ModalManager');
  
  return {
    div: {
      className: () => modalManager.isOpen(props.id) ? 'modal open' : 'modal',
      onclick: (e) => {
        if (e.target === e.currentTarget) {
          modalManager.close(props.id);
        }
      },
      children: () => modalManager.isOpen(props.id) ? [
        { div: {
          className: 'modal-content',
          children: [
            { button: {
              className: 'close',
              text: '×',
              onclick: () => modalManager.close(props.id)
            } },
            props.children
          ]
        } }
      ] : []
    }
  };
};

Data Fetching Pattern

// Generic data fetcher headless component
app.registerHeadlessComponent('DataFetcher', (props, { getState, setState }) => ({
  api: {
    fetch: async (key, url, options = {}) => {
      setState(`data.${key}.loading`, true);
      setState(`data.${key}.error`, null);
      
      try {
        const response = await fetch(url, options);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        
        const data = await response.json();
        setState(`data.${key}.data`, data);
        setState(`data.${key}.lastFetch`, Date.now());
      } catch (error) {
        setState(`data.${key}.error`, error.message);
      } finally {
        setState(`data.${key}.loading`, false);
      }
    },
    
    getData: (key) => getState(`data.${key}.data`),
    isLoading: (key) => getState(`data.${key}.loading`, false),
    getError: (key) => getState(`data.${key}.error`),
    shouldRefetch: (key, maxAge = 300000) => { // 5 minutes
      const lastFetch = getState(`data.${key}.lastFetch`, 0);
      return Date.now() - lastFetch > maxAge;
    }
  }
}));

// Usage in component
const UserList = (props, { components }) => {
  const dataFetcher = components.getHeadlessAPI('DataFetcher');
  
  // Auto-fetch on mount
  useEffect(() => {
    if (dataFetcher.shouldRefetch('users')) {
      dataFetcher.fetch('users', '/api/users');
    }
  });
  
  return {
    div: {
      children: () => {
        if (dataFetcher.isLoading('users')) {
          return [{ div: { text: 'Loading users...' } }];
        }
        
        if (dataFetcher.getError('users')) {
          return [{ div: { 
            className: 'error',
            text: `Error: ${dataFetcher.getError('users')}`
          } }];
        }
        
        const users = dataFetcher.getData('users') || [];
        return users.map(user => ({
          div: {
            key: user.id,
            className: 'user-card',
            children: [
              { h3: { text: user.name } },
              { p: { text: user.email } }
            ]
          }
        }));
      }
    }
  };
};

Debugging and Development

State Inspection

// Debug state in console
console.log('Current state:', app.stateManager.state);

// Monitor all state changes using middleware
const debugMiddleware = ({ path, oldValue, newValue }) => {
  console.log(`State changed: ${path}`, { oldValue, newValue });
  return newValue;
};

const app = new Juris({
  middleware: [debugMiddleware],
  // ... other config
});

// Or subscribe to specific top-level paths
app.subscribe('user', (newValue, oldValue, path) => {
  console.log(`User state changed: ${path}`, { oldValue, newValue });
});

app.subscribe('app', (newValue, oldValue, path) => {
  console.log(`App state changed: ${path}`, { oldValue, newValue });
});

// Get enhancement statistics
console.log('Enhancement stats:', app.getEnhancementStats());

// Get headless component status
console.log('Headless status:', app.getHeadlessStatus());

Performance Monitoring

// Monitor render performance
const startTime = performance.now();
app.render();
console.log(`Render took: ${performance.now() - startTime}ms`);

// Monitor state update frequency
let updateCount = 0;
app.subscribe('', () => {
  updateCount++;
  console.log(`State updates: ${updateCount}`);
});

Error Handling

// Global error handling in middleware
const errorHandlingMiddleware = ({ path, oldValue, newValue, context }) => {
  try {
    // Validate state updates
    if (path === 'user.age' && typeof newValue !== 'number') {
      throw new Error('Age must be a number');
    }
    return newValue;
  } catch (error) {
    console.error(`State validation error for ${path}:`, error);
    // Return old value to prevent invalid change
    return oldValue;
  }
};

const app = new Juris({
  middleware: [errorHandlingMiddleware]
});

Element Parameter in Reactive Functions

Overview

Juris reactive functions receive the DOM element instance as their first parameter, enabling context-aware updates based on the element's current state, attributes, and properties.

Basic Syntax

{
  div: {
    className: (element) => {
      // element is the actual DOM element
      const hasChildren = element.children.length > 0;
      return hasChildren ? 'parent' : 'empty';
    }
  }
}

Function Signatures

Without Element Parameter (Legacy)

className: () => getState('some.path')

With Element Parameter (Recommended)

className: (element) => {
  const state = getState('some.path');
  const elementContext = element.getAttribute('data-type');
  return `${elementContext}-${state}`;
}

Processing Order

Understanding when reactive functions execute is crucial:

  1. Element creation: document.createElement(tagName)
  2. Attributes processed: className, style, data-*, etc.
  3. Children added: children property processed last
{
  div: {
    // Runs BEFORE children are added
    className: (element) => {
      console.log(element.children.length); // Always 0
      return 'container';
    },
    
    // Runs AFTER element and attributes are set
    children: (element) => {
      console.log(element.className); // 'container'
      return [{ span: { text: 'Child content' } }];
    }
  }
}

Common Patterns

Conditional Styling Based on Element State

{
  input: {
    className: (element) => {
      const value = getState('form.email');
      const isRequired = element.hasAttribute('required');
      const inputType = element.type;
      
      let classes = ['form-field'];
      
      if (isRequired && !value) {
        classes.push('field-required');
      }
      
      if (inputType === 'email' && value && !value.includes('@')) {
        classes.push('field-invalid');
      }
      
      return classes.join(' ');
    }
  }
}

Performance Optimization

{
  div: {
    style: (element) => {
      const newColor = getState('theme.color');
      const currentColor = element.style.color;
      
      // Return same value to minimize framework overhead
      if (currentColor === newColor) {
        return { color: newColor }; // Framework will detect no change
      }
      
      return { color: newColor };
    }
  }
}

Responsive Behavior

{
  canvas: {
    width: (element) => {
      const scale = getState('ui.scale');
      const container = element.parentElement;
      const containerWidth = container ? container.offsetWidth : 800;
      
      return Math.floor(containerWidth * scale);
    }
  }
}

Element-Aware Children

{
  div: {
    children: (element) => {
      const items = getState('list.items');
      const elementId = element.getAttribute('data-list-type');
      
      if (elementId === 'compact') {
        return items.slice(0, 5).map(item => ({ 
          span: { text: item.title } 
        }));
      }
      
      return items.map(item => ({ 
        div: { 
          className: 'item',
          text: item.description 
        } 
      }));
    }
  }
}

Async Children with Element Context

{
  div: {
    children: async (element) => {
      const userId = element.getAttribute('data-user-id');
      const cacheKey = `user-${userId}`;
      
      // Check element for cached data to avoid duplicate requests
      if (element.dataset.loaded === 'true') {
        return getState(`cache.${cacheKey}`, []);
      }
      
      try {
        const userData = await fetchUserData(userId);
        setState(`cache.${cacheKey}`, userData);
        
        // Mark element as loaded to prevent re-fetching
        element.dataset.loaded = 'true';
        
        return userData.map(item => ({
          div: { 
            className: 'user-item',
            text: item.name 
          }
        }));
      } catch (error) {
        console.error('Failed to load user data:', error);
        return [{ div: { text: 'Failed to load data', className: 'error' } }];
      }
    }
  }
}

Best Practices

✅ Do

  • Use element parameters for context-aware logic
  • Check element existence: element && element.property
  • Leverage element attributes and properties
  • Let framework's change detection handle optimization
  • Use for form validation and responsive sizing
  • Cache async results using element dataset or state
  • Handle async errors gracefully in children functions

❌ Don't

  • Rely on element.children in attribute functions (timing issue)
  • Perform expensive DOM operations in every reactive call
  • Mutate the element directly (use return values)
  • Store element references outside the function
  • Make duplicate async requests without caching
  • Ignore error handling in async children functions

Special Return Values

Style Objects

style: (element) => {
  return {
    color: getState('theme.color'),
    fontSize: `${getState('ui.scale')}px`
  };
}

Conditional Values

className: (element) => {
  const newClass = getState('ui.class');
  const isVisible = getState('ui.visible');
  
  // Framework handles change detection automatically
  return isVisible ? newClass : 'hidden';
}

Async Children Considerations

When using async children functions with element parameters, careful handling is essential to prevent issues:

Preventing Duplicate Requests

{
  div: {
    'data-user-id': '123',
    children: async (element) => {
      const userId = element.getAttribute('data-user-id');
      
      // Use element dataset to track loading state
      if (element.dataset.loading === 'true') {
        return [{ div: { text: 'Loading...', className: 'loading' } }];
      }
      
      if (element.dataset.loaded === 'true') {
        // Return cached data from state
        return getState(`users.${userId}.items`, []);
      }
      
      element.dataset.loading = 'true';
      
      try {
        const data = await fetchUserData(userId);
        setState(`users.${userId}.items`, data);
        element.dataset.loaded = 'true';
        element.dataset.loading = 'false';
        
        return data.map(item => ({ span: { text: item.name } }));
      } catch (error) {
        element.dataset.loading = 'false';
        element.dataset.error = 'true';
        return [{ div: { text: 'Error loading data', className: 'error' } }];
      }
    }
  }
}

Race Condition Handling

{
  div: {
    children: async (element) => {
      const requestId = Date.now().toString();
      element.dataset.currentRequest = requestId;
      
      const data = await fetchData();
      
      // Check if this is still the current request
      if (element.dataset.currentRequest !== requestId) {
        return 'ignore'; // Another request has started
      }
      
      return data.map(item => ({ div: { text: item.title } }));
    }
  }
}

Memory Cleanup

{
  div: {
    children: async (element) => {
      // Set up cleanup when element is removed
      const cleanup = () => {
        delete element.dataset.loading;
        delete element.dataset.loaded;
        // Cancel any pending requests if needed
      };
      
      // Store cleanup function (framework will call on unmount)
      element._jurisCleanup = cleanup;
      
      const data = await fetchData();
      return data.map(item => ({ span: { text: item.name } }));
    }
  }
}

For element state that depends on children, use state updates to trigger re-evaluation:

{
  div: {
    children: [{ span: { text: 'child' } }],
    className: (element) => {
      const hasChildren = element.children.length > 0;
      return hasChildren ? 'parent' : 'empty';
    }
  }
}

// Initially returns 'empty', but after state update:
setState('trigger', Date.now()); // Forces re-evaluation
// Now returns 'parent'

Timing Considerations

Element parameters work in all modern browsers. For SSR environments, always check element existence:

className: (element) => {
  const state = getState('some.path');
  
  // SSR-safe
  if (!element) {
    return `server-${state}`;
  }
  
  // Client-side with element access
  const elementType = element.tagName.toLowerCase();
  return `${elementType}-${state}`;
}

Element parameters unlock powerful context-aware reactive patterns while maintaining Juris's declarative approach.


Best Practices

  1. Use keys for list items to enable efficient DOM reconciliation
  2. Batch state updates when making multiple changes
  3. Prefer headless components for complex business logic
  4. Use middleware for cross-cutting concerns like logging and validation
  5. Structure state paths logically (e.g., user.profile.name not userProfileName)
  6. Avoid deep nesting in state objects when possible
  7. Use local component state for UI-only state that doesn't need to be shared
  8. Enhance existing DOM rather than rebuilding when integrating with other libraries
  9. Implement error boundaries in components that fetch data
  10. Use templates for complex HTML structures with light JavaScript logic

Juris v0.8.0 - Built for performance, designed for simplicity.

⚠️ **GitHub.com Fallback** ⚠️