Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Developer's Guide

jurisauthor edited this page Aug 18, 2025 · 21 revisions

Juris Developer Reference Guide

Version v0.9.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/juris@0.88.2/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
class: () => 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: 'john@example.com'
 },
 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
}
// 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}`);
});
//or
const unsubscribe = app.subscribe('user.name', (newValue, oldValue, path) => {
 console.log(`${path} changed from ${oldValue} to ${newValue}`);
},false);
// 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', 'john@example.com');
})
// 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',
 class: 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: 'john@example.com', 
 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' }
 })
 };
};

Singleton Component

const TodoManager = (props, context) => {
 const [getTodos, setTodos] = context.newState('todos', []);
 
 return {
 render: () => [
 () => getTodos().map(todo => ({ div: { text: todo.text } }))
 ],
 api: {
 add: (text) => setTodos([...getTodos(), { id: Date.now(), text }]),
 clear: () => setTodos([]),
 getCount: () => getTodos().length
 }
 };
};
const Dashboard = (props, { components }) => ({
 render: () => [
 () => ({ div: { text: `Todos: ${components.getComponentAPI('TodoManager')?.getCount() || 0}` } }),
 { button: { text: 'Add Todo', onClick: () => components.getComponentAPI('TodoManager').add('New task') }},
 { button: { text: 'Clear All', onClick: () => components.getComponentAPI('TodoManager').clear() }}
 ]
});

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', {
 class: () => 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ドル'),
 class: () => 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
 class: () => 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() : '';
 },
 class: () => 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';
 },
 class: () => `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 }
 },
 }
});

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: {
 class: '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', 'john@example.com');
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: {
 class: () => 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

×ばつ', onclick: () => modalManager.close(props.id) } }, props.children ] } } ] : [] } }; };">
// 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: {
 class: () => modalManager.isOpen(props.id) ? 'modal open' : 'modal',
 onclick: (e) => {
 if (e.target === e.currentTarget) {
 modalManager.close(props.id);
 }
 },
 children: () => modalManager.isOpen(props.id) ? [
 { div: {
 class: 'modal-content',
 children: [
 { button: {
 class: '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: { 
 class: 'error',
 text: `Error: ${dataFetcher.getError('users')}`
 } }];
 }
 
 const users = dataFetcher.getData('users') || [];
 return users.map(user => ({
 div: {
 key: user.id,
 class: '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: {
 class: (element) => {
 // element is the actual DOM element
 const hasChildren = element.children.length > 0;
 return hasChildren ? 'parent' : 'empty';
 }
 }
}

Function Signatures

Without Element Parameter (Legacy)

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

With Element Parameter (Recommended)

class: (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: class, style, data-*, etc.
  3. Children added: children property processed last
{
 div: {
 // Runs BEFORE children are added
 class: (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: {
 class: (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: { 
 class: '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: { 
 class: 'user-item',
 text: item.name 
 }
 }));
 } catch (error) {
 console.error('Failed to load user data:', error);
 return [{ div: { text: 'Failed to load data', class: '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

class: (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...', class: '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', class: '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' } }],
 class: (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:

class: (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.

CSS Extraction

const app = new Juris({
 cssExtraction: true // Enable automatic CSS extraction for performance
});

When enabled, static styles are automatically extracted to CSS classes:

// This component:
{
 div: {
 style: {
 padding: '16px', // Static - extracted to CSS class
 border: '1px solid #ccc', // Static - extracted to CSS class 
 color: () => getState('theme.color') // Dynamic - stays inline
 }
 }
}
// Becomes:
// CSS: .j-div-abc123 { padding: 16px; border: 1px solid #ccc; }
// HTML: <div class="j-div-abc123" style="color: blue;">

Placeholder Configuration

const app = new Juris({
 // Global default placeholder for async content
 defaultPlaceholder: {
 class: 'app-loading',
 text: 'Please wait...',
 style: 'padding: 8px; background: #f5f5f5;'
 },
 
 // Element-specific placeholders
 placeholders: {
 'user-profile': {
 class: 'profile-loading',
 text: 'Loading user data...',
 style: 'background: linear-gradient(90deg, #f0f0f0 25%, transparent 37%, #f0f0f0 63%);'
 }
 }
});
// Runtime placeholder setup
app.setupIndicators('element-id', {
 class: 'custom-loading',
 text: 'Custom loading text',
 children: { div: { class: 'spinner' } }
});

Web Components

Creating Web Components

// Single Web Component
app.createWebComponent('user-card', (props, context) => ({
 div: {
 class: 'user-card',
 children: [
 { img: { src: props.avatar, alt: 'Avatar' } },
 { h3: { text: props.name } },
 { p: { text: props.email } },
 { button: { 
 text: props.following ? 'Unfollow' : 'Follow',
 onclick: () => context.component.emit('follow-change', { 
 userId: props.id,
 following: !props.following 
 })
 }}
 ]
 }
}), {
 attributes: ['name', 'email', 'avatar', 'following'], // Observed attributes
 shadowMode: 'open', // Shadow DOM
 styles: `
 .user-card { 
 padding: 16px; 
 border: 1px solid #ccc; 
 border-radius: 8px; 
 }
 `
});
// Multiple Web Components
app.createWebComponents({
 'data-table': {
 component: (props, context) => ({ /* table component */ }),
 options: {
 attributes: ['data', 'columns'],
 shadowMode: 'open'
 }
 },
 'progress-bar': {
 component: (props, context) => ({ /* progress component */ }),
 options: {
 attributes: ['value', 'max', 'color']
 }
 }
});

// Web Components with child web components using slots

juris.createWebComponent('my-component', (props, context) => {
 const {getState, setState} = context;
 
 return {
 div: {
 children: [
 {
 button: {
 text: () => 'Click Me: ' + getState('count', props.initialValue),
 onClick: () => {
 const count = getState('count', 0);
 setState('count', count + 1);
 }
 }
 },
 {
 slot: {} // This renders the children automatically
 }
 ]
 }
 };
}, {
 attributes: ['initialValue']
});

Web Component Usage in HTML

<!-- Use as standard HTML -->
<user-card 
 name="John Doe" 
 email="john@example.com" 
 avatar="/avatar.jpg"
 following="false">
</user-card>

Web Component Usage with child web component

<my-component initialValue="1" id="myComponent">
 <my-component2 />
</my-component>

Web Component Usage in Object VDOM

// 1. Simple Web Component Usage
{
 'user-card': {
 name: "John Doe",
 email: "john@example.com", 
 avatar: "/avatar.jpg",
 following: "false"
 }
}
// 2. Web Component with Reactive Props
{
 'user-card': {
 name: () => getState('user.name', 'Anonymous'),
 email: () => getState('user.email'),
 avatar: () => getState('user.avatar', '/default.jpg'),
 following: () => getState('user.isFollowing', false).toString()
 }
}
// 3. Web Component with Child Web Components
{
 'my-component': {
 initialValue: "1",
 id: "myComponent",
 children: [
 {
 'my-component2': {}
 }
 ]
 }
}
// 4. Multiple Child Components
{
 'my-component': {
 initialValue: "5",
 children: [
 {
 'my-component2': { 
 value: "child1" 
 }
 },
 {
 'my-component2': { 
 value: "child2" 
 }
 }
 ]
 }
}
// 5. Web Component with Mixed Children (Web Components + Regular Elements)
{
 'my-component': {
 initialValue: "10",
 children: [
 {
 h3: { 
 text: "Before Child Component" 
 }
 },
 {
 'my-component2': {
 data: "some-data"
 }
 },
 {
 p: { 
 text: "After Child Component" 
 }
 }
 ]
 }
}
// 6. Nested Web Components Structure
{
 'parent-component': {
 title: "Parent",
 children: [
 {
 'child-component': {
 name: "Child 1",
 children: [
 {
 'grandchild-component': {
 value: "Grandchild"
 }
 }
 ]
 }
 }
 ]
 }
}
// 7. Web Component in a Layout
{
 div: {
 class: "app-container",
 children: [
 {
 header: {
 children: [
 {
 'nav-component': {
 items: JSON.stringify(['Home', 'About', 'Contact'])
 }
 }
 ]
 }
 },
 {
 main: {
 children: [
 {
 'user-card': {
 name: "Alice Smith",
 email: "alice@example.com"
 }
 },
 {
 'user-card': {
 name: "Bob Jones", 
 email: "bob@example.com"
 }
 }
 ]
 }
 }
 ]
 }
}

Web Component Context

const WebComponent = (props, context) => {
 const { component } = context;
 
 // Web Component specific methods:
 component.getState(key) // Component-scoped state
 component.setState(key, value) // Component-scoped state
 component.getAttribute(name) // Get attribute value
 component.setAttribute(name, val) // Set attribute value
 component.emit(eventName, detail) // Emit custom event
 component.getSlot(name) // Get slotted content
 component.getAllSlots() // Get all slots
 
 return { div: { text: 'Web Component' } };
};

Enhanced Context API

Complete Context Reference

const MyComponent = (props, context) => {
 const {
 // Core State Management
 getState, // Get state value with optional tracking
 setState, // Set state value with optional context
 subscribe, // Hierarchical subscription to state changes
 subscribeExact, // Non-hierarchical subscription (exact path only)
 executeBatch, // Execute multiple state changes in a batch
 
 // Component-Specific State
 newState, // Create local component state [getter, setter]
 
 // Component Management
 components: {
 register, // Register new component
 registerHeadless, // Register headless component
 get, // Get component function by name
 getHeadless, // Get headless component instance
 initHeadless, // Initialize headless component
 reinitHeadless, // Reinitialize headless component
 getComponentAPI, // Get component's public API
 getComponentElement, // Get component's DOM element
 getNamedComponents, // Get all named components
 getHeadlessAPI, // Get headless component API
 getAllHeadlessAPIs, // Get all headless APIs
 },
 
 // Utility Methods
 utils: {
 render, // Render to container
 cleanup, // Cleanup framework resources
 forceRender, // Force immediate re-render
 objectToHtml, // Convert VDOM to DOM element
 setRenderMode, // Change render mode (fine-grained/batch)
 getRenderMode, // Get current render mode
 isFineGrained, // Check if fine-grained mode
 isBatchMode, // Check if batch mode
 getHeadlessStatus, // Get headless components status
 },
 
 // Direct Methods (also available at root level)
 objectToHtml, // Convert VDOM to DOM element
 setupIndicators, // Setup async loading indicators
 
 // Framework References
 juris, // Full Juris instance reference
 element, // Current element (if provided)
 
 // Environment Detection
 isSSR, // True if server-side rendering
 
 // Headless Component Context
 headless, // Headless manager context
 
 // Services (if configured - direct access)
 services, // Services object
 // Individual services spread at root level:
 // api, storage, NotificationManager, etc.
 
 // Headless APIs (if configured - direct access)
 // Individual headless APIs spread at root level
 
 // Logging System
 logger: {
 log, // General logging
 lwarn, // Warning logger
 error, // Error logger 
 info, // Info logger
 debug, // Debug logger
 subscribe, // Subscribe to log events
 unsubscribe, // Unsubscribe from log events
 }
 
 } = context;
 
 // Example usage:
 const [localCount, setLocalCount] = newState('count', 0);
 const globalUser = getState('user.name', 'Guest');
 
 return { 
 div: { 
 class: 'my-component',
 text: `Hello ${globalUser}, local count: ${localCount()}` 
 } 
 };
};

Enhancement Options

Advanced Enhancement Configuration

app.enhance(selector, definition, {
 // Mutation observation
 debounceMs: 100, // Debounce DOM mutations
 batchUpdates: true, // Batch multiple updates
 observeSubtree: true, // Watch nested changes
 observeChildList: true, // Watch added/removed elements
 observeNewElements: true, // Auto-enhance new elements
 
 // Viewport awareness (performance optimization)
 viewportAware: false, // Use Intersection Observer
 viewportMargin: '50px', // Intersection margin
 minimal: { // Minimal enhancement when off-screen
 style: { height: '200px' },
 class: 'placeholder'
 },
 
 // Callbacks
 onEnhanced: (element, context) => {
 console.log('Element enhanced:', element);
 }
});

Viewport-Aware Enhancement

// Enhance elements only when visible (performance optimization)
app.enhance('.expensive-widget', {
 // Full enhancement when visible
 class: 'widget-active',
 children: () => getState('widgets.data', []).map(renderComplexWidget)
}, {
 viewportAware: true,
 viewportMargin: '100px', // Start loading 100px before visible
 minimal: {
 // Minimal placeholder when not visible
 style: { height: '200px', background: '#f0f0f0' },
 text: 'Scroll to load widget'
 }
});

Template System Extensions

Service Injection in Templates

<template data-component="UserProfile" data-context="setState, getState, api, storage">
<script>
const user = () => getState('user', {});
const updateUser = (field, value) => setState(`user.${field}`, value);
const saveUser = async () => {
 await api.post('/api/user', user());
 storage.save('userBackup', user());
};
</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)}" />
 <button onclick="{()=>saveUser()}">Save Profile</button>
</div>
</template>

The data-context attribute injects specified services and APIs into the template scope.

State Management Extensions

Non-Reactive State Access

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

State Reset

// Reset all state to initial values
app.stateManager.reset();

Exact Path Subscriptions

// Subscribe only to exact path (not children)
const unsubscribe = app.subscribeExact('user', (value, oldValue, path) => {
 // Only triggers for exact 'user' path changes
 // Not for 'user.name', 'user.email', etc.
});

Development Tools

Framework Statistics

// Get framework statistics
app.getEnhancementStats(); // Enhancement statistics
app.getHeadlessStatus(); // Headless component status
// Render mode utilities
app.setRenderMode('batch'); // Change render mode
app.getRenderMode(); // Get current mode
app.isFineGrained(); // Check if fine-grained
app.isBatchMode(); // Check if batch mode

VDOM Utilities

// Convert VDOM to DOM element
const element = app.objectToHtml(vnode);
// Available in context too
const MyComponent = (props, { utils }) => {
 const domElement = utils.objectToHtml({ div: { text: 'Hello' } });
 return { div: { text: 'Component' } };
};

Framework Lifecycle

// Framework cleanup
app.cleanup(); // Manual cleanup
app.destroy(); // Full framework destruction

Reactive Arrays Developer Guide

Array of Reactive Anonymous Functions

Juris supports reactive anonymous functions in layout: [], component return arrays, and children arrays.

1. Layout Array

const juris = new Juris({
 layout: [
 { div: { text: 'Header' } },
 () => ({ div: { text: `User: ${juris.getState('user.name', 'Guest')}` } }),
 () => juris.getState('notifications', []).map(n => ({ div: { text: n.message } }))
 ]
});

2. Component Return Array

const TodoList = (props, { getState, setState }) => [
 { h2: { text: 'Todos' } },
 () => getState('todos', []).map(todo => ({
 div: { 
 key: todo.id,
 text: todo.text,
 onclick: () => setState(`todos.${todo.id}.done`, !todo.done)
 }
 })),
 () => ({ div: { text: `Total: ${getState('todos', []).length}` } })
];

3. Children Array

const Dashboard = (props, { getState }) => ({
 div: {
 children: [
 { nav: { text: 'Navigation' } },
 () => {
 const route = getState('route', 'home');
 return route === 'home' 
 ? { div: { text: 'Home Page' } }
 : { div: { text: 'Other Page' } };
 },
 () => getState('alerts', []).map(alert => ({ div: { text: alert } }))
 ]
 }
});

4. Component Render Method

const ReactiveComponent = (props, { getState, setState }) => ({
 render: () => [
 { h3: { text: 'Reactive Render' } },
 () => ({ div: { text: `Status: ${getState('status', 'idle')}` } }),
 () => getState('items', []).map(item => ({ 
 div: { key: item.id, text: item.name } 
 }))
 ]
});
// With lifecycle hooks
const LifecycleComponent = (props, { getState }) => ({
 hooks: {
 onMount: () => console.log('Component mounted'),
 onUpdate: (oldProps, newProps) => console.log('Updated'),
 onUnmount: () => console.log('Unmounting')
 },
 render: () => [
 { div: { text: 'Mounted component' } },
 () => ({ div: { text: `Time: ${getState('currentTime')}` } })
 ]
});

Key Features:

  • Auto-tracking: Functions re-render when accessed state changes
  • Async support: Return Promises for loading states
  • Mixed content: Combine static elements with reactive functions
  • Conditional: Return null to hide elements
  • Performance: Use getState(path, default, false) to skip tracking

ARM API Developer Guide

Enhanced Event Management with Context Access

The arm() API attaches event handlers to DOM elements, window, document, body, or any event target with full Juris context access.

Basic Element Usage

const button = document.querySelector('#btn');
juris.arm(button, ({ getState, setState }) => ({
 onclick: () => setState('count', getState('count', 0) + 1),
 onmouseenter: () => setState('hovered', true)
}));

Window Events

juris.arm(window, ({ getState, setState }) => ({
 onresize: () => setState('windowWidth', window.innerWidth),
 onscroll: () => setState('scrollY', window.scrollY),
 onbeforeunload: (e) => {
 if (getState('hasUnsavedChanges')) {
 e.returnValue = 'You have unsaved changes';
 }
 }
}));

Document Events

juris.arm(document, ({ setState, logger }) => ({
 onkeydown: (e) => {
 if (e.ctrlKey && e.key === 's') {
 e.preventDefault();
 setState('saveTriggered', true);
 logger.info('Save shortcut pressed');
 }
 },
 onclick: (e) => setState('lastClickTarget', e.target.tagName)
}));

Body Events

juris.arm(document.body, ({ getState, setState }) => ({
 'on-dragover': (e) => {
 e.preventDefault();
 setState('isDragOver', true);
 },
 'on-drop': (e) => {
 e.preventDefault();
 setState('isDragOver', false);
 setState('droppedFiles', Array.from(e.dataTransfer.files));
 }
}));

Event Name Formats

juris.arm(element, (context) => ({
 onclick: (e) => {}, // Standard
 'on-keydown': (e) => {}, // Dash format
 'on:focus': (e) => {} // Colon format
}));

Testing Support

const armed = juris.arm(button, ({ setState }) => ({
 onclick: () => setState('clicked', true)
}));
// Test utilities
console.log(armed.events); // List registered events
armed.trigger('onclick'); // Manually trigger event
armed.cleanup(); // Remove all listeners

Advanced Context

juris.arm(form, ({ getState, setState, services, executeBatch }) => ({
 onsubmit: async (e) => {
 e.preventDefault();
 executeBatch(() => {
 setState('loading', true);
 setState('errors', null);
 });
 
 try {
 await services.api.save(new FormData(e.target));
 setState('saved', true);
 } catch (error) {
 setState('errors', error.message);
 } finally {
 setState('loading', false);
 }
 }
}));

Key Features:

  • Any Event Target: Elements, window, document, body
  • Full Context: State, services, components, logger access
  • Event Formats: onclick, on-click, on:click
  • Testing: Built-in trigger() and cleanup() methods
  • Batching: Use executeBatch() for multiple state updates

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.

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /