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
andx-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>
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>
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>
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>
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);
});
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>
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>
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>
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>
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>
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>
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);
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;
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 -->
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());
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>
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>
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();
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()');
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>
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>
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:
- True Islands: Each enhancement is completely isolated
- Full Debugging: Transparent state management with console access
- Pure JavaScript: All logic in standard, debuggable JS functions
- Service Architecture: Clean dependency injection and separation of concerns
- 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.
Long live to Juris ; #NORAV :-)