-
Notifications
You must be signed in to change notification settings - Fork 8
Developer's 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
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:
-
Use labeled closing brackets for nested structures (
}//div,}//button) -
Prefer semantic HTML tags over unnecessary CSS classes (
div,button,ulnotdiv.wrapperunless styling needed) -
Use either
textORchildren- the last defined property wins -
Structure state paths logically (
user.profile.nameoruserName- both are fine, use nesting when it makes sense) - Use services for stateless utilities, headless components for stateful business logic
- Leverage headless components for business logic, regular components for UI
- 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
- 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.
<!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>
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();
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'
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!
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 }
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 }
// 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]);
// 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();
// 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'); }
// Skip reactivity subscription (3rd parameter = false) const value = getState('some.path', defaultValue, false);
// 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 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 }
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' } }) }; };
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() }} ] });
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 }; };
// 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 }); } }; });
// 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) }; } } });
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 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>
<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>
// 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 });
// 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' } } ] } });
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 } }, } });
// 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 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); } };
// 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(); });
// SSR-ready configuration const app = new Juris({ states: { isHydration: true }, layout: { App: {} } }); // Render with hydration mode app.render(); // Automatically handles SSR hydration
// 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' });
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; } ] });
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); } } });
// DOM renderer automatically recycles elements // No configuration needed - handles pool management // Clear caches when needed app.domRenderer.clearAsyncCache(); app.componentManager.clearAsyncPropsCache();
// 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();
// 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 } })); } } }; };
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()
// 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 } };
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' };
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') } } ] } }; };
// 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 ] } } ] : [] } }; };
// 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 } } ] } })); } } }; };
// 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());
// 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}`); });
// 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] });
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.
{ div: { class: (element) => { // element is the actual DOM element const hasChildren = element.children.length > 0; return hasChildren ? 'parent' : 'empty'; } } }
class: () => getState('some.path')
class: (element) => { const state = getState('some.path'); const elementContext = element.getAttribute('data-type'); return `${elementContext}-${state}`; }
Understanding when reactive functions execute is crucial:
-
Element creation:
document.createElement(tagName) -
Attributes processed:
class,style,data-*, etc. -
Children added:
childrenproperty 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' } }]; } } }
{ 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(' '); } } }
{ 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 }; } } }
{ canvas: { width: (element) => { const scale = getState('ui.scale'); const container = element.parentElement; const containerWidth = container ? container.offsetWidth : 800; return Math.floor(containerWidth * scale); } } }
{ 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 } })); } } }
{ 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' } }]; } } } }
- 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
- Rely on
element.childrenin 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
style: (element) => { return { color: getState('theme.color'), fontSize: `${getState('ui.scale')}px` }; }
class: (element) => { const newClass = getState('ui.class'); const isVisible = getState('ui.visible'); // Framework handles change detection automatically return isVisible ? newClass : 'hidden'; }
When using async children functions with element parameters, careful handling is essential to prevent issues:
{ 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' } }]; } } } }
{ 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 } })); } } }
{ 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'
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.
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;">
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' } } });
// 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'] });
<!-- Use as standard HTML --> <user-card name="John Doe" email="john@example.com" avatar="/avatar.jpg" following="false"> </user-card>
<my-component initialValue="1" id="myComponent"> <my-component2 /> </my-component>
// 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" } } ] } } ] } }
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' } }; };
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()}` } }; };
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); } });
// 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 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.
// Skip reactivity tracking for performance (3rd parameter) const value = getState('some.path', defaultValue, false); // No subscription created
// Reset all state to initial values app.stateManager.reset();
// 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. });
// 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
// 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 cleanup app.cleanup(); // Manual cleanup app.destroy(); // Full framework destruction
Reactive Arrays Developer Guide
Juris supports reactive anonymous functions in layout: [], component return arrays, and children arrays.
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 } })) ] });
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}` } }) ];
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 } })) ] } });
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')}` } }) ] });
- 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
nullto hide elements -
Performance: Use
getState(path, default, false)to skip tracking
ARM API Developer Guide
The arm() API attaches event handlers to DOM elements, window, document, body, or any event target with full Juris context access.
const button = document.querySelector('#btn'); juris.arm(button, ({ getState, setState }) => ({ onclick: () => setState('count', getState('count', 0) + 1), onmouseenter: () => setState('hovered', true) }));
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'; } } }));
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) }));
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)); } }));
juris.arm(element, (context) => ({ onclick: (e) => {}, // Standard 'on-keydown': (e) => {}, // Dash format 'on:focus': (e) => {} // Colon format }));
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
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); } } }));
-
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()andcleanup()methods -
Batching: Use
executeBatch()for multiple state updates
- Use keys for list items to enable efficient DOM reconciliation
- Batch state updates when making multiple changes
- Prefer headless components for complex business logic
- Use middleware for cross-cutting concerns like logging and validation
-
Structure state paths logically (e.g.,
user.profile.namenotuserProfileName) - Avoid deep nesting in state objects when possible
- Use local component state for UI-only state that doesn't need to be shared
- Enhance existing DOM rather than rebuilding when integrating with other libraries
- Implement error boundaries in components that fetch data
- Use templates for complex HTML structures with light JavaScript logic
Juris v0.8.0 - Built for performance, designed for simplicity.