Juris Platform Documentation
JavaScript Unified Reactive Interface Solution - A comprehensive object-first architecture that makes reactivity an intentional choice rather than an automatic behavior.
๐ Quick Start
Get up and running with JurisJS in minutes
๐ก Examples
See real-world applications and demos
๐ Enhancement API
Progressive enhancement for existing websites
๐ API Reference
Complete API documentation and guides
๐ฏ Best Practices
Learn the recommended patterns and techniques
๐ Features
๐ Reactive State Management
Automatic UI updates when state changes with precise control over reactivity
๐งฉ Component System
Reusable, composable UI components with pure JavaScript objects
๐ฃ๏ธ Advanced Routing
Hash-based routing with guards, parameters, and middleware
๐พ Multi-tier Sync
localStorage, cross-tab, and remote server synchronization
๐ Route Guards
Authentication, authorization, data loading, and unsaved changes protection
โก Zero Dependencies
Pure JavaScript, no external libraries required
๐ Quick Start
Download the Platofrm Core
Download Juris Core (juris.js)Basic Setup
Pure JavaScript, no external libraries required
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JurisJS Counter Example</title>
<body>
<div id="app"></div>
<script src="https://jurisjs.com/juris.js"></script>
<script>
const app = new Juris({
states: {
counter: 0
},
components: {
Counter: (props, { getState, setState }) => ({
div: {
children: () => [{
h2: { text: () => `Count: ${getState('counter')}` }
}, {button: {
text: 'Increment',
onClick: () => setState('counter', getState('counter') + 1)
}
}]
}
})
},
layout: {
div: {
children: () => [{ Counter: {} }]
}
}
});
app.render('#app');
</script>
</body>
</html>
๐ฏ Core Concepts
State Management
Juris uses a centralized non-reactive state store:
<script>
// Set state
app.setState('user.name', 'John Doe');
app.setState('todos', [...todos, newTodo]);
// Get state
const userName = app.getState('user.name', 'Guest');
const todoCount = app.getState('todos', []).length;
// Subscribe to changes
const unsubscribe = app.subscribe('user.name', (newName, oldName) => {
console.log(`Name changed from ${oldName} to ${newName}`);
});
</script>
Component System
Components are pure functions that return UI objects:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JurisJS Parent-Child Components</title>
</head>
<body>
<div id="app"></div>
<script src="https://jurisjs.com/juris.js"></script>
<script>
// Child Component: Simple display component
const Greeting = (props) => ({
h2: {
text: () => {
// Handle both function and string values for props.name
return `Hello, ${props.name}!`;
}
}
});
// Child Component: Interactive button
const Counter = (props, { getState, setState }) => ({
div: {
children: () => [{
p: { text: () => `Count: ${getState('count', 0)}` }
}, {
button: {
text: 'Click me',
onClick: () => setState('count', getState('count', 0) + 1)
}
}]
}
});
// Parent Component: Uses child components
const App = (props, { getState }) => ({
div: {
children: () => [{
h1: { text: 'My App' }
}, {
Greeting: { name: 'World' }
}, {
Greeting: { name: () => getState('user.name', 'Guest') + ', you current count is: ' + getState('count', 0) }
}, {
Counter: {}
}]
}
});
const app = new Juris({
states: {
count: 0,
user: { name: 'John' }
},
components: {
Greeting, // Child component
Counter, // Child component
App // Parent component
},
layout: {
div: {
children: () => [{ App: {} }]
}
}
});
app.render('#app');
</script>
</body>
</html>
Reactive Attributes
Any attribute can be reactive by using a function:
<script>
const DynamicComponent = (props, { getState }) => ({
div: {
// Static attributes
className: 'container',
// Reactive attributes
style: {
backgroundColor: () => getState('theme') === 'dark' ? '#333' : '#fff',
color: () => getState('theme') === 'dark' ? '#fff' : '#333'
},
text: () => `Current user: ${getState('user.name', 'Guest')}`,
// Reactive children
children: () => getState('items', []).map(item => ({
div: { text: item.name }
}))
}
});
</script>
๐ Progressive Enhancement API
The enhance()
method allows you to progressively enhance existing HTML elements with reactive behavior, making it perfect for adding modern interactivity to existing websites without rebuilding them.
Basic Enhancement
Transform static HTML into reactive components:
<script>
// Existing HTML works immediately
<button class="counter-btn" data-count="0">Count: 0</button>
// Enhance with reactivity
app.enhance('.counter-btn', (props, { useState }) => {
const [getCount, setCount] = useState('count', 0);
return {
textContent: () => `Count: ${getCount()}`,
onClick: () => setCount(getCount() + 1),
disabled: () => getCount() >= 10
};
});
</script>
Enhancement Syntax
<script>
app.enhance(selector, enhancementFunction, options?)
</script>
Parameters:
selector
- CSS selector for elements to enhanceenhancementFunction
- Function that returns reactive propertiesoptions
- Optional configuration object
Enhancement Function Context
Enhancement functions receive the same context as components:
<script>
app.enhance('.my-element', (props, context) => {
// props: { element, dataset, index }
// context: { useState, getState, setState, navigate, services }
const { element, dataset, index } = props;
const { useState, getState, setState } = context;
return {
// Reactive properties
textContent: () => `Item ${index}: ${getState('data')}`,
className: () => getState('theme') === 'dark' ? 'dark-item' : 'light-item',
// Event handlers
onClick: (event) => {
setState('selectedItem', index);
},
// Attributes
'data-active': () => getState('selectedItem') === index,
disabled: () => getState('isLoading', false)
};
});
</script>
Real-World Examples
Shopping Cart Enhancement
<!-- Existing HTML -->
<div class="product-card">
<h3>Awesome Product</h3>
<p class="price">$29.99</p>
<button class="add-to-cart" data-product-id="123">Add to Cart</button>
</div>
<script>
// Enhance add-to-cart buttons
app.enhance('.add-to-cart', (props, { useState }) => {
const [getCart, setCart] = useState('cart.items', []);
const productId = props.dataset.productId;
const isInCart = () => getCart().some(item => item.id === productId);
return {
textContent: () => isInCart() ? 'โ In Cart' : 'Add to Cart',
className: () => isInCart() ? 'btn in-cart' : 'btn',
disabled: () => isInCart(),
onClick: () => {
if (!isInCart()) {
const newItem = {
id: productId,
name: props.element.closest('.product-card').querySelector('h3').textContent,
price: 29.99
};
setCart([...getCart(), newItem]);
}
}
};
});
</script>
Form Validation Enhancement
<!-- Existing form -->
<form class="contact-form">
<input type="email" class="email-input" placeholder="Your email" required>
<textarea class="message-input" placeholder="Your message" required></textarea>
<button type="submit" class="submit-btn">Send Message</button>
</form>
<script>
// Enhance form inputs with validation
app.enhance('.email-input', (props, { useState }) => {
const [getEmail, setEmail] = useState('form.email', '');
const [getErrors, setErrors] = useState('form.errors', {});
const isValid = () => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(getEmail());
return {
value: () => getEmail(),
className: () => {
if (!getEmail()) return 'form-input';
return isValid() ? 'form-input valid' : 'form-input invalid';
},
onInput: (e) => {
setEmail(e.target.value);
if (getErrors().email) {
setErrors({ ...getErrors(), email: null });
}
},
onBlur: () => {
if (getEmail() && !isValid()) {
setErrors({ ...getErrors(), email: 'Please enter a valid email' });
}
}
};
});
// Enhance submit button
app.enhance('.submit-btn', (props, { getState }) => {
const isFormValid = () => {
const email = getState('form.email', '');
const message = getState('form.message', '');
const errors = getState('form.errors', {});
return email && message && !errors.email && !errors.message;
};
return {
disabled: () => !isFormValid(),
className: () => isFormValid() ? 'btn btn-primary' : 'btn btn-disabled'
};
});
</script>
Live Search Enhancement
<!-- Existing search -->
<div class="search-container">
<input type="text" class="search-input" placeholder="Search products...">
<div class="search-results"></div>
</div>
<script>
// Enhance search input with live results
app.enhance('.search-input', (props, { useState, services }) => {
const [getQuery, setQuery] = useState('search.query', '');
const [getResults, setResults] = useState('search.results', []);
let debounceTimer;
return {
value: () => getQuery(),
onInput: (e) => {
const query = e.target.value;
setQuery(query);
// Debounced search
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
if (query.length >= 2) {
const results = await services.searchService.search(query);
setResults(results);
} else {
setResults([]);
}
}, 300);
}
};
});
// Enhance results container
app.enhance('.search-results', (props, { getState }) => {
return {
innerHTML: () => {
const results = getState('search.results', []);
const query = getState('search.query', '');
if (!query) return '';
if (results.length === 0) return '<p>No results found</p>';
return results.map(item =>
`<div class="result-item">
<h4>${item.name}</h4>
<p>${item.description}</p>
</div>`
).join('');
}
};
});
</script>
Enhancement Options
<script>
app.enhance('.my-element', enhancementFn, {
// Only enhance elements that match additional criteria
filter: (element) => !element.hasAttribute('data-enhanced'),
// Run after enhancement
onEnhanced: (element, instance) => {
console.log('Enhanced:', element);
},
// Cleanup function
onDestroy: (element, instance) => {
console.log('Cleaning up:', element);
}
});
</script>
Mass Enhancement
Enhance multiple element types at once:
<script>
// Enhance all interactive elements
const interactiveElements = {
'.like-btn': (props, { useState }) => {
const [getLikes, setLikes] = useState(`likes.${props.dataset.postId}`, 0);
return {
textContent: () => `โฅ ${getLikes()}`,
onClick: () => setLikes(getLikes() + 1)
};
},
'.share-btn': (props, { getState }) => ({
onClick: () => {
const url = window.location.href;
navigator.share({ url, title: document.title });
}
}),
'.bookmark-btn': (props, { useState }) => {
const [getBookmarks, setBookmarks] = useState('bookmarks', []);
const itemId = props.dataset.itemId;
return {
className: () => getBookmarks().includes(itemId) ? 'bookmarked' : '',
onClick: () => {
const bookmarks = getBookmarks();
if (bookmarks.includes(itemId)) {
setBookmarks(bookmarks.filter(id => id !== itemId));
} else {
setBookmarks([...bookmarks, itemId]);
}
}
};
}
};
// Apply all enhancements
Object.entries(interactiveElements).forEach(([selector, enhancement]) => {
app.enhance(selector, enhancement);
});
</script>
Enhancement vs Components
๐ Enhancement API
For: Existing HTML, progressive enhancement, gradual adoption
- Works with existing markup
- SEO-friendly content first
- Gradual feature addition
- Zero breaking changes
๐งฉ Component System
For: New applications, complete SPAs, complex UIs
- Full application architecture
- Component composition
- Advanced routing
- Complex state management
Best Practices
- Start with working HTML - Ensure your page works without JavaScript
- Enhance progressively - Add features gradually without breaking existing functionality
- Use data attributes - Store configuration data in
data-*
attributes - Handle edge cases - Check for element existence and valid data
- Clean up properly - Use
onDestroy
for cleanup when elements are removed
Migration Path
Transform your existing website step-by-step:
<script>
// Phase 1: Add basic interactivity
app.enhance('.interactive', basicEnhancements);
// Phase 2: Add state management
app.enhance('.stateful', stateEnhancements);
// Phase 3: Add navigation features
app.enhance('.navigation', routingEnhancements);
// Phase 4: Convert to full components (optional)
app.registerComponent('AdvancedFeature', fullComponent);
</script>
๐ State Management
Basic and Advance Operations
<script>
// Simple values
app.setState('counter', 42);
app.setState('isLoading', true);
// Nested objects
app.setState('user.profile.name', 'John');
app.setState('settings.theme', 'dark');
// Arrays
app.setState('todos', [...currentTodos, newTodo]);
// With context
app.setState('data', newData, { skipPersist: true });
// Safe handling of undefined paths
const userName = app.getState('user.profile.name', 'Anonymous');
const preferences = app.getState('user.preferences', {});
const todos = app.getState('todos', []);
// Setting null vs undefined
app.setState('user.avatar', null); // Explicitly null
app.setState('user.tempData', undefined); // Remove property
// Deep null checks
const email = app.getState('user.contact.email', null);
if (email !== null) {
// Email exists and is not explicitly null
}
// Safe array operations
const currentTodos = app.getState('todos', []);
// Add item
app.setState('todos', [...currentTodos, newTodo]);
// Remove item by ID
app.setState('todos', currentTodos.filter(todo => todo.id !== todoId));
// Update item by ID
app.setState('todos', currentTodos.map(todo =>
todo.id === todoId ? { ...todo, completed: true } : todo
));
// Insert at specific position
const insertAt = (arr, index, item) => [
...arr.slice(0, index),
item,
...arr.slice(index)
];
app.setState('todos', insertAt(currentTodos, 2, newTodo));
// Move item (reorder)
const moveItem = (arr, fromIndex, toIndex) => {
const item = arr[fromIndex];
const filtered = arr.filter((_, i) => i !== fromIndex);
return [
...filtered.slice(0, toIndex),
item,
...filtered.slice(toIndex)
];
};
app.setState('todos', moveItem(currentTodos, 0, 3));
// Deep object merging
const currentUser = app.getState('user', {});
app.setState('user', {
...currentUser,
profile: {
...currentUser.profile,
name: 'Updated Name',
lastUpdated: new Date().toISOString()
}
});
// Partial updates with spread
const updateUserProfile = (updates) => {
const current = app.getState('user.profile', {});
app.setState('user.profile', { ...current, ...updates });
};
// Safe nested property updates
const setNestedProperty = (path, value) => {
const pathParts = path.split('.');
const current = app.getState(pathParts[0], {});
let updated = { ...current };
let pointer = updated;
for (let i = 1; i < pathParts.length - 1; i++) {
pointer[pathParts[i]] = { ...pointer[pathParts[i]] };
pointer = pointer[pathParts[i]];
}
pointer[pathParts[pathParts.length - 1]] = value;
app.setState(pathParts[0], updated);
};
// Batch multiple updates for performance
const batchUpdate = (updates) => {
Object.entries(updates).forEach(([path, value]) => {
app.setState(path, value);
});
};
batchUpdate({
'user.name': 'John Doe',
'user.email': 'john@example.com',
'user.lastLogin': new Date().toISOString(),
'ui.notifications.count': 0
});
// Conditional batching
const updateUserWithValidation = (userData) => {
const updates = {};
if (userData.name && userData.name.length > 0) {
updates['user.name'] = userData.name;
}
if (userData.email && userData.email.includes('@')) {
updates['user.email'] = userData.email;
}
if (Object.keys(updates).length > 0) {
updates['user.lastUpdated'] = new Date().toISOString();
batchUpdate(updates);
}
};
/ Input validation before setting state
const setValidatedState = (path, value, validator) => {
const isValid = validator(value);
if (isValid) {
app.setState(path, value);
app.setState(`${path}.error`, null);
} else {
app.setState(`${path}.error`, 'Invalid value');
}
};
// Email validation example
setValidatedState('user.email', email, (val) =>
val && val.includes('@') && val.includes('.')
);
// Transform data before storing
const setNormalizedState = (path, value, transformer) => {
const transformed = transformer(value);
app.setState(path, transformed);
};
// Normalize user input
setNormalizedState('user.name', rawName, (name) =>
name.trim().toLowerCase().replace(/\s+/g, ' ')
);
// Normalized data patterns (recommended for large datasets)
const normalizeEntities = (entities, idField = 'id') => {
const byId = {};
const allIds = [];
entities.forEach(entity => {
byId[entity[idField]] = entity;
allIds.push(entity[idField]);
});
return { byId, allIds };
};
// Store normalized todos
const todos = [
{ id: 1, text: 'Buy groceries', completed: false },
{ id: 2, text: 'Walk the dog', completed: true }
];
const normalized = normalizeEntities(todos);
app.setState('todos.byId', normalized.byId);
app.setState('todos.allIds', normalized.allIds);
// Access normalized data
const getTodos = () => {
const byId = app.getState('todos.byId', {});
const allIds = app.getState('todos.allIds', []);
return allIds.map(id => byId[id]).filter(Boolean);
};
// Update single normalized entity
const updateTodo = (id, updates) => {
const current = app.getState(`todos.byId.${id}`, {});
app.setState(`todos.byId.${id}`, { ...current, ...updates });
};
// Safe state hydration from external sources
const hydrateState = (data, allowedPaths = []) => {
Object.entries(data).forEach(([path, value]) => {
if (allowedPaths.length === 0 || allowedPaths.includes(path)) {
try {
app.setState(path, value);
} catch (error) {
console.warn(`Failed to hydrate path: ${path}`, error);
}
}
});
};
// Serialize specific state branches
const serializeState = (paths) => {
const serialized = {};
paths.forEach(path => {
const value = app.getState(path);
if (value !== undefined) {
serialized[path] = value;
}
});
return serialized;
};
// Example usage
const savedState = serializeState(['user', 'preferences', 'todos']);
localStorage.setItem('appState', JSON.stringify(savedState));
// Restore on app load
const savedData = JSON.parse(localStorage.getItem('appState') || '{}');
hydrateState(savedData, ['user', 'preferences', 'todos']);
// Handle state structure changes over time
const migrateState = (currentState, fromVersion, toVersion) => {
let migrated = { ...currentState };
// Migration from v1 to v2
if (fromVersion < 2) {
// Move user.settings to user.preferences
if (migrated.user?.settings) {
migrated.user.preferences = migrated.user.settings;
delete migrated.user.settings;
}
}
// Migration from v2 to v3
if (fromVersion < 3) {
// Convert array todos to normalized structure
if (Array.isArray(migrated.todos)) {
const normalized = normalizeEntities(migrated.todos);
migrated.todos = normalized;
}
}
return migrated;
};
// Version-aware state loading
const loadStateWithMigration = () => {
const saved = JSON.parse(localStorage.getItem('appState') || '{}');
const currentVersion = 3;
const savedVersion = saved.__version__ || 1;
if (savedVersion < currentVersion) {
const migrated = migrateState(saved, savedVersion, currentVersion);
migrated.__version__ = currentVersion;
// Save migrated state
localStorage.setItem('appState', JSON.stringify(migrated));
hydrateState(migrated);
} else {
hydrateState(saved);
}
};
// Debounced state updates for rapid changes
const debouncedSetState = (() => {
const timeouts = new Map();
return (path, value, delay = 300) => {
if (timeouts.has(path)) {
clearTimeout(timeouts.get(path));
}
const timeoutId = setTimeout(() => {
app.setState(path, value);
timeouts.delete(path);
}, delay);
timeouts.set(path, timeoutId);
};
})();
// Usage: debounced search input
const handleSearchInput = (query) => {
debouncedSetState('search.query', query, 500);
};
// Throttled state updates
const throttledSetState = (() => {
const lastCall = new Map();
return (path, value, interval = 100) => {
const now = Date.now();
const last = lastCall.get(path) || 0;
if (now - last >= interval) {
app.setState(path, value);
lastCall.set(path, now);
}
};
})();
// Usage: throttled scroll position
const handleScroll = (scrollY) => {
throttledSetState('ui.scrollPosition', scrollY, 16); // ~60fps
};
// Safe state operations with error handling
const safeSetState = (path, value, fallback = null) => {
try {
app.setState(path, value);
app.setState(`errors.${path}`, null); // Clear any previous errors
} catch (error) {
console.error(`Failed to set state at ${path}:`, error);
app.setState(`errors.${path}`, error.message);
if (fallback !== null) {
app.setState(path, fallback);
}
}
};
// State recovery mechanisms
const recoverState = (path, factory) => {
const current = app.getState(path);
if (current === undefined || current === null) {
const recovered = factory();
app.setState(path, recovered);
return recovered;
}
return current;
};
// Usage examples
safeSetState('user.complexData', riskyData, {});
const todos = recoverState('todos', () => []);
const user = recoverState('user', () => ({ name: 'Guest', preferences: {} }));
// Development helpers
const inspectState = (path = '') => {
if (path) {
console.log(`State at '${path}':`, app.getState(path));
} else {
console.log('Full state tree:', app.state);
}
};
// State change logging
const logStateChanges = (enabled = true) => {
if (!enabled) return () => {};
return app.subscribe('*', (newValue, oldValue, path) => {
console.log(`๐ State change at '${path}':`, {
from: oldValue,
to: newValue,
timestamp: new Date().toISOString()
});
});
};
// State validation in development
const validateStateStructure = (expectedStructure, currentPath = '') => {
const current = currentPath ? app.getState(currentPath) : app.state;
Object.entries(expectedStructure).forEach(([key, expectedType]) => {
const fullPath = currentPath ? `${currentPath}.${key}` : key;
const value = app.getState(fullPath);
if (typeof expectedType === 'string') {
if (typeof value !== expectedType) {
console.warn(`Type mismatch at '${fullPath}': expected ${expectedType}, got ${typeof value}`);
}
} else if (typeof expectedType === 'object') {
validateStateStructure(expectedType, fullPath);
}
});
};
// Usage in development
if (process.env.NODE_ENV === 'development') {
const stateSchema = {
user: {
name: 'string',
email: 'string',
preferences: 'object'
},
todos: 'object',
ui: {
theme: 'string',
isLoading: 'boolean'
}
};
validateStateStructure(stateSchema);
const unsubscribeLogger = logStateChanges(true);
}
// React-like useState hook for components
const useCounter = (initialValue = 0) => {
const [getCount, setCount] = app.useState('counter', initialValue);
const increment = () => setCount(getCount() + 1);
const decrement = () => setCount(getCount() - 1);
const reset = () => setCount(initialValue);
return { getCount, setCount, increment, decrement, reset };
};
// Component using useState pattern
const CounterComponent = (props, context) => {
const { getCount, increment, decrement, reset } = useCounter(props.initialValue);
return {
div: {
children: () => [
{ p: { text: () => `Count: ${getCount()}` } },
{ button: { text: 'Increment', onClick: increment } },
{ button: { text: 'Decrement', onClick: decrement } },
{ button: { text: 'Reset', onClick: reset } }
]
}
};
};
// Powerful hooks with complex logic
const useCompleteFeature = (config) => {
const [getData, setData] = app.useState('feature.data', []);
const [getLoading, setLoading] = app.useState('feature.loading', false);
const processData = async () => {
setLoading(true);
// Can call external services during tracking!
const result = await app.services.dataProcessor.analyze(getData());
setData(result);
setLoading(false);
};
return { getData, setData, getLoading, processData };
};
</script>
Middleware
Transform state changes globally:
<script>
const app = new Juris({
middleware: [
// Logging middleware
({ path, oldValue, newValue }) => {
console.log(`${path}: ${oldValue} โ ${newValue}`);
return newValue;
},
// Validation middleware
({ path, newValue }) => {
if (path === 'user.age' && newValue < 0) {
console.warn('Age cannot be negative');
return 0;
}
return newValue;
},
// Auto-save middleware
({ path, newValue }) => {
if (path.startsWith('form.')) {
localStorage.setItem('formData', JSON.stringify(newValue));
}
return newValue;
}
]
});
</script>
External Subscriptions
Listen to state changes from outside components:
<script>
// Subscribe to specific paths
const unsubscribe = app.subscribe('user.isLoggedIn', (isLoggedIn) => {
if (isLoggedIn) {
initializeUserDashboard();
} else {
cleanupUserData();
}
});
// Multiple subscriptions
app.subscribe('todos', updateTodoCount);
app.subscribe('filter', refreshTodoList);
app.subscribe('user.preferences', savePreferences);
// Cleanup
unsubscribe();
</script>
๐งฉ Component System
Component Structure
<script>
const ComponentName = (props, context) => {
// props: passed from parent
// context: { setState, getState, navigate, services }
return {
tagName: {
// Attributes
className: 'my-component',
id: 'unique-id',
// Event handlers
onClick: (event, context) => {
// Handle click
},
// Content
text: 'Hello World',
// Children
children: () => [
{ span: { text: 'Child 1' } },
{ span: { text: 'Child 2' } }
]
}
};
};
</script>
Component Registration
<script>
const app = new Juris({
components: {
// Simple component
HelloWorld: () => ({
h1: { text: 'Hello, World!' }
}),
// Component with props
Greeting: (props) => ({
p: { text: `Hello, ${props.name}!` }
}),
// Reactive component
Counter: (props, { getState, setState }) => ({
div: {
children: () => [{
span: { text: () => `Count: ${getState('counter', 0)}` }
}, {
button: {
text: 'Increment',
onClick: () => setState('counter', getState('counter', 0) + 1)
}
}]
}
})
}
});
</script>
๐ฃ๏ธ Routing System
Route Configuration
<script>
const app = new Juris({
router: {
routes: {
// Simple routes
'/': 'HomePage',
'/about': 'AboutPage',
// Routes with guards
'/dashboard': {
component: 'DashboardPage',
guards: ['authGuard']
},
// Parameterized routes
'/user/:id': {
component: 'UserPage',
guards: ['authGuard'],
loadData: 'loadUserData'
}
},
guards: {
// Authentication guard
authGuard: ({ getState, navigate }) => {
if (!getState('user.isAuthenticated')) {
navigate('/login');
return false;
}
return true;
}
}
}
});
</script>
Navigation
<script>
// Programmatic navigation
app.navigate('/dashboard');
app.navigate('/user/123');
// Navigation in components
const Navigation = (props, { navigate, getState }) => ({
nav: {
children: () => [{
button: {
text: 'Dashboard',
onClick: () => navigate('/dashboard'),
disabled: () => !getState('user.isAuthenticated')
}
}]
}
});
</script>
๐ Route Guards
All 4 types of guards implemented in our demo:
1. Authentication Guard
Ensures user is logged in before accessing protected routes
2. Authorization Guard
Checks user permissions and roles for specific resources
3. Data Loading Guard
Loads required data before component renders
4. Unsaved Changes Guard
Prevents navigation when user has unsaved changes
Implementation Example
<script>
....
router: {
routes: {
'/admin': {
component: 'AdminPage',
guards: ['authGuard', 'adminGuard'],
loadData: 'loadAdminData'
}
},
guards: {
// Authentication Guard
authGuard: ({ getState, navigate }) => {
if (!getState('user.isAuthenticated')) {
navigate('/login');
return false;
}
return true;
},
// Authorization Guard
adminGuard: ({ getState, navigate }) => {
if (!getState('user.isAdmin')) {
navigate('/unauthorized');
return false;
}
return true;
},
// Data Loading Guard (Async)
loadAdminData: async ({ setState }) => {
await new Promise(resolve => setTimeout(resolve, 1000));
setState('adminData', {
users: 150,
todos: 2500,
lastUpdate: new Date().toISOString()
});
}
}
}
....
</script>
๐พ Synchronization
Our implementation includes a complete 3-tier synchronization system:
1. localStorage Sync
Automatic persistence and restoration on page reload
2. Cross-tab Sync
Real-time synchronization across browser tabs
3. Remote Server Sync
Bidirectional sync with backend API
localStorage Sync
<script>
// Automatic persistence - no code needed!
app.setState('todos', newTodos); // Automatically saved to localStorage
app.setState('user.name', 'John'); // Nested paths supported
// Skip persistence when needed
app.setState('temp', data, { skipPersist: true });
</script>
Remote Sync Configuration
<script>
// Enable remote sync
app.services.remoteSyncService.config.baseUrl = 'https://your-api.com';
app.services.remoteSyncService.setEnabled(true, {
getState: app.getState.bind(app),
setState: app.setState.bind(app)
});
// Manual sync operations
await app.services.remoteSyncService.sync({ getState, setState });
await app.services.remoteSyncService.pushToServer({ getState });
await app.services.remoteSyncService.pullFromServer({ getState, setState });
</script>
๐๏ธ Backend Setup
Complete PHP backend implementation included!
Quick Setup
- Download our PHP backend files
- Create database:
CREATE DATABASE juris_sync; </script>
- Configure database in config.php
- Upload files and run setup
API Endpoints
Endpoint | Method | Description |
---|---|---|
/api/push.php |
POST | Push local state to server |
/api/pull.php |
GET | Pull remote state from server |
/api/sync.php |
POST | Bidirectional sync with conflict resolution |
/api/admin.php |
GET | Admin interface for data management |
๐ฏ Design Patterns
Proven patterns and best practices for building applications with Juris across different use cases and architectural needs.
๐ Enhancement Patterns
Progressive enhancement for existing websites and gradual adoption strategies
๐๏ธ Architecture Patterns
Component-first, layout-driven, and feature module organization patterns
๐ง State Patterns
Flat state, domain state, computed state, and event-driven patterns
โก Performance Patterns
Surgical updates, batched state, and virtualization techniques
Pattern Selection Guide
Use Case | Recommended Patterns | Benefits |
---|---|---|
New SPA | Component-First, Layout-Driven, Flat State | Clean architecture, maintainable code |
Existing Website | Progressive Enhancement, Selective Enhancement | No breaking changes, gradual adoption |
E-commerce | Domain State, Guard-Protected, Performance | Scalable, secure, fast user experience |
Enterprise | Feature Modules, Security Patterns, Testing | Team collaboration, maintainability |
๐ Enhancement Patterns
Progressive Enhancement Pattern
Start with working HTML, enhance with JavaScript. Perfect for existing websites and CMS integration.
<!-- HTML works without JavaScript -->
<div class="status-badge">Active</div>
<script>
// Enhancement adds reactive behavior
app.enhance('.status-badge', (props, { getState }) => ({
textContent: () => getState('user.status', 'Active'),
style: {
backgroundColor: () => getState('user.status') === 'Active' ? 'green' : 'red'
}
}));
</script>
Selective Enhancement Pattern
Enhance specific elements while leaving others static - perfect for large websites where only certain sections need interactivity.
<script>
// Only enhance interactive elements
app.enhance('.interactive-button', (props, { useState }) => {
const [getClicks, setClicks] = useState('clicks', 0);
return {
onClick: () => setClicks(getClicks() + 1),
style: {
backgroundColor: () => getClicks() > 5 ? 'gold' : 'blue'
},
textContent: () => `Clicked ${getClicks()} times`
};
});
// Static content remains unchanged
// <p>This paragraph stays static</p>
</script>
Layered Enhancement Pattern
Apply multiple enhancement layers to the same elements for complex interactions.
<script>
// Base enhancement for core functionality
app.enhance('.interactive-card', (props, { useState }) => {
const [getHovered, setHovered] = useState('isHovered', false);
return {
onMouseEnter: () => setHovered(true),
onMouseLeave: () => setHovered(false),
style: {
transform: () => getHovered() ? 'scale(1.05)' : 'scale(1)',
transition: 'transform 0.2s ease'
}
};
});
// Additional enhancement for advanced features
app.enhance('.interactive-card', (props, { useState }) => {
const [getClicked, setClicked] = useState('clickCount', 0);
return {
onClick: () => setClicked(getClicked() + 1),
'data-clicks': () => getClicked()
};
});
</script>
๐๏ธ Architecture Patterns
Component-First Pattern
Build applications as collections of reusable components - ideal for new applications and design systems.
<script>
const StatusCard = (props, { useState }) => {
const [getMessage] = useState(`status.${props.id}.message`, 'No status');
return {
div: {
className: 'status-card',
style: {
backgroundColor: () => props.type === 'error' ? '#fee' : '#efe',
borderLeft: () => `4px solid ${props.type === 'error' ? 'red' : 'green'}`
},
children: () => [
{ h3: { text: props.title } },
{ p: { text: () => getMessage() } },
{
button: {
text: 'Update Status',
onClick: () => {
const newMessage = prompt('Enter new status:');
if (newMessage) {
props.onUpdate(props.id, newMessage);
}
}
}
}
]
}
};
};
</script>
Feature Module Pattern
Organize code by features rather than technical layers - perfect for large applications and team collaboration.
<script>
// User feature module
const UserModule = {
components: {
UserProfile: (props, { getState }) => ({
div: {
className: 'user-profile',
children: () => [
{ h2: { text: () => getState('user.name', 'Guest') } },
{ p: { text: () => getState('user.email', 'No email') } }
]
}
}),
UserSettings: (props, { getState, setState }) => ({
form: {
children: () => [
{
input: {
type: 'text',
value: () => getState('user.name', ''),
onInput: (e) => setState('user.name', e.target.value)
}
}
]
}
})
},
services: {
userService: {
async loadUser(id) {
// API call to load user data
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}
}
};
</script>
Micro-Frontend Pattern
Independent applications that compose together - ideal for large teams and legacy integration.
<script>
// Each team owns specific routes or features
const ShoppingCartApp = new Juris({
components: {
ShoppingCart: (props, { getState, setState }) => ({
div: {
className: 'shopping-cart',
children: () => getState('cart.items', []).map(item => ({
CartItem: { item }
}))
}
})
},
// Only manage cart-related state
states: {
'cart.items': [],
'cart.total': 0
}
});
// User profile managed by different team
const UserProfileApp = new Juris({
components: {
UserProfile: (props, { getState }) => ({
div: { /* user profile implementation */ }
})
},
states: {
'user.name': '',
'user.preferences': {}
}
});
</script>
๐ง Middleware
Transform state changes globally with middleware functions that run on every state update.
Logging Middleware
<script>
const loggingMiddleware = ({ path, oldValue, newValue }) => {
console.log(`State change: ${path}`, {
from: oldValue,
to: newValue,
timestamp: new Date().toISOString()
});
return newValue;
};
</script>
Validation Middleware
<script>
const validationMiddleware = ({ path, newValue }) => {
// Validate user age
if (path === 'user.age' && newValue < 0) {
console.warn('Age cannot be negative, setting to 0');
return 0;
}
// Validate email format
if (path === 'user.email' && newValue && !newValue.includes('@')) {
console.warn('Invalid email format');
return oldValue; // Keep previous value
}
return newValue;
};
</script>
Auto-save Middleware
<script>
const autoSaveMiddleware = ({ path, newValue }) => {
// Auto-save form data
if (path.startsWith('form.')) {
localStorage.setItem('formData', JSON.stringify(newValue));
}
// Auto-sync user preferences
if (path.startsWith('user.preferences.')) {
debounce(() => {
fetch('/api/preferences', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newValue)
});
}, 1000)();
}
return newValue;
};
</script>
Performance Monitoring Middleware
<script>
const performanceMiddleware = ({ path, oldValue, newValue }) => {
const start = performance.now();
// Process the change
const result = newValue;
const end = performance.now();
const duration = end - start;
// Log slow state changes
if (duration > 10) {
console.warn(`Slow state change detected: ${path} took ${duration}ms`);
}
return result;
};
</script>
๐ Services
Services provide reusable functionality and can be injected into components through the context.
API Service
<script>
const ApiService = {
baseUrl: 'https://api.example.com',
async get(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) throw new Error(`API Error: ${response.status}`);
return response.json();
},
async post(endpoint, data) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`API Error: ${response.status}`);
return response.json();
}
};
// Use in components
const UserList = (props, { services }) => ({
div: {
children: () => [
{
button: {
text: 'Load Users',
onClick: async () => {
try {
const users = await services.api.get('/users');
setState('users', users);
} catch (error) {
setState('error', error.message);
}
}
}
}
]
}
});
</script>
Theme Service
<script>
const ThemeService = (props, { getState, setState, subscribe }) => ({
onRegistered: () => {
// Initialize theme from localStorage
const savedTheme = localStorage.getItem('theme') || 'light';
setState('ui.theme', savedTheme);
// Apply theme to document
const applyTheme = (theme) => {
document.body.className = `theme-${theme}`;
document.body.style.backgroundColor = theme === 'dark' ? '#1a1a1a' : '#ffffff';
document.body.style.color = theme === 'dark' ? '#ffffff' : '#000000';
};
// Apply initial theme
applyTheme(savedTheme);
// Watch for theme changes
const unsubscribe = subscribe('ui.theme', (newTheme) => {
localStorage.setItem('theme', newTheme);
applyTheme(newTheme);
});
return unsubscribe; // Cleanup function
}
});
</script>
Notification Service
<script>
const NotificationService = {
show(message, type = 'info', duration = 3000) {
const notification = {
id: Date.now(),
message,
type,
timestamp: new Date()
};
// Add to notifications array
const current = app.getState('ui.notifications', []);
app.setState('ui.notifications', [...current, notification]);
// Auto-remove after duration
setTimeout(() => {
this.remove(notification.id);
}, duration);
return notification.id;
},
remove(id) {
const current = app.getState('ui.notifications', []);
app.setState('ui.notifications', current.filter(n => n.id !== id));
},
clear() {
app.setState('ui.notifications', []);
}
};
</script>
๐ก State Subscriptions
Listen to state changes from outside components for cross-cutting concerns and external integrations.
Basic Subscriptions
<script>
// Subscribe to authentication changes
const authUnsubscribe = app.subscribe('user.isAuthenticated', (isLoggedIn) => {
if (isLoggedIn) {
console.log('User logged in, initializing dashboard');
initializeUserDashboard();
} else {
console.log('User logged out, cleaning up');
cleanupUserData();
}
});
// Subscribe to theme changes
const themeUnsubscribe = app.subscribe('ui.theme', (theme) => {
document.body.className = `theme-${theme}`;
localStorage.setItem('preferred-theme', theme);
});
// Cleanup subscriptions
// authUnsubscribe();
// themeUnsubscribe();
</script>
Complex Subscription Patterns
<script>
// Subscribe to multiple related paths
const subscriptions = [];
// Cart total calculation
subscriptions.push(
app.subscribe('cart.items', (items) => {
const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
app.setState('cart.total', total);
})
);
// Save draft automatically
let saveTimer;
subscriptions.push(
app.subscribe('editor.content', (content) => {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
localStorage.setItem('draft', content);
app.setState('editor.lastSaved', new Date().toISOString());
}, 2000); // Debounce saves
})
);
// Analytics tracking
subscriptions.push(
app.subscribe('router.currentRoute', (route) => {
// Track page views
if (typeof gtag !== 'undefined') {
gtag('config', 'GA_TRACKING_ID', {
page_path: route
});
}
})
);
// Cleanup all subscriptions
const cleanup = () => subscriptions.forEach(unsub => unsub());
</script>
Conditional Subscriptions
<script>
// Only subscribe when user is authenticated
let analyticsSubscription = null;
app.subscribe('user.isAuthenticated', (isAuthenticated) => {
if (isAuthenticated && !analyticsSubscription) {
// Start tracking when logged in
analyticsSubscription = app.subscribe('user.actions', (action) => {
trackUserAction(action);
});
} else if (!isAuthenticated && analyticsSubscription) {
// Stop tracking when logged out
analyticsSubscription();
analyticsSubscription = null;
}
});
</script>
โก Performance Optimization
Surgical Update Pattern
Juris's default behavior - only update specific elements that changed, not entire components.
<script>
// Traditional framework: Entire list re-renders when any item changes
// Juris: Only the specific item that changed updates
app.enhance('.todo-item', (props, { getState }) => {
const id = props.element.dataset.id;
return {
className: () => {
const todo = getState(`todos.${id}`);
return `todo-item ${todo?.completed ? 'completed' : 'active'}`;
},
style: () => {
const todo = getState(`todos.${id}`);
return {
backgroundColor: todo?.priority === 'high' ? '#fee' : '#fff',
borderLeft: `4px solid ${todo?.completed ? 'green' : 'orange'}`
};
}
};
});
</script>
Batched State Updates
<script>
// Group multiple state changes together
const updateUserProfile = (userData) => {
// Collect all changes
const updates = {
'user.name': userData.name,
'user.email': userData.email,
'user.preferences.theme': userData.theme,
'user.lastUpdated': new Date().toISOString()
};
// Apply as single batch
Object.entries(updates).forEach(([path, value]) => {
app.setState(path, value);
});
};
</script>
Lazy Loading Pattern
<script>
const LazyComponent = (props, { getState, setState }) => {
const loadComponent = async () => {
if (!getState('lazyData.loaded')) {
setState('lazyData.loading', true);
try {
const data = await import('./heavy-component.js');
setState('lazyData.component', data.default);
setState('lazyData.loaded', true);
} catch (error) {
setState('lazyData.error', error.message);
} finally {
setState('lazyData.loading', false);
}
}
};
return {
div: {
children: () => {
if (getState('lazyData.loading')) {
return [{ div: { text: 'Loading...' } }];
}
if (getState('lazyData.error')) {
return [{ div: { text: `Error: ${getState('lazyData.error')}` } }];
}
const Component = getState('lazyData.component');
return Component ? [{ Component: props }] : [{
button: {
text: 'Load Component',
onClick: loadComponent
}
}];
}
}
};
};
</script>
Virtual Scrolling for Large Lists
<script>
const VirtualList = (props, { getState, setState }) => {
const itemHeight = 50;
const visibleCount = Math.ceil(props.height / itemHeight);
return {
div: {
style: {
height: `${props.height}px`,
overflow: 'auto'
},
onScroll: (e) => {
const scrollTop = e.target.scrollTop;
const startIndex = Math.floor(scrollTop / itemHeight);
setState('virtualList.startIndex', startIndex);
},
children: () => {
const items = getState('virtualList.items', []);
const startIndex = getState('virtualList.startIndex', 0);
const endIndex = Math.min(startIndex + visibleCount, items.length);
const visibleItems = items.slice(startIndex, endIndex).map((item, i) => ({
div: {
key: startIndex + i,
style: {
height: `${itemHeight}px`,
transform: `translateY(${(startIndex + i) * itemHeight}px)`
},
text: item.name
}
}));
return visibleItems;
}
}
};
};
</script>
๐ก๏ธ Security Patterns
XSS Prevention
Juris provides built-in protection against cross-site scripting attacks through safe content handling.
<script>
// Juris automatically sanitizes content
const UserContent = (props, { getState }) => ({
div: {
// Safe: Juris escapes HTML automatically
text: () => getState('user.input'), // <script>
becomes <script>
// Dangerous: Only use innerHTML with trusted content
innerHTML: () => {
const content = getState('user.input');
// Always sanitize user input before using innerHTML
return sanitizeHTML(content);
}
}
});
// Simple HTML sanitizer
const sanitizeHTML = (html) => {
const div = document.createElement('div');
div.textContent = html; // This escapes HTML
return div.innerHTML;
};
</script>
Input Validation Pattern
<script>
const SecureForm = (props, { getState, setState }) => {
const validateEmail = (email) => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
};
const validateInput = (field, value) => {
const errors = getState('form.errors', {});
switch (field) {
case 'email':
if (!validateEmail(value)) {
errors.email = 'Please enter a valid email address';
} else {
delete errors.email;
}
break;
case 'password':
if (value.length < 8) {
errors.password = 'Password must be at least 8 characters';
} else {
delete errors.password;
}
break;
}
setState('form.errors', errors);
};
return {
form: {
onSubmit: (e) => {
e.preventDefault();
// Validate all fields before submission
const email = getState('form.email');
const password = getState('form.password');
validateInput('email', email);
validateInput('password', password);
const errors = getState('form.errors', {});
if (Object.keys(errors).length === 0) {
// Submit form securely
submitForm({ email, password });
}
},
children: () => [
{
input: {
type: 'email',
value: () => getState('form.email', ''),
onInput: (e) => {
setState('form.email', e.target.value);
validateInput('email', e.target.value);
}
}
},
{
div: {
className: 'error',
text: () => getState('form.errors.email', ''),
style: {
display: () => getState('form.errors.email') ? 'block' : 'none'
}
}
}
]
}
};
};
</script>
Authentication Pattern
<script>
const AuthService = {
async login(credentials) {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) {
throw new Error('Login failed');
}
const data = await response.json();
// Store token securely
localStorage.setItem('auth_token', data.token);
// Update application state
app.setState('user.isAuthenticated', true);
app.setState('user.data', data.user);
return data;
} catch (error) {
app.setState('auth.error', error.message);
throw error;
}
},
logout() {
localStorage.removeItem('auth_token');
app.setState('user.isAuthenticated', false);
app.setState('user.data', null);
app.navigate('/');
},
getToken() {
return localStorage.getItem('auth_token');
},
// Add token to API requests
async authenticatedRequest(url, options = {}) {
const token = this.getToken();
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
}
};
</script>
๐งช Testing Patterns
Component Testing
<script>
// Test utility functions
const createTestApp = (initialState = {}) => {
return new Juris({
states: initialState,
components: {},
services: {}
});
};
const renderComponent = (Component, props = {}, context = {}) => {
const mockContext = {
getState: jest.fn().mockReturnValue(undefined),
setState: jest.fn(),
navigate: jest.fn(),
services: {},
...context
};
return Component(props, mockContext);
};
// Example test
describe('UserProfile Component', () => {
test('renders user name from state', () => {
const mockContext = {
getState: jest.fn((path) => {
if (path === 'user.name') return 'John Doe';
return undefined;
}),
setState: jest.fn(),
navigate: jest.fn(),
services: {}
};
const component = UserProfile({}, mockContext);
const result = component.div.children()[0];
expect(result.h2.text()).toBe('John Doe');
});
test('handles missing user data gracefully', () => {
const mockContext = {
getState: jest.fn().mockReturnValue(undefined),
setState: jest.fn(),
navigate: jest.fn(),
services: {}
};
const component = UserProfile({}, mockContext);
const result = component.div.children()[0];
expect(result.h2.text()).toBe('Guest');
});
});
</script>
Enhancement Testing
<script>
// Test enhancement behavior
describe('Button Enhancement', () => {
let app, button;
beforeEach(() => {
// Create test HTML
document.body.innerHTML = `
<button class="test-button" data-count="0">Click me</button>
`;
button = document.querySelector('.test-button');
// Create test app
app = createTestApp({ count: 0 });
// Apply enhancement
app.enhance('.test-button', (props, { getState, setState }) => ({
textContent: () => `Clicked ${getState('count', 0)} times`,
onClick: () => setState('count', getState('count', 0) + 1)
}));
});
test('updates text content when state changes', () => {
app.setState('count', 5);
expect(button.textContent).toBe('Clicked 5 times');
});
test('handles click events', () => {
button.click();
expect(app.getState('count')).toBe(1);
expect(button.textContent).toBe('Clicked 1 times');
});
});
</script>
Integration Testing
<script>
// Test complete application flows
describe('Todo Application', () => {
let app;
beforeEach(() => {
app = new Juris({
states: { todos: [] },
components: { TodoApp, TodoItem },
router: {
routes: {
'/': 'TodoApp'
}
}
});
app.render('#test-container');
});
test('complete todo workflow', async () => {
// Add a todo
const input = document.querySelector('.todo-input');
const addButton = document.querySelector('.add-todo');
input.value = 'Test todo';
input.dispatchEvent(new Event('input'));
addButton.click();
// Verify todo was added
expect(app.getState('todos')).toHaveLength(1);
expect(app.getState('todos')[0].text).toBe('Test todo');
// Toggle completion
const toggleButton = document.querySelector('.todo-toggle');
toggleButton.click();
expect(app.getState('todos')[0].completed).toBe(true);
// Delete todo
const deleteButton = document.querySelector('.todo-delete');
deleteButton.click();
expect(app.getState('todos')).toHaveLength(0);
});
});
</script>
๐ Deployment
Static Deployment
Deploy Juris applications as static files with no server requirements.
๐ฆ Build Process
No build step required - deploy JavaScript files directly
๐ CDN Distribution
Serve from CDNs for global performance
๐ฐ Cost Effective
Static hosting costs significantly less than server hosting
โก High Availability
Static files provide better uptime and reliability
Deployment Checklist
- Minify JavaScript files - Reduce file sizes for production
- Configure HTTPS - Required for service workers and secure features
- Set up CDN - Improve global performance
- Configure caching headers - Optimize repeat visits
- Test on target devices - Verify functionality across platforms
- Monitor performance - Track real-world usage metrics
Production Configuration
<script>
// Production app configuration
const app = new Juris({
// Disable debug logging in production
debug: false,
// Production error handling
errorHandler: (error) => {
// Log to external service
console.error('Production error:', error);
// Show user-friendly message
app.setState('ui.error', 'Something went wrong. Please try again.');
},
// Production middleware
middleware: [
// Only include essential middleware in production
validationMiddleware,
errorHandlingMiddleware
],
// Production services
services: {
api: {
baseUrl: 'https://api.yoursite.com',
timeout: 10000
}
}
});
</script>
Environment Configuration
<script>
// Environment-specific configuration
const config = {
development: {
apiUrl: 'http://localhost:3000/api',
debug: true,
logLevel: 'verbose'
},
staging: {
apiUrl: 'https://staging-api.yoursite.com',
debug: true,
logLevel: 'info'
},
production: {
apiUrl: 'https://api.yoursite.com',
debug: false,
logLevel: 'error'
}
};
// Detect environment
const environment = window.location.hostname.includes('localhost')
? 'development'
: window.location.hostname.includes('staging')
? 'staging'
: 'production';
const currentConfig = config[environment];
</script>
Progressive Deployment
<script>
// Feature flags for gradual rollout
const FeatureFlags = {
newDashboard: window.location.search.includes('beta=true'),
experimentalFeatures: localStorage.getItem('experimental') === 'true',
// Server-controlled flags (loaded from API)
async loadServerFlags() {
try {
const response = await fetch('/api/feature-flags');
const flags = await response.json();
Object.assign(this, flags);
} catch (error) {
console.warn('Failed to load feature flags:', error);
}
}
};
// Use feature flags in components
const Dashboard = (props, { getState }) => ({
div: {
children: () => FeatureFlags.newDashboard
? [{ NewDashboard: {} }]
: [{ LegacyDashboard: {} }]
}
});
</script>
Monitoring and Analytics
<script>
// Production monitoring
const MonitoringService = {
// Performance monitoring
trackPageLoad() {
const loadTime = performance.timing.loadEventEnd - performance.timing.navigationStart;
this.track('page_load_time', { duration: loadTime });
},
// Error tracking
trackError(error) {
this.track('error', {
message: error.message,
stack: error.stack,
url: window.location.href,
userAgent: navigator.userAgent
});
},
// User interaction tracking
trackUserAction(action, data = {}) {
this.track('user_action', {
action,
...data,
timestamp: new Date().toISOString()
});
},
// Generic tracking function
track(event, data) {
// Send to analytics service
if (typeof gtag !== 'undefined') {
gtag('event', event, data);
}
// Send to custom analytics
fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event, data })
}).catch(console.error);
}
};
</script>
๐ API Reference
Juris Class
Constructor
<script>
new Juris(config)
</script>
Config options:
states
- Initial state objectcomponents
- Component definitionsservices
- Service definitionsmiddleware
- State middleware functionsrouter
- Router configurationlayout
- Root layout component
Methods
State Management
<script>
setState(path, value, context?)
getState(path, defaultValue?)
subscribe(path, callback)
</script>
Component Management
<script>
registerComponent(name, componentFn)
render(container?)
</script>
Navigation
<script>
navigate(path)
getCurrentRoute()
</script>
Component Context
Components receive a context object with:
<script>
{
setState: (path, value, context?) => void,
getState: (path, defaultValue?) => any,
navigate: (path) => void,
services: object
}
</script>
๐ Advanced Router
The Juris Router provides enterprise-grade routing capabilities including parameter validation, query string handling, route guards, lazy loading, transitions, and nested routing.
Routing Modes
๐ Hash Mode (Default)
Uses URL fragments for routing - works without server configuration
๐ History Mode
Clean URLs using HTML5 History API - requires server configuration
๐พ Memory Mode
In-memory routing for testing and server-side rendering
// Router mode configuration
const app = new Juris({
router: {
mode: 'history', // 'hash' | 'history' | 'memory'
base: '/app', // Base path for history mode
scrollBehavior: 'top', // 'top' | 'none' | 'maintain' | function
transitions: true // Enable route transitions
}
});
// Hash mode URLs: /#/users/123
// History mode URLs: /app/users/123
// Memory mode: No URL changes
Advanced Route Definition
routes: {
// Simple routes
'/': 'HomePage',
'/about': 'AboutPage',
// Advanced route configuration
'/users/:id': {
component: 'UserPage',
meta: { title: 'User Profile', requiresAuth: true },
params: {
id: {
type: 'number',
required: true,
min: 1,
max: 999999
}
},
query: {
tab: { type: 'string', enum: ['profile', 'settings', 'activity'] },
page: { type: 'number', default: 1, min: 1 }
},
guards: ['authGuard', 'userAccessGuard'],
loadData: 'loadUserData',
transitions: { enter: 'fadeIn', leave: 'fadeOut' }
},
// Pattern types
'/products/:category?': 'ProductPage', // Optional parameter
'/admin/*': 'AdminPage', // Wildcard routes
'/posts/:id/comments/:commentId': 'CommentPage', // Multiple parameters
'RegExp:^/files/(.+)\\.(jpg|png|gif)$': 'ImageViewer' // Custom regex
}
Parameter Validation & Types
Comprehensive parameter validation with multiple data types and constraints:
Number Parameters
params: {
id: {
type: 'number',
required: true,
min: 1,
max: 999999,
default: 1
},
page: {
type: 'number',
min: 1,
max: 1000,
default: 1
}
}
String Parameters
params: {
slug: {
type: 'string',
pattern: /^[a-z0-9-]+$/,
minLength: 3,
maxLength: 50,
required: true
},
category: {
type: 'string',
enum: ['electronics', 'books', 'clothing'],
optional: true
}
}
Boolean & Date Parameters
params: {
published: {
type: 'boolean',
default: false
},
date: {
type: 'date',
required: true,
min: new Date('2020-01-01'),
max: new Date('2030-12-31')
}
}
Query Parameters
Handle complex query strings with validation and type conversion:
'/search': {
component: 'SearchPage',
query: {
q: { type: 'string', required: true, minLength: 2 },
page: { type: 'number', default: 1, min: 1, max: 100 },
sort: {
type: 'string',
enum: ['date', 'name', 'relevance'],
default: 'relevance'
},
filters: { type: 'array' }, // For multiple values
published: { type: 'boolean', default: true }
}
}
// Usage in components
const SearchPage = (props, { getState }) => {
const query = getState('router.query', {});
return {
div: {
children: () => [
{ h1: { text: `Search: "${query.q}"` } },
{ p: { text: `Page ${query.page} - Sort by ${query.sort}` } },
{ p: { text: `Filters: ${query.filters?.join(', ') || 'None'}` } }
]
}
};
};
Navigation & Route Building
// Programmatic navigation with options
context.navigate('/users/123', {
replace: true, // Replace current history entry
query: { tab: 'profile' }, // Add query parameters
state: { fromDashboard: true } // History state
});
// Build routes with parameters
const userRoute = app.buildRoute('/users/:id', { id: 123 });
// Result: "/users/123"
// Build routes with query parameters
const searchRoute = app.buildRoute('/search', {}, {
q: 'javascript',
page: 2,
filters: ['recent', 'popular']
});
// Result: "/search?q=javascript&page=2&filters[]=recent&filters[]=popular"
// Navigation with return URL
context.navigate(`/login?returnUrl=${encodeURIComponent(currentRoute)}`);
Router Components
RouterLink Component
// Navigation links with automatic active state
{
RouterLink: {
to: '/users/123', // Target route
params: { id: 123 }, // Route parameters
query: { tab: 'profile' }, // Query parameters
exact: false, // Exact match for active state
activeClass: 'router-link-active', // CSS class for active state
replace: false, // Replace history entry
text: 'View User Profile', // Link text
onClick: (e) => { // Custom click handler
// Additional logic before navigation
}
}
}
Router Component with Loading States
{
Router: {
loadingComponent: 'LoadingPage', // Component for loading state
errorComponent: 'ErrorPage', // Component for errors
notFoundComponent: 'NotFoundPage' // Component for 404s
}
}
Lazy Loading
Load components dynamically for better performance:
router: {
lazy: {
AdminPage: {
loader: () => import('./components/AdminPage.js')
},
UserDashboard: {
loader: async () => {
const module = await import('./components/UserDashboard.js');
return module.UserDashboard;
}
},
HeavyComponent: {
loader: () => import('./components/HeavyComponent.js'),
placeholder: { div: { text: 'Loading heavy component...' } }
}
}
}
// Route configuration with lazy loading
routes: {
'/admin': {
component: 'AdminPage', // Will be loaded lazily
guards: ['authGuard', 'adminGuard']
}
}
Route Data Loading
routes: {
'/users/:id': {
component: 'UserPage',
loadData: async (context) => {
try {
context.setState('router.isLoading', true);
// Load user data
const userId = context.params.id;
const userData = await fetchUser(userId);
context.setState('user.current', userData);
// Load related data
const posts = await fetchUserPosts(userId);
context.setState('user.posts', posts);
} catch (error) {
context.setState('router.error', 'Failed to load user data');
} finally {
context.setState('router.isLoading', false);
}
}
}
}
Route Transitions
Add smooth transitions between routes:
// Global transition configuration
router: {
transitions: true,
transitionDuration: 300
}
// Per-route transitions
routes: {
'/dashboard': {
component: 'DashboardPage',
transitions: {
enter: 'slideInRight',
leave: 'slideOutLeft',
duration: 400
}
},
'/profile': {
component: 'ProfilePage',
transitions: {
enter: 'fadeIn',
leave: 'fadeOut',
duration: 250
}
}
}
// CSS for transitions
/*
.slideInRight {
animation: slideInRight 0.3s ease-out;
}
.slideOutLeft {
animation: slideOutLeft 0.3s ease-out;
}
@keyframes slideInRight {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
@keyframes slideOutLeft {
from { transform: translateX(0); }
to { transform: translateX(-100%); }
}
*/
Nested Routing
Create complex hierarchical routing structures:
routes: {
'/admin': {
component: 'AdminLayout',
guards: ['authGuard', 'adminGuard'],
children: {
'': { component: 'AdminDashboard' }, // /admin
'users': { component: 'AdminUsers' }, // /admin/users
'users/:id': {
component: 'AdminUserDetail', // /admin/users/123
params: { id: { type: 'number', required: true } }
},
'settings': {
component: 'AdminSettings', // /admin/settings
children: {
'general': { component: 'GeneralSettings' },
'security': { component: 'SecuritySettings' }
}
}
}
}
}
// AdminLayout component with RouterOutlet
const AdminLayout = (props, context) => ({
div: {
className: 'admin-layout',
children: () => [
{ AdminSidebar: {} },
{
div: {
className: 'admin-content',
children: [{ RouterOutlet: {} }] // Renders child routes
}
}
]
}
});
Route Middleware
Apply logic to multiple routes with middleware:
router: {
middleware: [
{
path: '/admin/*',
guard: async (context) => {
// Log admin access
console.log('Admin area accessed:', context.route, context.user);
// Track analytics
if (typeof gtag !== 'undefined') {
gtag('event', 'admin_access', {
page: context.route,
user_id: context.getState('auth.user.id')
});
}
return true;
}
},
{
path: '/api/*',
guard: async (context) => {
// Add API token to headers
const token = context.getState('auth.token');
if (token) {
context.headers = { Authorization: `Bearer ${token}` };
}
return true;
}
}
]
}
Redirects and Aliases
routes: {
// Redirects
'/old-dashboard': {
redirectTo: '/dashboard'
},
'/legacy/:id': {
redirectTo: '/users/:id' // Parameter forwarding
},
// Aliases - multiple URLs for same component
'/users/:id': {
component: 'UserPage',
alias: ['/profile/:id', '/member/:id', '/u/:id']
},
// Conditional redirects
'/home': {
component: 'HomePage',
beforeEnter: (context) => {
if (!context.getState('auth.isAuthenticated')) {
context.navigate('/welcome');
return false;
}
return true;
}
}
}
Advanced Scroll Behavior
router: {
scrollBehavior: (to, from, savedPosition) => {
// Return to saved position (browser back/forward)
if (savedPosition) {
return savedPosition;
}
// Scroll to anchor
if (to.hash) {
const element = document.querySelector(to.hash);
if (element) {
return {
x: 0,
y: element.offsetTop - 80 // Account for fixed header
};
}
}
// Scroll to top for new pages
if (to.path !== from.path) {
return { x: 0, y: 0 };
}
// Maintain scroll position for same page
return false;
}
}
Router State Management
The router maintains comprehensive state information:
// Router state structure
{
router: {
currentRoute: '/users/123', // Current route path
previousRoute: '/dashboard', // Previous route
params: { id: 123 }, // Route parameters
query: { tab: 'profile', page: 1 }, // Query parameters
hash: 'section1', // URL hash
isLoading: false, // Loading state
error: null, // Error message
notFound: false, // 404 state
transition: { // Transition state
isTransitioning: false,
direction: 'forward'
}
}
}
// Accessing router state in components
const NavigationInfo = (props, { getState }) => ({
div: {
children: () => [
{ p: { text: () => `Current: ${getState('router.currentRoute')}` } },
{ p: { text: () => `Previous: ${getState('router.previousRoute', 'None')}` } },
{
p: {
text: () => {
const params = getState('router.params', {});
return `Params: ${JSON.stringify(params)}`;
}
}
}
]
}
});
// Subscribe to router changes
app.subscribe('router.currentRoute', (newRoute, oldRoute) => {
console.log(`Route changed from ${oldRoute} to ${newRoute}`);
// Update page title
const routeConfig = app.getRouteConfig(newRoute);
if (routeConfig?.meta?.title) {
document.title = `${routeConfig.meta.title} - My App`;
}
});
Complete E-commerce Example
const app = new Juris({
router: {
mode: 'history',
base: '/shop',
guards: {
authGuard: async (context) => {
if (!context.getState('auth.isAuthenticated')) {
const returnUrl = encodeURIComponent(context.route);
context.navigate(`/login?returnUrl=${returnUrl}`);
return false;
}
return true;
}
},
routes: {
'/': 'HomePage',
// Product catalog with filtering
'/products': {
component: 'ProductListPage',
query: {
category: {
type: 'string',
enum: ['electronics', 'books', 'clothing'],
optional: true
},
brand: { type: 'string', optional: true },
sort: {
type: 'string',
enum: ['price-asc', 'price-desc', 'name', 'rating'],
default: 'name'
},
page: { type: 'number', min: 1, default: 1 },
limit: { type: 'number', min: 10, max: 100, default: 20 },
priceMin: { type: 'number', min: 0, optional: true },
priceMax: { type: 'number', min: 0, optional: true },
inStock: { type: 'boolean', default: true }
},
loadData: async (context) => {
const query = context.query;
const products = await fetchProducts(query);
context.setState('products.list', products);
context.setState('products.totalCount', products.total);
}
},
// Product details
'/products/:id': {
component: 'ProductDetailPage',
params: {
id: { type: 'number', required: true, min: 1 }
},
loadData: async (context) => {
const product = await fetchProduct(context.params.id);
const reviews = await fetchProductReviews(context.params.id);
context.setState('product.current', product);
context.setState('product.reviews', reviews);
}
},
// Shopping cart
'/cart': {
component: 'CartPage',
loadData: async (context) => {
const cartItems = await fetchCartItems();
context.setState('cart.items', cartItems);
}
},
// Checkout process
'/checkout': {
component: 'CheckoutLayout',
guards: ['authGuard'],
children: {
'': { redirectTo: '/checkout/shipping' },
'shipping': { component: 'ShippingStep' },
'payment': {
component: 'PaymentStep',
guards: ['shippingCompleteGuard']
},
'review': {
component: 'ReviewStep',
guards: ['paymentCompleteGuard']
},
'confirmation/:orderId': {
component: 'ConfirmationStep',
params: {
orderId: { type: 'string', required: true }
}
}
}
},
// User account
'/account': {
component: 'AccountLayout',
guards: ['authGuard'],
children: {
'': { redirectTo: '/account/profile' },
'profile': { component: 'ProfilePage' },
'orders': {
component: 'OrdersPage',
query: {
status: {
type: 'string',
enum: ['all', 'pending', 'shipped', 'delivered'],
default: 'all'
},
page: { type: 'number', min: 1, default: 1 }
}
},
'orders/:orderId': {
component: 'OrderDetailPage',
params: {
orderId: { type: 'string', required: true }
}
},
'addresses': { component: 'AddressesPage' },
'payment-methods': { component: 'PaymentMethodsPage' }
}
}
}
}
});
Router API Reference
๐ navigate(path, options)
Navigate to a route programmatically with options like replace, query, and state
๐๏ธ buildRoute(pattern, params, query)
Build route URLs with parameters and query strings
๐ matchRoute(path)
Match a path against route patterns and return match details
๐ getCurrentRoute()
Get the current route path and state information
Best Practices
- Parameter Validation - Always validate route parameters with appropriate types and constraints
- Error Handling - Implement proper error handling in route guards and data loading
- Loading States - Show loading indicators during async route operations
- Nested Routes - Use nested routing for complex application structures
- SEO Friendly - Use history mode and proper meta tags for better SEO
- Performance - Implement lazy loading for heavy components
๐ก Examples
Complete Todo Application
Download and run our complete implementation with all features demonstrated:
โ Todo CRUD operations
Add, edit, delete todos with reactive UI
โ Advanced routing
Authentication and parameterized routes
โ Multi-tier synchronization
localStorage + cross-tab + remote server
โ User authentication
Role-based access control system
Testing Our Demo
Try these scenarios:
- Basic Todo Operations: Add, edit, delete todos; toggle completion status; filter by status
- Authentication Flow: Login with different user types (
admin
,user
,newuser
) - Multi-tier Sync: Open multiple tabs and test cross-tab synchronization
- Route Guards: Test access controls and data loading states
Demo Accounts
admin
Has admin access to all routes
user
Regular user with completed profile
newuser
User without profile (blocked from profile routes)
๐ฏ Best Practices
State Management
- Keep state flat - Avoid deep nesting when possible
- Use descriptive paths -
user.profile.name
instead ofuser.data.n
- Normalize data - Store arrays as objects with IDs as keys for easier updates
- Use middleware - For cross-cutting concerns like logging and validation
Components
- Keep components pure - Same props should always render the same output
- Use reactive functions - For dynamic content that depends on state
- Avoid side effects - Use services for API calls and complex logic
- Compose components - Build complex UIs from simple, reusable components
Routing
- Use descriptive routes -
/user/:id/posts/:postId
is better than/u/:i/p/:p
- Implement proper guards - Always validate permissions and load required data
- Handle loading states - Show loading indicators during async operations
- Plan for errors - Implement error boundaries and fallback routes
Performance
- Minimize re-renders - Use specific state paths in
getState
- Lazy load data - Load data in route guards or component lifecycle
- Debounce inputs - For search and filter inputs
- Use keys for lists - Help framework efficiently update list items
Security
- Validate all inputs - Both client and server side
- Implement proper authentication - Use secure tokens and validation
- Use HTTPS - Always use secure connections for remote sync
- Sanitize data - Prevent XSS attacks with proper data sanitization
๐ค Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Setup
# Clone the repository
git clone https://github.com/jurisjs/juris.git
cd juris-framework
# Install dependencies (if any)
npm install
# Run tests
npm test
# Start development server
npm run dev
</script>
Reporting Issues
Please use the GitHub Issues page to report bugs or request features.
Code Style
- Use ES6+ features
- Follow JSDoc conventions for documentation
- Write tests for new features
- Keep functions small and focused
๐ License
MIT License - see the LICENSE file for details
๐จโ๐ป Author
Resti Guay - Version 0.0.1
๐ฌ Support
GitHub Issues and Discussions available
๐ Acknowledgments
Inspired by modern reactive frameworks, built with vanilla JavaScript