DEV Community

Pinoy Codie
Pinoy Codie

Posted on • Edited on

Alpine.js to Juris.js Migration: Pure, Debuggable, Islandable Solution

Migrating from Alpine's template-based approach to Juris's pure JavaScript, debuggable, and islandable architecture


Why Migrate from Alpine.js to Juris.js?

Alpine.js is excellent for sprinkling interactivity into HTML, but Juris.js offers compelling advantages for modern development:

Alpine.js Limitations:

  • Template-bound logic - Business logic mixed with HTML attributes
  • Debugging challenges - Hard to debug x-data and x-show in DevTools
  • No true islands - Global Alpine state affects entire page
  • Limited composability - Difficult to share logic between components
  • Opaque reactivity - Can't easily inspect state changes

Juris.js Advantages:

  • Pure JavaScript - All logic in debuggable JS functions
  • True islands - Each enhancement is completely isolated
  • Transparent state - Clear state management with full debugging support
  • Service injection - Clean dependency injection pattern
  • Selector targeting - Precise DOM targeting without attribute pollution

Migration Philosophy: Islands over Globals

Alpine.js Global Approach:

<!-- Everything shares global Alpine context --> <div x-data="{ count: 0 }"> <button x-on:click="count++">Count: <span x-text="count"></span></button> </div> <div x-data="{ name: '' }"> <input x-model="name" placeholder="Name"> <p x-text="name"></p> </div> 
Enter fullscreen mode Exit fullscreen mode

Juris.js Island Approach:

<!-- Each island is independent and debuggable --> <div class="counter-island"> <button>Count: <span class="count-display">0</span></button> </div> <div class="name-island"> <input class="name-input" placeholder="Name"> <p class="name-display"></p> </div> <script src="https://unpkg.com/juris@0.5.2/juris.js"></script> <script> // Island 1: Counter (completely isolated) const counterJuris = new Juris({ states: { count: 0 } }); counterJuris.enhance('.counter-island button', { onclick: () => { const current = counterJuris.getState('count'); counterJuris.setState('count', current + 1); } }); counterJuris.enhance('.count-display', { text: () => counterJuris.getState('count') }); // Island 2: Name input (completely isolated)  const nameJuris = new Juris({ states: { name: '' } }); nameJuris.enhance('.name-input', { oninput: (e) => nameJuris.setState('name', e.target.value) }); nameJuris.enhance('.name-display', { text: () => nameJuris.getState('name') }); </script> 
Enter fullscreen mode Exit fullscreen mode

Key Benefits:

  • Debuggable: Each island can be inspected via counterJuris.getState('count')
  • Isolated: Islands don't affect each other
  • Pure JS: All logic in standard JavaScript functions
  • No attribute pollution: Clean HTML without x- attributes

Step 1: Understanding the Fundamental Differences

Alpine.js Reactive Model:

<div x-data="{ items: [], newItem: '', addItem() { this.items.push({ id: Date.now(), text: this.newItem }); this.newItem = ''; } }"> <input x-model="newItem" placeholder="Add item"> <button x-on:click="addItem()">Add</button> <template x-for="item in items"> <div x-text="item.text"></div> </template> </div> 
Enter fullscreen mode Exit fullscreen mode

Juris.js Reactive Model:

<div class="todo-island"> <input class="new-item" placeholder="Add item"> <button class="add-button">Add</button> <div class="items-container"></div> </div> <script> const todoIsland = new Juris({ states: { items: [], newItem: '' }, services: { todoService: { addItem: () => { const newItem = todoIsland.getState('newItem'); const items = todoIsland.getState('items'); if (newItem.trim()) { todoIsland.setState('items', [ ...items, { id: Date.now(), text: newItem } ]); todoIsland.setState('newItem', ''); } } } } }); // Pure, debuggable enhancements todoIsland.enhance('.new-item', { value: () => todoIsland.getState('newItem'), oninput: (e) => todoIsland.setState('newItem', e.target.value) }); todoIsland.enhance('.add-button', { onclick: ({ todoService }) => todoService.addItem() }); todoIsland.enhance('.items-container', { children: () => { const items = todoIsland.getState('items'); return items.map(item => ({ div: { text: item.text, key: item.id } })); } }); </script> 
Enter fullscreen mode Exit fullscreen mode

Debugging Comparison:

// Alpine.js debugging (limited) // Have to use Alpine.store() or inspect $data in browser console.log('Alpine state:', document.querySelector('[x-data]').__alpine__.$data); // Juris.js debugging (transparent) console.log('Todo state:', todoIsland.getState('todos')); console.log('Items:', todoIsland.getState('items')); console.log('New item:', todoIsland.getState('newItem')); // Live state inspection todoIsland.subscribe('items', (items) => { console.log('Items changed:', items); }); 
Enter fullscreen mode Exit fullscreen mode

Step 2: Migration Patterns by Use Case

2.1 Simple Toggle Components

Alpine.js Version:

<div x-data="{ open: false }"> <button x-on:click="open = !open"> <span x-text="open ? 'Close' : 'Open'"></span> </button> <div x-show="open" x-transition> <p>Content is now visible!</p> </div> </div> 
Enter fullscreen mode Exit fullscreen mode

Juris.js Island Version:

<div class="toggle-island"> <button class="toggle-button">Open</button> <div class="toggle-content" style="display: none;"> <p>Content is now visible!</p> </div> </div> <script> const toggleIsland = new Juris({ states: { open: false } }); toggleIsland.enhance('.toggle-button', { text: () => toggleIsland.getState('open') ? 'Close' : 'Open', onclick: () => { const current = toggleIsland.getState('open'); toggleIsland.setState('open', !current); } }); toggleIsland.enhance('.toggle-content', { style: () => ({ display: toggleIsland.getState('open') ? 'block' : 'none', transition: 'all 0.3s ease' }) }); // Debug the toggle state anytime console.log('Toggle state:', toggleIsland.getState('open')); </script> 
Enter fullscreen mode Exit fullscreen mode

2.2 Form Handling with Validation

Alpine.js Version:

<form x-data="{ email: '', password: '', errors: {}, validate() { this.errors = {}; if (!this.email.includes('@')) { this.errors.email = 'Invalid email'; } if (this.password.length < 6) { this.errors.password = 'Password too short'; } }, submit() { this.validate(); if (Object.keys(this.errors).length === 0) { alert('Form submitted!'); } } }" x-on:submit.prevent="submit()"> <input x-model="email" placeholder="Email" x-on:blur="validate()" :class="{ 'error': errors.email }"> <span x-show="errors.email" x-text="errors.email"></span> <input x-model="password" type="password" placeholder="Password" x-on:blur="validate()" :class="{ 'error': errors.password }"> <span x-show="errors.password" x-text="errors.password"></span> <button type="submit" :disabled="Object.keys(errors).length > 0"> Submit </button> </form> 
Enter fullscreen mode Exit fullscreen mode

Juris.js Island Version:

<form class="login-island"> <input name="email" placeholder="Email" class="email-input"> <span class="email-error" style="display: none;"></span> <input name="password" type="password" placeholder="Password" class="password-input"> <span class="password-error" style="display: none;"></span> <button type="submit" class="submit-button">Submit</button> </form> <script> const loginIsland = new Juris({ states: { email: '', password: '', errors: {} }, services: { validator: { validateEmail: (email) => { const error = !email.includes('@') ? 'Invalid email' : null; loginIsland.setState('errors.email', error); return !error; }, validatePassword: (password) => { const error = password.length < 6 ? 'Password too short' : null; loginIsland.setState('errors.password', error); return !error; }, isFormValid: () => { const errors = loginIsland.getState('errors'); return !errors.email && !errors.password; } }, formHandler: { submit: () => { const { validator } = loginIsland.services; const email = loginIsland.getState('email'); const password = loginIsland.getState('password'); const emailValid = validator.validateEmail(email); const passwordValid = validator.validatePassword(password); if (emailValid && passwordValid) { alert('Form submitted!'); // Debug: inspect form state console.log('Form data:', { email, password }); } } } } }); // Email input enhancement loginIsland.enhance('.email-input', { value: () => loginIsland.getState('email'), oninput: (e) => loginIsland.setState('email', e.target.value), onblur: ({ validator }) => validator.validateEmail(loginIsland.getState('email')), className: () => loginIsland.getState('errors.email') ? 'error' : '' }); // Email error display loginIsland.enhance('.email-error', { text: () => loginIsland.getState('errors.email', ''), style: () => ({ display: loginIsland.getState('errors.email') ? 'block' : 'none', color: 'red' }) }); // Password input enhancement loginIsland.enhance('.password-input', { value: () => loginIsland.getState('password'), oninput: (e) => loginIsland.setState('password', e.target.value), onblur: ({ validator }) => validator.validatePassword(loginIsland.getState('password')), className: () => loginIsland.getState('errors.password') ? 'error' : '' }); // Password error display loginIsland.enhance('.password-error', { text: () => loginIsland.getState('errors.password', ''), style: () => ({ display: loginIsland.getState('errors.password') ? 'block' : 'none', color: 'red' }) }); // Submit button enhancement loginIsland.enhance('.submit-button', { disabled: ({ validator }) => !validator.isFormValid(), onclick: (e, { formHandler }) => { e.preventDefault(); formHandler.submit(); } }); // Debug form state anytime console.log('Login form state:', loginIsland.getState('login')); console.log('Form errors:', loginIsland.getState('errors')); </script> 
Enter fullscreen mode Exit fullscreen mode

2.3 Dynamic Lists with State Management

Alpine.js Version:

<div x-data="{ todos: [ { id: 1, text: 'Learn Alpine', done: false }, { id: 2, text: 'Build app', done: true } ], newTodo: '', addTodo() { if (this.newTodo.trim()) { this.todos.push({ id: Date.now(), text: this.newTodo, done: false }); this.newTodo = ''; } }, toggleTodo(id) { const todo = this.todos.find(t => t.id === id); todo.done = !todo.done; }, removeTodo(id) { this.todos = this.todos.filter(t => t.id !== id); } }"> <input x-model="newTodo" placeholder="Add todo" x-on:keyup.enter="addTodo()"> <button x-on:click="addTodo()">Add</button> <template x-for="todo in todos" :key="todo.id"> <div :class="{ 'done': todo.done }"> <span x-text="todo.text"></span> <input type="checkbox" x-model="todo.done"> <button x-on:click="removeTodo(todo.id)">Remove</button> </div> </template> </div> 
Enter fullscreen mode Exit fullscreen mode

Juris.js Island Version:

<div class="todos-island"> <div class="todo-input-section"> <input class="new-todo" placeholder="Add todo"> <button class="add-todo">Add</button> </div> <div class="todos-list"></div> <div class="todos-stats"></div> </div> <script> const todosIsland = new Juris({ states: { todos: [ { id: 1, text: 'Learn Juris', done: false }, { id: 2, text: 'Build app', done: true } ], newTodo: '' }, services: { todoManager: { addTodo: () => { const newTodo = todosIsland.getState('newTodo'); if (newTodo.trim()) { const todos = todosIsland.getState('todos'); todosIsland.setState('todos', [ ...todos, { id: Date.now(), text: newTodo, done: false } ]); todosIsland.setState('newTodo', ''); } }, toggleTodo: (id) => { const todos = todosIsland.getState('todos'); const updated = todos.map(todo => todo.id === id ? { ...todo, done: !todo.done } : todo ); todosIsland.setState('todos', updated); }, removeTodo: (id) => { const todos = todosIsland.getState('todos'); todosIsland.setState('todos', todos.filter(t => t.id !== id)); }, getStats: () => { const todos = todosIsland.getState('todos'); return { total: todos.length, done: todos.filter(t => t.done).length, remaining: todos.filter(t => !t.done).length }; } } } }); // New todo input todosIsland.enhance('.new-todo', { value: () => todosIsland.getState('newTodo'), oninput: (e) => todosIsland.setState('newTodo', e.target.value), onkeyup: (e, { todoManager }) => { if (e.key === 'Enter') { todoManager.addTodo(); } } }); // Add button todosIsland.enhance('.add-todo', { onclick: ({ todoManager }) => todoManager.addTodo() }); // Todos list (dynamic children) todosIsland.enhance('.todos-list', ({ todoManager }) => ({ children: () => { const todos = todosIsland.getState('todos'); return todos.map(todo => ({ div: { key: todo.id, className: todo.done ? 'todo-item done' : 'todo-item', children: [ { span: { text: todo.text } }, { input: { type: 'checkbox', checked: todo.done, onchange: () => todoManager.toggleTodo(todo.id) } }, { button: { text: 'Remove', onclick: () => todoManager.removeTodo(todo.id) } } ] } })); } })); // Live stats todosIsland.enhance('.todos-stats', ({ todoManager }) => ({ text: () => { const stats = todoManager.getStats(); return `${stats.remaining} of ${stats.total} remaining`; } })); // Debug todos state anytime console.log('Todos state:', todosIsland.getState('todos')); console.log('New todo:', todosIsland.getState('newTodo')); // Live debugging - watch state changes todosIsland.subscribe('todos', (todos) => { console.log('Todos updated:', todos); }); </script> 
Enter fullscreen mode Exit fullscreen mode

Step 3: Advanced Debugging Capabilities

3.1 State Inspection and Time-Travel

// Create a debug-enabled island const debugTodosIsland = new Juris({ states: { todos: [], filter: 'all' } }); // State history for time-travel debugging const stateHistory = []; const maxHistory = 50; // Intercept all state changes const originalSetState = debugTodosIsland.setState.bind(debugTodosIsland); debugTodosIsland.setState = function(path, value) { // Record state before change const snapshot = { timestamp: Date.now(), path, oldValue: this.getState(path), newValue: value, fullState: JSON.parse(JSON.stringify(this.state)) }; // Apply the change const result = originalSetState(path, value); // Record the change stateHistory.push(snapshot); if (stateHistory.length > maxHistory) { stateHistory.shift(); } console.log('State change:', snapshot); return result; }; // Debug utilities window.debugTodos = { // Current state getState: () => debugTodosIsland.getState('todos'), // State history getHistory: () => stateHistory, // Time travel revertToState: (index) => { const snapshot = stateHistory[index]; if (snapshot) { debugTodosIsland.state = JSON.parse(JSON.stringify(snapshot.fullState)); console.log('Reverted to state:', snapshot); } }, // Reset to initial state reset: () => { debugTodosIsland.setState('todos', []); debugTodosIsland.setState('filter', 'all'); }, // Performance monitoring getUpdateCount: () => stateHistory.length, getLastUpdate: () => stateHistory[stateHistory.length - 1] }; console.log('Debug utilities available:', window.debugTodos); 
Enter fullscreen mode Exit fullscreen mode

3.2 Island Communication Debugging

// Multi-island setup with debug communication const headerIsland = new Juris({ states: { user: null } }); const sidebarIsland = new Juris({ states: { menuOpen: false } }); // Debug-enabled island communication const islandCommunication = { channels: new Map(), // Subscribe to cross-island events subscribe: (channel, callback) => { if (!this.channels.has(channel)) { this.channels.set(channel, []); } this.channels.get(channel).push(callback); console.log(`Subscribed to channel: ${channel}`); }, // Publish cross-island events publish: (channel, data) => { console.log(`Publishing to ${channel}:`, data); const callbacks = this.channels.get(channel) || []; callbacks.forEach(callback => { try { callback(data); } catch (error) { console.error(`Error in ${channel} callback:`, error); } }); }, // Debug island states debugAllIslands: () => { console.log('All island states:', { header: headerIsland.getState('header'), sidebar: sidebarIsland.getState('sidebar') }); } }; // Example: User login affects multiple islands islandCommunication.subscribe('user-login', (user) => { headerIsland.setState('user', user); sidebarIsland.setState('menuOpen', true); }); // Trigger cross-island communication headerIsland.enhance('.login-button', { onclick: () => { const user = { name: 'John Doe', id: 1 }; islandCommunication.publish('user-login', user); } }); // Debug island communication window.islandDebug = islandCommunication; 
Enter fullscreen mode Exit fullscreen mode

Step 4: Performance and Bundle Comparison

4.1 Bundle Size Analysis

<!-- Alpine.js Setup --> <script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script> <!-- Size: ~15KB gzipped --> <!-- Juris.js Setup --> <script src="https://unpkg.com/juris@0.5.2/juris.js"></script> <!-- Size: ~25KB gzipped --> 
Enter fullscreen mode Exit fullscreen mode

Trade-off Analysis:

  • Alpine.js: Smaller initial bundle, but template-bound logic
  • Juris.js: Slightly larger bundle, but pure JavaScript with full debugging

4.2 Performance Monitoring

// Performance monitoring for Juris islands const performanceMonitor = { timers: new Map(), measurements: [], start: (label) => { performanceMonitor.timers.set(label, performance.now()); }, end: (label) => { const startTime = performanceMonitor.timers.get(label); if (startTime) { const duration = performance.now() - startTime; performanceMonitor.measurements.push({ label, duration, timestamp: Date.now() }); console.log(`${label}: ${duration.toFixed(2)}ms`); performanceMonitor.timers.delete(label); } }, getReport: () => { const report = {}; performanceMonitor.measurements.forEach(m => { if (!report[m.label]) { report[m.label] = []; } report[m.label].push(m.duration); }); // Calculate averages Object.keys(report).forEach(label => { const times = report[label]; report[label] = { count: times.length, average: times.reduce((a, b) => a + b, 0) / times.length, min: Math.min(...times), max: Math.max(...times) }; }); return report; } }; // Monitor island creation performance performanceMonitor.start('island-creation'); const performanceTestIsland = new Juris({ states: { items: Array.from({length: 1000}, (_, i) => ({ id: i, text: `Item ${i}` })) } }); performanceMonitor.end('island-creation'); // Monitor enhancement performance performanceMonitor.start('enhancement-setup'); performanceTestIsland.enhance('.performance-test', { children: () => { const items = performanceTestIsland.getState('items'); return items.slice(0, 100).map(item => ({ div: { text: item.text, key: item.id } })); } }); performanceMonitor.end('enhancement-setup'); // Get performance report console.log('Performance Report:', performanceMonitor.getReport()); 
Enter fullscreen mode Exit fullscreen mode

Step 5: Migration Strategy and Timeline

5.1 Gradual Island-by-Island Migration

<!-- Phase 1: Keep Alpine for complex parts, start with simple islands --> <div x-data="{ complexState: {...} }"> <!-- Keep complex Alpine logic temporarily --> <div x-show="complexState.showAdvanced">...</div> </div> <!-- Convert simple toggles to Juris islands first --> <div class="simple-toggle-island"> <button class="toggle-btn">Toggle</button> <div class="toggle-content">Content</div> </div> <script> // Simple island conversion const toggleIsland = new Juris({ states: { open: false } }); toggleIsland.enhance('.toggle-btn', { onclick: () => { const current = toggleIsland.getState('open'); toggleIsland.setState('open', !current); } }); toggleIsland.enhance('.toggle-content', { style: () => ({ display: toggleIsland.getState('open') ? 'block' : 'none' }) }); </script> 
Enter fullscreen mode Exit fullscreen mode

5.2 Migration Priority Matrix

Component Type Complexity Debug Benefit Migration Priority
Simple toggles Low Medium High - Start here
Form validation Medium High High - Major debug benefit
Data lists Medium High Medium - Good debug benefit
Complex modals High High Medium - Plan carefully
Nested components High Medium Low - Migrate last

5.3 Side-by-Side Comparison Tool

<!DOCTYPE html> <html> <head> <title>Alpine vs Juris Comparison</title> <script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script> <script src="https://unpkg.com/juris@0.5.2/juris.js"></script> <style> .comparison { display: flex; gap: 20px; } .alpine-side, .juris-side { flex: 1; border: 1px solid #ccc; padding: 20px; } .debug-panel { background: #f5f5f5; padding: 10px; margin-top: 10px; } </style> </head> <body> <div class="comparison"> <!-- Alpine.js side --> <div class="alpine-side"> <h3>Alpine.js Version</h3> <div x-data="{ count: 0 }"> <button x-on:click="count++">Alpine Count: <span x-text="count"></span></button> <div class="debug-panel"> <strong>Debug:</strong> Hard to inspect Alpine state </div> </div> </div> <!-- Juris.js side --> <div class="juris-side"> <h3>Juris.js Island</h3> <div class="counter-island"> <button class="juris-counter">Juris Count: <span class="count">0</span></button> <div class="debug-panel"> <strong>Debug:</strong> <button onclick="debugJuris()">Inspect State</button> </div> </div> </div> </div> <script> // Juris island setup const comparisonIsland = new Juris({ states: { count: 0 } }); comparisonIsland.enhance('.juris-counter', { onclick: () => { const current = comparisonIsland.getState('count'); comparisonIsland.setState('count', current + 1); } }); comparisonIsland.enhance('.count', { text: () => comparisonIsland.getState('count') }); // Debug function function debugJuris() { console.log('Juris state:', comparisonIsland.getState('any')); alert(`Current count: ${comparisonIsland.getState('count')}`); } // Make debugging available globally window.jurisDebug = { getState: () => comparisonIsland.getState('any'), setState: (path, value) => comparisonIsland.setState(path, value), reset: () => comparisonIsland.setState('count', 0) }; </script> </body> </html> 
Enter fullscreen mode Exit fullscreen mode

Step 6: Testing and Quality Assurance

6.1 Island Testing Framework

// Simple testing framework for Juris islands const IslandTester = { tests: [], test: (name, islandFactory, testFn) => { IslandTester.tests.push({ name, islandFactory, testFn }); }, run: () => { console.log('Running island tests...'); let passed = 0; let failed = 0; IslandTester.tests.forEach(({ name, islandFactory, testFn }) => { try { console.log(`Testing: ${name}`); const island = islandFactory(); testFn(island); console.log(`✅ ${name} passed`); passed++; } catch (error) { console.error(`❌ ${name} failed:`, error); failed++; } }); console.log(`Tests complete: ${passed} passed, ${failed} failed`); } }; // Example tests IslandTester.test('Counter increment', () => new Juris({ states: { count: 0 } }), (island) => { // Test initial state if (island.getState('count') !== 0) { throw new Error('Initial count should be 0'); } // Test state change island.setState('count', 5); if (island.getState('count') !== 5) { throw new Error('Count should be 5 after setState'); } } ); IslandTester.test('Todo management', () => new Juris({ states: { todos: [] }, services: { todoManager: { add: (text) => { const todos = island.getState('todos'); island.setState('todos', [...todos, { id: Date.now(), text }]); } } } }), (island) => { // Test adding todos island.services.todoManager.add('Test todo'); const todos = island.getState('todos'); if (todos.length !== 1) { throw new Error('Should have 1 todo'); } if (todos[0].text !== 'Test todo') { throw new Error('Todo text should match'); } } ); // Run tests IslandTester.run(); 
Enter fullscreen mode Exit fullscreen mode

6.2 Debug Console Integration

// Enhanced debug console for production debugging const JurisDebugConsole = { islands: new Map(), register: (name, island) => { JurisDebugConsole.islands.set(name, island); console.log(`Registered island: ${name}`); }, inspect: (islandName) => { const island = JurisDebugConsole.islands.get(islandName); if (island) { return { state: island.getState('any'), services: Object.keys(island.services || {}), enhancementStats: island.getEnhancementStats?.() || 'Not available' }; } return null; }, listIslands: () => Array.from(JurisDebugConsole.islands.keys()), getGlobalState: () => { const globalState = {}; JurisDebugConsole.islands.forEach((island, name) => { globalState[name] = island.getState('any'); }); return globalState; }, exportState: () => { return JSON.stringify(JurisDebugConsole.getGlobalState(), null, 2); }, importState: (stateJson) => { try { const state = JSON.parse(stateJson); Object.keys(state).forEach(islandName => { const island = JurisDebugConsole.islands.get(islandName); if (island) { Object.keys(state[islandName]).forEach(key => { island.setState(key, state[islandName][key]); }); } }); console.log('State imported successfully'); } catch (error) { console.error('Failed to import state:', error); } } }; // Register islands for debugging JurisDebugConsole.register('todos', todosIsland); JurisDebugConsole.register('login', loginIsland); // Make debug console globally available window.JurisDebug = JurisDebugConsole; console.log('Juris Debug Console available as window.JurisDebug'); console.log('Commands: listIslands(), inspect(name), getGlobalState(), exportState()'); 
Enter fullscreen mode Exit fullscreen mode

Step 7: Complete Migration Example

7.1 Full Alpine.js Component

<div class="alpine-dashboard" x-data="{ user: { name: 'John Doe', avatar: '/avatar.jpg' }, notifications: [ { id: 1, text: 'Welcome!', read: false }, { id: 2, text: 'Update available', read: true } ], sidebarOpen: false, toggleSidebar() { this.sidebarOpen = !this.sidebarOpen; }, markAsRead(id) { const notification = this.notifications.find(n => n.id === id); if (notification) { notification.read = true; } }, get unreadCount() { return this.notifications.filter(n => !n.read).length; } }"> <!-- Header --> <header class="header"> <button x-on:click="toggleSidebar()" x-text="sidebarOpen ? 'Close' : 'Menu'"></button> <div class="user-info"> <img :src="user.avatar" :alt="user.name"> <span x-text="user.name"></span> </div> <div class="notifications"> <span x-text="unreadCount"></span> notifications </div> </header> <!-- Sidebar --> <aside x-show="sidebarOpen" x-transition class="sidebar"> <nav> <a href="/dashboard">Dashboard</a> <a href="/profile">Profile</a> <a href="/settings">Settings</a> </nav> </aside> <!-- Notifications --> <div class="notifications-panel"> <template x-for="notification in notifications" :key="notification.id"> <div :class="{ 'unread': !notification.read }" x-on:click="markAsRead(notification.id)"> <span x-text="notification.text"></span> <span x-show="!notification.read"></span> </div> </template> </div> </div> 
Enter fullscreen mode Exit fullscreen mode

7.2 Equivalent Juris.js Islands

<div class="juris-dashboard"> <!-- Header Island --> <header class="header-island"> <button class="sidebar-toggle">Menu</button> <div class="user-info"> <img class="user-avatar" src="/avatar.jpg" alt="User"> <span class="user-name">John Doe</span> </div> <div class="notifications-badge">0 notifications</div> </header> <!-- Sidebar Island --> <aside class="sidebar-island" style="display: none;"> <nav> <a href="/dashboard">Dashboard</a> <a href="/profile">Profile</a> <a href="/settings">Settings</a> </nav> </aside> <!-- Notifications Island --> <div class="notifications-island"> <div class="notifications-list"></div> </div> </div> <script src="https://unpkg.com/juris@0.5.2/juris.js"></script> <script> // User Island const userIsland = new Juris({ states: { user: { name: 'John Doe', avatar: '/avatar.jpg' } } }); userIsland.enhance('.user-name', { text: () => userIsland.getState('user.name') }); userIsland.enhance('.user-avatar', { src: () => userIsland.getState('user.avatar'), alt: () => userIsland.getState('user.name') }); // Sidebar Island const sidebarIsland = new Juris({ states: { open: false } }); sidebarIsland.enhance('.sidebar-toggle', { text: () => sidebarIsland.getState('open') ? 'Close' : 'Menu', onclick: () => { const current = sidebarIsland.getState('open'); sidebarIsland.setState('open', !current); } }); sidebarIsland.enhance('.sidebar-island', { style: () => ({ display: sidebarIsland.getState('open') ? 'block' : 'none', transition: 'all 0.3s ease' }) }); // Notifications Island const notificationsIsland = new Juris({ states: { notifications: [ { id: 1, text: 'Welcome!', read: false }, { id: 2, text: 'Update available', read: true } ] }, services: { notificationManager: { markAsRead: (id) => { const notifications = notificationsIsland.getState('notifications'); const updated = notifications.map(n => n.id === id ? { ...n, read: true } : n ); notificationsIsland.setState('notifications', updated); }, getUnreadCount: () => { const notifications = notificationsIsland.getState('notifications'); return notifications.filter(n => !n.read).length; } } } }); notificationsIsland.enhance('.notifications-badge', ({ notificationManager }) => ({ text: () => `${notificationManager.getUnreadCount()} notifications` })); notificationsIsland.enhance('.notifications-list', ({ notificationManager }) => ({ children: () => { const notifications = notificationsIsland.getState('notifications'); return notifications.map(notification => ({ div: { key: notification.id, className: notification.read ? 'notification read' : 'notification unread', onclick: () => notificationManager.markAsRead(notification.id), children: [ { span: { text: notification.text } }, notification.read ? null : { span: { text: '', className: 'unread-dot' } } ].filter(Boolean) } })); } })); // Debug all islands window.dashboardDebug = { user: userIsland, sidebar: sidebarIsland, notifications: notificationsIsland, getAllStates: () => ({ user: userIsland.getState('any'), sidebar: sidebarIsland.getState('any'), notifications: notificationsIsland.getState('any') }), simulateNotification: () => { const notifications = notificationsIsland.getState('notifications'); const newNotification = { id: Date.now(), text: `New notification ${notifications.length + 1}`, read: false }; notificationsIsland.setState('notifications', [...notifications, newNotification]); } }; console.log('Dashboard debug available as window.dashboardDebug'); </script> 
Enter fullscreen mode Exit fullscreen mode

Migration Benefits Summary

Debugging Advantages

  • Transparent State: island.getState('any') shows exact current state
  • State History: Track all state changes with timestamps
  • Live Inspection: Console access to all island states
  • Isolated Testing: Test each island independently
  • Performance Monitoring: Measure render times and state updates

Code Quality Improvements

  • Pure JavaScript: No template-bound logic
  • Service Injection: Clean dependency management
  • Type Safety: Easier to add TypeScript later
  • Testability: Standard JavaScript testing approaches
  • Maintainability: Clear separation of concerns

Development Experience

  • Better DevTools: Standard JavaScript debugging
  • Predictable Behavior: No hidden Alpine magic
  • Incremental Migration: Migrate one island at a time
  • Team Collaboration: Easier code reviews
  • Documentation: Self-documenting service architecture

Conclusion

Migrating from Alpine.js to Juris.js transforms your application from template-bound interactivity to pure, debuggable, islandable architecture. The key benefits:

  1. True Islands: Each enhancement is completely isolated
  2. Full Debugging: Transparent state management with console access
  3. Pure JavaScript: All logic in standard, debuggable JS functions
  4. Service Architecture: Clean dependency injection and separation of concerns
  5. Incremental Migration: Migrate one component at a time with confidence

The slightly larger bundle size (25KB vs 15KB) is offset by dramatically improved debugging capabilities, cleaner architecture, and better long-term maintainability. For teams prioritizing debuggability and code quality, Juris.js provides a compelling upgrade path from Alpine.js.

Top comments (3)

Collapse
 
artydev profile image
artydev

Long live to Juris ; #NORAV :-)

Collapse
 
nevodavid profile image
Nevo David

Pretty cool how clean the debugging gets here, honestly makes me wanna refactor my own mess instead of constantly poking random variables.

Collapse
 
lynphp profile image
Pinoy Codie

Thanks, and let me know of any feedback you may have