Developer's Guide - jurisjs/juris GitHub Wiki
Version 0.82.0 - The only Non-Blocking Reactive Framework for JavaScript
Developers are now enjoying coding with Juris with VSCode Snippets and IntelliSense: VSCode-Snippets | JSDoc-TypeScript-Definitions
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
,ul
notdiv.wrapper
unless styling needed) -
Use either
text
ORchildren
- the last defined property wins -
Structure state paths logically (
user.profile.name
oruserName
- 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/[email protected]/juris.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const app = new Juris({
states: { count: 0 },
layout: {
div: {
text: () => app.getState('count', 0),
children: [
{ button: { text: '+', onclick: () => app.setState('count', app.getState('count') + 1) } },
{ button: { text: '-', onclick: () => app.setState('count', app.getState('count') - 1) } }
]
}//div
}
});
app.render();
</script>
</body>
</html>
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
className: () => 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: '[email protected]'
},
settings: {
theme: 'dark'
}
}
};
// When you call:
setState('user.profile.name', 'Jane');
// Juris automatically triggers updates for subscribers to:
// 1. 'user.profile.name' (exact match)
// 2. 'user.profile' (parent path)
// 3. 'user' (grandparent path)
// 4. Any child paths (if they existed)
Dependency Re-discovery: Reactive functions can discover new dependencies as they run:
const ConditionalComponent = (props, { getState }) => ({
div: {
text: () => {
const showDetails = getState('ui.showDetails', false);
if (showDetails) {
// When showDetails becomes true, Juris automatically
// subscribes to 'user.name' for future updates
return getState('user.name', 'No name');
}
return 'Click to show details';
}
}//div
});
// Initially subscribed to: ['ui.showDetails']
// After showDetails becomes true: ['ui.showDetails', 'user.name']
// Juris handles this subscription change automatically
Propagation in Action:
// Multiple components can subscribe to different levels
const UserProfile = (props, { getState }) => ({
div: {
// Subscribes to 'user.profile.name'
text: () => `Name: ${getState('user.profile.name', '')}`
}//div
});
const UserCard = (props, { getState }) => ({
div: {
// Subscribes to entire 'user.profile' object
text: () => {
const profile = getState('user.profile', {});
return `${profile.name} (${profile.email})`;
}
}//div
});
const UserSection = (props, { getState }) => ({
div: {
// Subscribes to entire 'user' object
children: () => {
const user = getState('user', {});
return [
{ UserProfile: {} },
{ UserCard: {} },
{ div: { text: `Theme: ${user.settings?.theme}` } }
];
}
}//div
});
// When you call setState('user.profile.name', 'Alice'):
// - UserProfile updates (direct subscription)
// - UserCard updates (parent subscription)
// - UserSection updates (grandparent subscription)
// All automatically, no manual event handling needed!
Juris uses a lean object syntax for virtual DOM:
// Single element
{ tagName: { prop: value } }
// With children
{
div: {
children: [
{ h1: { text: 'Title' } },
{ p: { text: 'Content' } }
]
}//div
}
// Use CSS selectors only when you need specific classes/IDs
{
'div.container': {
children: [
{ 'h1#main-title': { text: 'Main Title' } },
{ 'p.highlight': { text: 'Important content' } }
]
}//div.container
}
// Reactive properties use functions
{
div: {
text: () => getState('message'),
style: () => ({ color: getState('theme.color') }),
children: () => getState('items', []).map(item => ({ span: { text: item.name } }))
}//div
}
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}`);
});
// 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', '[email protected]');
})
// 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',
className: props.variant || 'default',
onclick: props.onClick || (() => {})
}//button
}));
// Register multiple components
const app = new Juris({
components: {
Header: (props, { getState }) => ({
header: {
h1: { text: () => getState('app.title', 'My App') }
}//header
}),
Counter: (props, { getState, setState }) => {
const count = () => getState('counter.value', 0);
return {
div: {
children: [
{ span: { text: () => `Count: ${count()}` } },
{ button: { text: '+', onclick: () => setState('counter.value', count() + 1) } },
{ button: { text: '-', onclick: () => setState('counter.value', count() - 1) } }
]
}//div
};
}
}
});
// Component with props
const UserCard = (props, { getState }) => ({
div: {
children: [
{ img: { src: props.avatar, alt: 'Avatar' } },
{ h3: { text: props.name } },
{ p: { text: props.email } },
{ button: {
text: 'Follow',
onclick: props.onFollow,
disabled: props.isFollowing
}}//button
]
}//div
});
// Usage with props
{
UserCard: {
name: 'John Doe',
email: '[email protected]',
avatar: '/avatar.jpg',
isFollowing: () => getState('following.includes', false),
onFollow: () => setState('following', [...getState('following', []), userId])
}//UserCard
}
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 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', {
className: () => getState('theme') === 'dark' ? 'btn-dark' : 'btn-light',
onclick: () => setState('clicks', getState('clicks', 0) + 1),
text: () => `Clicked ${getState('clicks', 0)} times`
});
// Enhance with function-based definition
app.enhance('.counter', (context) => {
const { getState, setState, element } = context;
return {
text: () => getState('counter.value', 0),
style: () => ({
color: getState('counter.value', 0) > 10 ? 'red' : 'blue'
}),
onclick: () => setState('counter.value', getState('counter.value', 0) + 1)
};
});
// Enhancement with services access
app.enhance('.api-button', (context) => {
const { api, storage, setState } = context; // Services from config
return {
text: 'Load Data',
onclick: async () => {
setState('loading', true);
try {
const data = await api.get('/api/users');
storage.save('users', data);
setState('users', data);
} catch (error) {
setState('error', error.message);
} finally {
setState('loading', false);
}
},
disabled: () => getState('loading', false)
};
});
// Enhancement with headless component access (direct from context)
app.enhance('.notification-trigger', (context) => {
// Headless APIs are available directly from context
const { NotificationManager } = context;
return {
text: 'Show Notification',
onclick: () => {
NotificationManager.show({
type: 'success',
message: 'Enhancement triggered!',
duration: 3000
});
}
};
});
// Enhance containers with multiple selectors
app.enhance('.dashboard', {
selectors: {
'.metric': {
text: () => getState('metrics.revenue', '$0'),
className: () => getState('metrics.trend') === 'up' ? 'positive' : 'negative'
},
'.chart': (context) => ({
innerHTML: () => `<canvas data-value="${getState('metrics.data', [])}"></canvas>`
}),
'.refresh-btn': {
onclick: () => {
setState('loading', true);
fetchMetrics().then(data => {
setState('metrics', data);
setState('loading', false);
});
},
disabled: () => getState('loading', false)
}
}
});
// Selector enhancement with services and headless components
app.enhance('.user-dashboard', {
// Container-level enhancement
className: () => getState('user.role', 'guest'),
selectors: {
'.user-avatar': (context) => {
const { api, storage } = context; // Services
return {
src: () => getState('user.avatar', '/default-avatar.png'),
onclick: async () => {
const newAvatar = await api.uploadAvatar();
storage.save('userAvatar', newAvatar);
setState('user.avatar', newAvatar);
}
};
},
'.notification-bell': (context) => {
// Direct access to headless component API
const { NotificationManager } = context;
return {
text: () => {
const count = NotificationManager.getUnreadCount();
return count > 0 ? count.toString() : '';
},
className: () => NotificationManager.hasUnread() ? 'has-notifications' : '',
onclick: () => NotificationManager.markAllAsRead()
};
},
'.sync-status': (context) => {
const { SyncManager, api } = context;
return {
text: () => {
const status = SyncManager.getStatus();
return status === 'syncing' ? 'Syncing...' :
status === 'error' ? 'Sync Failed' : 'Synced';
},
className: () => `sync-${SyncManager.getStatus()}`,
onclick: () => SyncManager.forceSync()
};
},
'.data-export': (context) => {
const { api, storage, DataManager } = context;
return {
text: 'Export Data',
onclick: async () => {
setState('exporting', true);
try {
const data = DataManager.getAllData();
const blob = await api.exportToCSV(data);
const url = URL.createObjectURL(blob);
// Create download link
const a = document.createElement('a');
a.href = url;
a.download = 'data-export.csv';
a.click();
storage.save('lastExport', Date.now());
} finally {
setState('exporting', false);
}
},
disabled: () => getState('exporting', false)
};
}
}
});
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 }
},
// Simple function (no auto-init)
CacheManager: (props, context) => ({
api: {
set: (key, value) => context.setState(`cache.${key}`, value),
get: (key) => context.getState(`cache.${key}`)
}
})
}
});
// Components handle async props automatically
const AsyncComponent = (props, context) => ({
div: {
// Async text - shows loading state automatically
text: fetch('/api/message').then(r => r.text()),
// Async children
children: fetch('/api/items').then(r => r.json()).then(items =>
items.map(item => ({ li: { text: item.name } }))
),
// Async styles
style: fetch('/api/theme').then(r => r.json())
}
});
// Mixed sync/async props
{
div: {
className: 'container', // sync
text: () => getState('title'), // reactive
style: fetchUserTheme(), // async promise
children: [
{ span: { text: 'Static content' } },
{ span: { text: fetchDynamicContent() } } // async
]
}
}
// Async 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', '[email protected]');
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: {
className: () => getState(`items.${props.itemId}.status`, 'active'),
children: [
{ span: { text: () => getState(`items.${props.itemId}.name`, '') } },
{ small: { text: () => getState(`items.${props.itemId}.updatedAt`, '') } }
]
}
}));
const OptimizedList = (props, { getState }) => {
let lastItemIds = [];
return {
ul: {
children: () => {
const items = getState('itemsList', []); // Just the list of IDs
const currentItemIds = items.map(item => item.id);
// If the list structure (IDs) hasn't changed, skip re-rendering
// Individual ListItem components will still update when their data changes
if (JSON.stringify(currentItemIds) === JSON.stringify(lastItemIds)) {
return "ignore";
}
lastItemIds = currentItemIds;
return items.map(item => ({
ListItem: {
itemId: item.id,
key: item.id
}
}));
}
}
};
};
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: {
className: () => modalManager.isOpen(props.id) ? 'modal open' : 'modal',
onclick: (e) => {
if (e.target === e.currentTarget) {
modalManager.close(props.id);
}
},
children: () => modalManager.isOpen(props.id) ? [
{ div: {
className: 'modal-content',
children: [
{ button: {
className: 'close',
text: '×',
onclick: () => modalManager.close(props.id)
} },
props.children
]
} }
] : []
}
};
};
// Generic data fetcher headless component
app.registerHeadlessComponent('DataFetcher', (props, { getState, setState }) => ({
api: {
fetch: async (key, url, options = {}) => {
setState(`data.${key}.loading`, true);
setState(`data.${key}.error`, null);
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
setState(`data.${key}.data`, data);
setState(`data.${key}.lastFetch`, Date.now());
} catch (error) {
setState(`data.${key}.error`, error.message);
} finally {
setState(`data.${key}.loading`, false);
}
},
getData: (key) => getState(`data.${key}.data`),
isLoading: (key) => getState(`data.${key}.loading`, false),
getError: (key) => getState(`data.${key}.error`),
shouldRefetch: (key, maxAge = 300000) => { // 5 minutes
const lastFetch = getState(`data.${key}.lastFetch`, 0);
return Date.now() - lastFetch > maxAge;
}
}
}));
// Usage in component
const UserList = (props, { components }) => {
const dataFetcher = components.getHeadlessAPI('DataFetcher');
// Auto-fetch on mount
useEffect(() => {
if (dataFetcher.shouldRefetch('users')) {
dataFetcher.fetch('users', '/api/users');
}
});
return {
div: {
children: () => {
if (dataFetcher.isLoading('users')) {
return [{ div: { text: 'Loading users...' } }];
}
if (dataFetcher.getError('users')) {
return [{ div: {
className: 'error',
text: `Error: ${dataFetcher.getError('users')}`
} }];
}
const users = dataFetcher.getData('users') || [];
return users.map(user => ({
div: {
key: user.id,
className: 'user-card',
children: [
{ h3: { text: user.name } },
{ p: { text: user.email } }
]
}
}));
}
}
};
};
// 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: {
className: (element) => {
// element is the actual DOM element
const hasChildren = element.children.length > 0;
return hasChildren ? 'parent' : 'empty';
}
}
}
className: () => getState('some.path')
className: (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:
className
,style
,data-*
, etc. -
Children added:
children
property processed last
{
div: {
// Runs BEFORE children are added
className: (element) => {
console.log(element.children.length); // Always 0
return 'container';
},
// Runs AFTER element and attributes are set
children: (element) => {
console.log(element.className); // 'container'
return [{ span: { text: 'Child content' } }];
}
}
}
{
input: {
className: (element) => {
const value = getState('form.email');
const isRequired = element.hasAttribute('required');
const inputType = element.type;
let classes = ['form-field'];
if (isRequired && !value) {
classes.push('field-required');
}
if (inputType === 'email' && value && !value.includes('@')) {
classes.push('field-invalid');
}
return classes.join(' ');
}
}
}
{
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: {
className: '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: {
className: 'user-item',
text: item.name
}
}));
} catch (error) {
console.error('Failed to load user data:', error);
return [{ div: { text: 'Failed to load data', className: 'error' } }];
}
}
}
}
- 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.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
style: (element) => {
return {
color: getState('theme.color'),
fontSize: `${getState('ui.scale')}px`
};
}
className: (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...', className: 'loading' } }];
}
if (element.dataset.loaded === 'true') {
// Return cached data from state
return getState(`users.${userId}.items`, []);
}
element.dataset.loading = 'true';
try {
const data = await fetchUserData(userId);
setState(`users.${userId}.items`, data);
element.dataset.loaded = 'true';
element.dataset.loading = 'false';
return data.map(item => ({ span: { text: item.name } }));
} catch (error) {
element.dataset.loading = 'false';
element.dataset.error = 'true';
return [{ div: { text: 'Error loading data', className: 'error' } }];
}
}
}
}
{
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' } }],
className: (element) => {
const hasChildren = element.children.length > 0;
return hasChildren ? 'parent' : 'empty';
}
}
}
// Initially returns 'empty', but after state update:
setState('trigger', Date.now()); // Forces re-evaluation
// Now returns 'parent'
Element parameters work in all modern browsers. For SSR environments, always check element existence:
className: (element) => {
const state = getState('some.path');
// SSR-safe
if (!element) {
return `server-${state}`;
}
// Client-side with element access
const elementType = element.tagName.toLowerCase();
return `${elementType}-${state}`;
}
Element parameters unlock powerful context-aware reactive patterns while maintaining Juris's declarative approach.
- 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.name
notuserProfileName
) - 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.