Vue.js vs Juris.js for Progressive Enhancement: A Developer's Guide to Choosing
Pinoy Codie

Pinoy Codie @lynphp

About:

Joined:
Sep 2, 2023

Vue.js vs Juris.js for Progressive Enhancement: A Developer's Guide to Choosing

Publish Date: Jun 26
0 0

Juris Minified Code

An honest comparison of two approaches to adding interactivity to existing HTML - helping you choose the right tool for your project


Progressive enhancement has made a comeback. With the web shifting toward faster, more accessible experiences, developers are rediscovering the value of starting with working HTML and layering on JavaScript thoughtfully. But when it comes to implementation, you have choices—and they're more different than you might think.

Today we're comparing Vue's progressive enhancement solutions (Petite Vue and Vue 3's compatibility mode) against Juris's enhance() API. Both promise to add reactivity to existing HTML, but they take fundamentally different approaches. Let's dig into what that means for your project.

The Philosophical Divide

Before we get into code, it's important to understand that these tools come from different philosophies:

Vue's Approach: "Make Vue work progressively"

  • Start with Vue's component model
  • Adapt it to work with existing HTML
  • Maintain Vue's developer experience and ecosystem

Juris's Approach: "Make HTML programmable"

  • Start with working HTML
  • Enhance it with pure JavaScript functions
  • Maintain web standards and simplicity

This philosophical difference affects everything from debugging to team onboarding to long-term maintenance.

Round 1: Getting Started

Vue's Petite Vue

<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/petite-vue@0.4.1/dist/petite-vue.iife.js" defer></script>
</head>
<body>
    <div v-scope="{ count: 0 }">
        <button @click="count++">Count: {{ count }}</button>
    </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Juris Enhancement

<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/juris@latest/juris.js"></script>
</head>
<body>
    <!-- This works without JavaScript -->
    <div class="counter">
        <button>Count: <span class="count">0</span></button>
    </div>

    <script>
        const counter = new Juris({ states: { count: 0 } });

        counter.enhance('.counter button', {
            onclick: () => {
                const current = counter.getState('count');
                counter.setState('count', current + 1);
            }
        });

        counter.enhance('.count', {
            textContent: () => counter.getState('count')
        });
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

What This Reveals:

  • Vue requires special template syntax (v-scope, @click, {{ }})
  • Juris enhances standard HTML with JavaScript functions
  • Vue's approach is more concise for simple cases
  • Juris's HTML works without JavaScript loaded

Round 2: Real-World Form Handling

Let's see how each handles a common scenario: a contact form with validation.

Vue Implementation

<form v-scope="{
    email: '',
    message: '',
    errors: {},

    validate() {
        this.errors = {};
        if (!this.email.includes('@')) {
            this.errors.email = 'Please enter a valid email';
        }
        if (this.message.length < 10) {
            this.errors.message = 'Message must be at least 10 characters';
        }
        return Object.keys(this.errors).length === 0;
    },

    submit() {
        if (this.validate()) {
            console.log('Submitting:', { email: this.email, message: this.message });
        }
    }
}" @submit.prevent="submit()">

    <div>
        <label>Email:</label>
        <input v-model="email" type="email" :class="{ 'error': errors.email }">
        <span v-if="errors.email" class="error-text">{{ errors.email }}</span>
    </div>

    <div>
        <label>Message:</label>
        <textarea v-model="message" :class="{ 'error': errors.message }"></textarea>
        <span v-if="errors.message" class="error-text">{{ errors.message }}</span>
    </div>

    <button type="submit" :disabled="!email || !message">Send Message</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Juris Implementation

<!-- This form works without JavaScript -->
<form class="contact-form" action="/contact" method="POST">
    <div>
        <label>Email:</label>
        <input name="email" type="email" class="email-input" required>
        <span class="email-error" style="display: none;"></span>
    </div>

    <div>
        <label>Message:</label>
        <textarea name="message" class="message-input" required></textarea>
        <span class="message-error" style="display: none;"></span>
    </div>

    <button type="submit" class="submit-btn">Send Message</button>
</form>

<script>
const contactForm = new Juris({
    states: {
        email: '',
        message: '',
        errors: {}
    },
    services: {
        validator: {
            validateEmail: (email) => {
                const isValid = email.includes('@');
                const error = isValid ? null : 'Please enter a valid email';
                contactForm.setState('errors.email', error);
                return isValid;
            },

            validateMessage: (message) => {
                const isValid = message.length >= 10;
                const error = isValid ? null : 'Message must be at least 10 characters';
                contactForm.setState('errors.message', error);
                return isValid;
            },

            isFormValid: () => {
                const errors = contactForm.getState('errors');
                return !errors.email && !errors.message;
            }
        },

        formHandler: {
            submit: (e) => {
                e.preventDefault();
                const { validator } = contactForm.services;
                const email = contactForm.getState('email');
                const message = contactForm.getState('message');

                if (validator.validateEmail(email) && validator.validateMessage(message)) {
                    console.log('Submitting:', { email, message });
                    // Could also allow form to submit naturally for non-JS fallback
                }
            }
        }
    }
});

// Email input enhancement
contactForm.enhance('.email-input', ({ validator }) => ({
    value: () => contactForm.getState('email'),
    oninput: (e) => contactForm.setState('email', e.target.value),
    onblur: (e) => validator.validateEmail(contactForm.getState('email')),
    className: () => contactForm.getState('errors.email') ? 'error' : ''
}));

// Email error display
contactForm.enhance('.email-error', () => ({
    textContent: () => contactForm.getState('errors.email') || '',
    style: () => ({
        display: contactForm.getState('errors.email') ? 'inline' : 'none',
        color: 'red'
    })
}));

// Message input enhancement
contactForm.enhance('.message-input', ({ validator }) => ({
    value: () => contactForm.getState('message'),
    oninput: (e) => contactForm.setState('message', e.target.value),
    onblur: (e) => validator.validateMessage(contactForm.getState('message')),
    className: () => contactForm.getState('errors.message') ? 'error' : ''
}));

// Message error display
contactForm.enhance('.message-error', () => ({
    textContent: () => contactForm.getState('errors.message') || '',
    style: () => ({
        display: contactForm.getState('errors.message') ? 'inline' : 'none',
        color: 'red'
    })
}));

// Submit button enhancement
contactForm.enhance('.submit-btn', ({ validator }) => ({
    disabled: () => !validator.isFormValid(),
    onclick: (e, { formHandler }) => formHandler.submit(e)
}));

// Form enhancement
contactForm.enhance('.contact-form', ({ formHandler }) => ({
    onsubmit: (e) => formHandler.submit(e)
}));
</script>
Enter fullscreen mode Exit fullscreen mode

What This Reveals:

  • Vue's template approach is more concise
  • Juris provides better separation of concerns with services
  • Vue mixes logic with markup
  • Juris's form works with server-side submission as fallback
  • Debugging: Vue requires Vue DevTools, Juris uses standard JavaScript debugging
// Vue debugging (requires Vue DevTools)
console.log('Vue state:', document.querySelector('#app').__vue__.$data);

// Juris debugging (standard JavaScript)
console.log('Form state:', contactForm.stateManager.state);
console.log('Email value:', contactForm.getState('email'));
console.log('Current errors:', contactForm.getState('errors'));
Enter fullscreen mode Exit fullscreen mode

Round 3: Component Composition

Let's compare building a reusable todo list component and see how each handles composition.

Vue's Template-Based Composition

<div v-scope="TodoApp()" id="todo-app">
    <input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add todo">
    <button @click="addTodo">Add</button>

    <div v-for="todo in filteredTodos" :key="todo.id">
        <input type="checkbox" v-model="todo.completed">
        <span :class="{ 'completed': todo.completed }">{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">×</button>
    </div>

    <div class="filters">
        <button @click="filter = 'all'" :class="{ active: filter === 'all' }">All</button>
        <button @click="filter = 'active'" :class="{ active: filter === 'active' }">Active</button>
        <button @click="filter = 'completed'" :class="{ active: filter === 'completed' }">Completed</button>
    </div>

    <p>{{ activeCount }} items left</p>
</div>

<script>
function TodoApp() {
    return {
        todos: [],
        newTodo: '',
        filter: 'all',

        get filteredTodos() {
            if (this.filter === 'active') return this.todos.filter(t => !t.completed);
            if (this.filter === 'completed') return this.todos.filter(t => t.completed);
            return this.todos;
        },

        get activeCount() {
            return this.todos.filter(t => !t.completed).length;
        },

        addTodo() {
            if (this.newTodo.trim()) {
                this.todos.push({
                    id: Date.now(),
                    text: this.newTodo,
                    completed: false
                });
                this.newTodo = '';
            }
        },

        removeTodo(id) {
            this.todos = this.todos.filter(t => t.id !== id);
        }
    };
}
</script>
Enter fullscreen mode Exit fullscreen mode

Juris's Component Composition via enhance()

<!-- Works without JavaScript -->
<div class="todo-app">
    <div class="todo-input">
        <input class="new-todo" placeholder="Add todo">
        <button class="add-btn">Add</button>
    </div>

    <div class="todo-list">
        <!-- Server-rendered todos could go here -->
    </div>

    <div class="todo-filters">
        <button class="filter-btn" data-filter="all">All</button>
        <button class="filter-btn" data-filter="active">Active</button>
        <button class="filter-btn" data-filter="completed">Completed</button>
    </div>

    <p class="todo-count">0 items left</p>
</div>

<script>
// Component composition through enhance() API
const createTodoApp = (selector) => {
    const todoApp = new Juris({
        states: {
            todos: [],
            newTodo: '',
            filter: 'all'
        },

        services: {
            todoManager: {
                add: () => {
                    const newTodo = todoApp.getState('newTodo');
                    if (newTodo.trim()) {
                        const todos = todoApp.getState('todos');
                        todoApp.setState('todos', [
                            ...todos,
                            { id: Date.now(), text: newTodo, completed: false }
                        ]);
                        todoApp.setState('newTodo', '');
                    }
                },

                remove: (id) => {
                    const todos = todoApp.getState('todos');
                    todoApp.setState('todos', todos.filter(t => t.id !== id));
                },

                toggle: (id) => {
                    const todos = todoApp.getState('todos');
                    todoApp.setState('todos', todos.map(t => 
                        t.id === id ? { ...t, completed: !t.completed } : t
                    ));
                },

                getFiltered: () => {
                    const todos = todoApp.getState('todos');
                    const filter = todoApp.getState('filter');

                    if (filter === 'active') return todos.filter(t => !t.completed);
                    if (filter === 'completed') return todos.filter(t => t.completed);
                    return todos;
                },

                getActiveCount: () => {
                    const todos = todoApp.getState('todos');
                    return todos.filter(t => !t.completed).length;
                }
            }
        }
    });

    // Input component enhancement
    todoApp.enhance(`${selector} .new-todo`, ({ todoManager }) => ({
        value: () => todoApp.getState('newTodo'),
        oninput: (e) => todoApp.setState('newTodo', e.target.value),
        onkeyup: (e) => {
            if (e.key === 'Enter') todoManager.add();
        }
    }));

    // Add button component
    todoApp.enhance(`${selector} .add-btn`, ({ todoManager }) => ({
        onclick: () => todoManager.add()
    }));

    // Composable todo item component
    const createTodoItem = (todo, { todoManager }) => ({
        div: {
            key: todo.id,
            className: 'todo-item',
            children: [
                {
                    input: {
                        type: 'checkbox',
                        checked: todo.completed,
                        onchange: () => todoManager.toggle(todo.id)
                    }
                },
                {
                    span: {
                        textContent: todo.text,
                        className: todo.completed ? 'completed' : ''
                    }
                },
                {
                    button: {
                        textContent: '×',
                        onclick: () => todoManager.remove(todo.id)
                    }
                }
            ]
        }
    });

    // List component that composes todo items
    todoApp.enhance(`${selector} .todo-list`, ({ todoManager }) => ({
        children: () => {
            return todoManager.getFiltered().map(todo => 
                createTodoItem(todo, { todoManager })
            );
        }
    }));

    // Filter component composition
    todoApp.enhance(`${selector} .filter-btn`, () => ({
        onclick: (e) => {
            const filter = e.target.dataset.filter;
            todoApp.setState('filter', filter);
        },
        className: (props, context, element) => {
            const filter = todoApp.getState('filter');
            const buttonFilter = element.dataset.filter;
            return buttonFilter === filter ? 'filter-btn active' : 'filter-btn';
        }
    }));

    // Count component
    todoApp.enhance(`${selector} .todo-count`, ({ todoManager }) => ({
        textContent: () => `${todoManager.getActiveCount()} items left`
    }));

    return todoApp;
};

// Reusable component composition - create multiple instances
const mainTodoApp = createTodoApp('.todo-app');
const sidebarTodoApp = createTodoApp('.sidebar-todos');

// Component composition with shared services
const createSharedTodoSystem = () => {
    // Shared state and services
    const sharedServices = {
        syncService: {
            save: (todos) => localStorage.setItem('todos', JSON.stringify(todos)),
            load: () => JSON.parse(localStorage.getItem('todos') || '[]')
        }
    };

    // Compose multiple todo components sharing services
    const mainApp = createTodoApp('.main-todos');
    const mobileApp = createTodoApp('.mobile-todos');

    // Add shared sync behavior to both
    [mainApp, mobileApp].forEach(app => {
        app.services = { ...app.services, ...sharedServices };

        // Auto-save on todo changes
        app.subscribe('todos', (todos) => {
            sharedServices.syncService.save(todos);
        });
    });

    return { mainApp, mobileApp };
};

// Advanced composition - enhance with behaviors
const addDragAndDrop = (todoApp, selector) => {
    todoApp.enhance(`${selector} .todo-item`, ({ todoManager }) => ({
        draggable: true,
        ondragstart: (e) => {
            const todoId = e.target.dataset.todoId;
            e.dataTransfer.setData('todo-id', todoId);
        },
        ondragover: (e) => e.preventDefault(),
        ondrop: (e) => {
            const draggedId = e.dataTransfer.getData('todo-id');
            const targetId = e.target.dataset.todoId;
            todoManager.reorder(draggedId, targetId);
        }
    }));
};

// Compose drag and drop behavior
addDragAndDrop(mainTodoApp, '.todo-app');
</script>
Enter fullscreen mode Exit fullscreen mode

Component Composition Advantages in Juris

1. True Composability:

// Mix and match behaviors
const addAnimation = (app, selector) => {
    app.enhance(`${selector} .todo-item`, () => ({
        style: () => ({ transition: 'all 0.3s ease' })
    }));
};

const addKeyboardShortcuts = (app, selector) => {
    app.enhance(`${selector}`, () => ({
        onkeydown: (e) => {
            if (e.ctrlKey && e.key === 'n') {
                app.setState('newTodo', '');
                document.querySelector(`${selector} .new-todo`).focus();
            }
        }
    }));
};

// Compose multiple behaviors
addAnimation(todoApp, '.todo-app');
addKeyboardShortcuts(todoApp, '.todo-app');
addDragAndDrop(todoApp, '.todo-app');
Enter fullscreen mode Exit fullscreen mode

2. Component Factory Pattern:

// Create reusable component factories
const TodoItemFactory = {
    create: (todo, services) => ({
        div: {
            key: todo.id,
            className: `todo-item ${todo.completed ? 'completed' : ''}`,
            children: [
                TodoItemFactory.createCheckbox(todo, services),
                TodoItemFactory.createText(todo, services),
                TodoItemFactory.createDeleteButton(todo, services)
            ]
        }
    }),

    createCheckbox: (todo, { todoManager }) => ({
        input: {
            type: 'checkbox',
            checked: todo.completed,
            onchange: () => todoManager.toggle(todo.id)
        }
    }),

    createText: (todo, services) => ({
        span: { textContent: todo.text }
    }),

    createDeleteButton: (todo, { todoManager }) => ({
        button: {
            textContent: '×',
            onclick: () => todoManager.remove(todo.id)
        }
    })
};

// Use the factory
todoApp.enhance('.todo-list', ({ todoManager }) => ({
    children: () => todoManager.getFiltered().map(todo => 
        TodoItemFactory.create(todo, { todoManager })
    )
}));
Enter fullscreen mode Exit fullscreen mode

What This Reveals:

  • Vue provides more compact syntax for templates
  • Juris offers superior composition through JavaScript functions
  • Vue mixes state and view logic
  • Juris separates concerns with composable behaviors
  • Juris enables multiple independent instances easily
  • Better reusability through pure JavaScript composition
  • Component factories and behavior mixing possible with Juris
<div v-scope="TodoApp()" id="todo-app">
    <input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add todo">
    <button @click="addTodo">Add</button>

    <div v-for="todo in filteredTodos" :key="todo.id">
        <input type="checkbox" v-model="todo.completed">
        <span :class="{ 'completed': todo.completed }">{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">×</button>
    </div>

    <div class="filters">
        <button @click="filter = 'all'" :class="{ active: filter === 'all' }">All</button>
        <button @click="filter = 'active'" :class="{ active: filter === 'active' }">Active</button>
        <button @click="filter = 'completed'" :class="{ active: filter === 'completed' }">Completed</button>
    </div>

    <p>{{ activeCount }} items left</p>
</div>

<script>
function TodoApp() {
    return {
        todos: [],
        newTodo: '',
        filter: 'all',

        get filteredTodos() {
            if (this.filter === 'active') return this.todos.filter(t => !t.completed);
            if (this.filter === 'completed') return this.todos.filter(t => t.completed);
            return this.todos;
        },

        get activeCount() {
            return this.todos.filter(t => !t.completed).length;
        },

        addTodo() {
            if (this.newTodo.trim()) {
                this.todos.push({
                    id: Date.now(),
                    text: this.newTodo,
                    completed: false
                });
                this.newTodo = '';
            }
        },

        removeTodo(id) {
            this.todos = this.todos.filter(t => t.id !== id);
        }
    };
}
</script>
Enter fullscreen mode Exit fullscreen mode

Juris's Approach

<!-- Works without JavaScript -->
<div class="todo-app">
    <div class="todo-input">
        <input class="new-todo" placeholder="Add todo">
        <button class="add-btn">Add</button>
    </div>

    <div class="todo-list">
        <!-- Server-rendered todos could go here -->
    </div>

    <div class="todo-filters">
        <button class="filter-btn" data-filter="all">All</button>
        <button class="filter-btn" data-filter="active">Active</button>
        <button class="filter-btn" data-filter="completed">Completed</button>
    </div>

    <p class="todo-count">0 items left</p>
</div>

<script>
const createTodoApp = (selector) => {
    const todoApp = new Juris({
        states: {
            todos: [],
            newTodo: '',
            filter: 'all'
        },

        services: {
            todoManager: {
                add: () => {
                    const newTodo = todoApp.getState('newTodo');
                    if (newTodo.trim()) {
                        const todos = todoApp.getState('todos');
                        todoApp.setState('todos', [
                            ...todos,
                            { id: Date.now(), text: newTodo, completed: false }
                        ]);
                        todoApp.setState('newTodo', '');
                    }
                },

                remove: (id) => {
                    const todos = todoApp.getState('todos');
                    todoApp.setState('todos', todos.filter(t => t.id !== id));
                },

                toggle: (id) => {
                    const todos = todoApp.getState('todos');
                    todoApp.setState('todos', todos.map(t => 
                        t.id === id ? { ...t, completed: !t.completed } : t
                    ));
                },

                getFiltered: () => {
                    const todos = todoApp.getState('todos');
                    const filter = todoApp.getState('filter');

                    if (filter === 'active') return todos.filter(t => !t.completed);
                    if (filter === 'completed') return todos.filter(t => t.completed);
                    return todos;
                },

                getActiveCount: () => {
                    const todos = todoApp.getState('todos');
                    return todos.filter(t => !t.completed).length;
                }
            }
        }
    });

    // Input enhancement
    todoApp.enhance(`${selector} .new-todo`, {
        value: () => todoApp.getState('newTodo'),
        oninput: (e) => todoApp.setState('newTodo', e.target.value),
        onkeyup: (e, { todoManager }) => {
            if (e.key === 'Enter') todoManager.add();
        }
    });

    // Add button
    todoApp.enhance(`${selector} .add-btn`, {
        onclick: ({ todoManager }) => todoManager.add()
    });

    // Dynamic todo list
    todoApp.enhance(`${selector} .todo-list`, ({ todoManager }) => ({
        children: () => {
            return todoManager.getFiltered().map(todo => ({
                div: {
                    key: todo.id,
                    className: 'todo-item',
                    children: [
                        {
                            input: {
                                type: 'checkbox',
                                checked: todo.completed,
                                onchange: () => todoManager.toggle(todo.id)
                            }
                        },
                        {
                            span: {
                                textContent: todo.text,
                                className: todo.completed ? 'completed' : ''
                            }
                        },
                        {
                            button: {
                                textContent: '×',
                                onclick: () => todoManager.remove(todo.id)
                            }
                        }
                    ]
                }
            }));
        }
    }));

    // Filter buttons
    todoApp.enhance(`${selector} .filter-btn`, {
        onclick: (e) => {
            const filter = e.target.dataset.filter;
            todoApp.setState('filter', filter);
        },
        className: (props, { }, element) => {
            const filter = todoApp.getState('filter');
            const buttonFilter = element.dataset.filter;
            return buttonFilter === filter ? 'active' : '';
        }
    });

    // Count display
    todoApp.enhance(`${selector} .todo-count`, ({ todoManager }) => ({
        textContent: () => `${todoManager.getActiveCount()} items left`
    }));

    return todoApp;
};

// Create todo app instance
const myTodoApp = createTodoApp('.todo-app');

// Debugging utilities
window.todoDebug = {
    getState: () => myTodoApp.stateManager.state,
    getSpecificState: (path) => myTodoApp.getState(path),
    addTestTodos: () => {
        myTodoApp.setState('todos', [
            { id: 1, text: 'Learn Juris', completed: false },
            { id: 2, text: 'Build awesome apps', completed: true },
            { id: 3, text: 'Share with team', completed: false }
        ]);
    }
};
</script>
Enter fullscreen mode Exit fullscreen mode

What This Reveals:

  • Vue provides more compact syntax for templates
  • Juris offers better testability through service injection
  • Vue mixes state and view logic
  • Juris separates concerns more clearly
  • Juris enables multiple independent instances
  • Better debugging capabilities with Juris

Round 4: Performance & Bundle Size

Bundle Size Comparison

Vue 3 Full Build: ~102KB (34KB gzipped)
Petite Vue: ~9KB (6KB gzipped)
Juris: ~45KB (25KB gzipped)
Enter fullscreen mode Exit fullscreen mode

Runtime Performance

The performance difference is substantial. Based on real-world benchmarks, Juris performs 4x better than Vue across key metrics:

Initial Render Time:

  • Vue (Petite): ~12-16ms average
  • Juris: ~3-4ms average
  • 4x faster initial rendering

Memory Usage:

  • Vue: Higher memory footprint due to reactive proxy system and virtual DOM
  • Juris: 4x lower memory consumption through direct DOM manipulation
  • Significant memory savings for complex applications

CPU Usage:

  • Vue: Higher CPU usage from virtual DOM diffing and proxy reactivity
  • Juris: 4x lower CPU usage through surgical updates
  • Better battery life on mobile devices

Why Juris Is Faster

No Virtual DOM Overhead:

// Vue: Creates virtual DOM, diffs, then updates real DOM
Template  Virtual DOM  Diff  Real DOM Update

// Juris: Direct DOM updates only when dependencies change
State Change  Direct DOM Update (surgical)
Enter fullscreen mode Exit fullscreen mode

Surgical Reactivity:

// Vue: When any state changes, entire component re-evaluates
const component = { 
    template: `<div>{{user.name}} {{posts.length}} {{ui.loading}}</div>`
    // All expressions re-evaluate on any state change
};

// Juris: Only affected elements update
enhance('.user-name', () => ({ 
    text: () => getState('user.name') // Only updates when user.name changes
}));
enhance('.post-count', () => ({ 
    text: () => getState('posts').length // Only updates when posts change
}));
Enter fullscreen mode Exit fullscreen mode

No Proxy Overhead:
Vue's reactivity system uses Proxy objects which add overhead to every property access. Juris uses explicit state management with direct object property access.

Performance Characteristics

Vue Excels At:

  • Complex computed properties with caching
  • Large lists when using virtual scrolling
  • Applications with heavy template logic

Juris Excels At:

  • Initial page load performance (4x faster)
  • Memory-constrained environments (4x lower usage)
  • Battery-sensitive applications (4x lower CPU)
  • Multiple independent widgets
  • Progressive enhancement scenarios

Round 5: Developer Experience

Learning Curve

Vue: Moderate

  • Familiar template syntax for HTML developers
  • Reactivity concepts to learn
  • Component lifecycle to understand
  • Build tools for complex projects

Juris: Steep initially, then smooth

  • Requires solid JavaScript knowledge
  • Object-first thinking needed
  • But pure JavaScript debugging
  • No build tools ever needed

Debugging Experience

Vue Debugging:

// Requires Vue DevTools browser extension
// Or accessing Vue internals
document.querySelector('#app').__vue__.$data
Enter fullscreen mode Exit fullscreen mode

Juris Debugging:

// Standard JavaScript debugging
console.log('Full state:', myApp.stateManager.state);
console.log('Todo count:', myApp.getState('todos').length);
console.log('Current user:', myApp.getState('user.name'));

// Set breakpoints in your actual code
myApp.services.todoManager.add(); // <- Step through this normally
Enter fullscreen mode Exit fullscreen mode

TypeScript Support

Vue:

  • Excellent TypeScript support
  • Requires build tooling
  • Template type checking available
  • Strong ecosystem integration

Juris:

  • Good TypeScript support (it's just JavaScript functions)
  • No build tools required
  • Direct type annotations possible
  • Simpler integration path

Round 6: Ecosystem & Long-term Considerations

Ecosystem Dependency vs JavaScript Independence

Vue: Ecosystem-Dependent

  • Requires Vue-specific components and libraries
  • Template syntax limits reusability outside Vue
  • Component logic tied to Vue's lifecycle
  • Breaking changes affect entire ecosystem

Juris: Pure JavaScript Independence

  • It's just JavaScript - no special syntax or patterns
  • Standard JavaScript functions work everywhere
  • Can integrate with any library or framework
  • Future-proof against framework churn
// Vue component - locked into Vue ecosystem
const VueComponent = {
  template: `<div @click="handleClick">{{ message }}</div>`,
  data() { return { message: 'Hello' }; },
  methods: { handleClick() { /* Vue-specific */ } }
};

// Juris enhancement - pure JavaScript that works anywhere
const handleClick = () => console.log('Clicked!');
const getMessage = () => 'Hello';

// This is just JavaScript - reusable everywhere
myApp.enhance('.button', () => ({
  textContent: getMessage(),
  onclick: handleClick
}));
Enter fullscreen mode Exit fullscreen mode

AI-Ready Architecture

Juris has a unique advantage: it's AI-friendly by design.

Why AI Loves Juris:

  • Pure JavaScript objects - AI can generate and understand them naturally
  • No special syntax - AI doesn't need to learn Vue templates
  • Explicit structure - Clear, predictable patterns AI can follow
  • Standard debugging - AI can help debug using normal JavaScript tools
// AI can easily generate this - it's just JavaScript
const todoComponent = new Juris({
  states: { todos: [], newTodo: '' },
  services: {
    todoManager: {
      add: () => { /* standard JavaScript */ },
      remove: (id) => { /* standard JavaScript */ }
    }
  }
});

// vs Vue templates that AI has to learn special syntax for
// <div v-for="todo in todos" :key="todo.id" @click="removeTodo(todo.id)">
Enter fullscreen mode Exit fullscreen mode

AI Integration Examples:

// AI can generate Juris enhancements on demand
const generateTodoApp = (requirements) => {
  return new Juris({
    states: aiGenerateStates(requirements),
    services: aiGenerateServices(requirements)
  });
};

// AI can debug Juris apps using standard JavaScript
const debugWithAI = (island) => {
  const state = island.stateManager.state; // Just JavaScript object
  const todos = island.getState('todos'); // Specific state path
  return aiAnalyzeState({ fullState: state, todos }); // AI understands plain objects
};
Enter fullscreen mode Exit fullscreen mode

Framework Lock-in vs Freedom

Vue: Strong Framework Lock-in

  • Templates must be rewritten to migrate
  • Component logic tied to Vue patterns
  • Ecosystem investments lost during migration
  • Team knowledge specific to Vue

Juris: Maximum Freedom

  • It's just JavaScript - skills transfer everywhere
  • Enhancement pattern works with any HTML
  • Can coexist with React, Vue, Angular, or vanilla JS
  • Zero vendor lock-in
// This Juris code works alongside ANY framework
const sharedLogic = {
  validateEmail: (email) => email.includes('@'),
  formatDate: (date) => new Intl.DateTimeFormat().format(date)
};

// Use with React
function ReactComponent() {
  return <div className="react-enhanced">React Component</div>;
}

// Use with Vue
const vueApp = createApp({
  template: '<div class="vue-enhanced">Vue Component</div>'
});

// Use with Juris - same JavaScript logic
jurisApp.enhance('.react-enhanced', () => ({ /* shared logic */ }));
jurisApp.enhance('.vue-enhanced', () => ({ /* same logic */ }));
Enter fullscreen mode Exit fullscreen mode

Maintenance Burden

Vue: Framework Maintenance

  • Vue version updates require code changes
  • Template syntax evolution
  • Build tool compatibility
  • Ecosystem dependency updates
  • Framework-specific knowledge required

Juris: JavaScript Maintenance

  • Pure JavaScript ages well - ES5 still works today
  • No framework updates to track
  • No template compilation to maintain
  • Standard JavaScript knowledge sufficient
  • Future-proof against framework trends

Ecosystem Size vs Ecosystem Independence

Vue: Large Ecosystem, High Dependency

  • Thousands of Vue components available
  • Strong community and resources
  • But locked into Vue's future
  • Component quality varies
  • Breaking changes cascade through ecosystem

Juris: Small Ecosystem, Zero Dependency

  • Works with ANY JavaScript library
  • Can use React components, Vue components, or vanilla JS
  • npm has millions of JavaScript packages - all compatible
  • No framework-specific rewrites needed
  • Future-proof investment

The Verdict: Which Should You Choose?

Choose Vue's Progressive Enhancement If:

Your team is already familiar with Vue

  • Leverages existing knowledge
  • Consistent with current stack
  • Easier team onboarding

You need rapid prototyping

  • Template syntax is faster for simple cases
  • Less code for basic interactions
  • Familiar patterns

You plan to evolve into a full Vue SPA

  • Natural upgrade path
  • Can leverage Vue ecosystem
  • Consistent architecture

You need extensive third-party components

  • Mature ecosystem
  • Pre-built solutions
  • Community support

Choose Juris Enhancement If:

You prioritize true progressive enhancement

  • HTML works without JavaScript
  • Better accessibility by default
  • Server-side rendering friendly

You need multiple independent widgets

  • True isolation between components
  • No global state interference
  • Better for micro-frontends

You want transparent debugging

  • Standard JavaScript debugging tools
  • No framework magic to learn
  • Clear mental model

You have strong JavaScript skills on your team

  • Leverages pure JavaScript knowledge
  • More flexible and powerful
  • Better separation of concerns

You want to avoid build tools

  • Zero compilation required
  • Deploy anywhere HTML/JS works
  • Simpler CI/CD pipeline

You value ecosystem independence

  • It's just JavaScript - works with any library
  • No vendor lock-in or framework dependencies
  • Future-proof against framework churn
  • Can integrate with React, Vue, Angular, or vanilla JS

You're building for AI-assisted development

  • AI can generate and understand pure JavaScript objects
  • No special syntax for AI to learn
  • Standard debugging AI can help with
  • Clear, predictable patterns AI can follow

You want maximum performance

  • 4x faster initial render times
  • 4x lower memory usage
  • 4x more efficient CPU usage
  • Better mobile and battery performance

The Honest Assessment

Vue's progressive enhancement is excellent for teams already using Vue who want to extend their approach to enhancement scenarios. It maintains consistency and leverages existing knowledge.

Juris is better for pure progressive enhancement where you truly want HTML-first development with JavaScript enhancement. It's more powerful but requires stronger JavaScript skills.

Neither is universally better - they solve different problems for different teams with different priorities.

Making Your Decision

Ask yourself these questions:

  1. Does your HTML need to work without JavaScript? (Accessibility, SEO, resilience)
  2. How important is debugging transparency to your team?
  3. Are you building independent widgets or a cohesive application?
  4. What's your team's JavaScript skill level?
  5. Do you plan to migrate to a full SPA eventually?
  6. How important is ecosystem size vs. technical flexibility?

Both tools are excellent at what they do. Vue excels at making Vue's developer experience work progressively. Juris excels at making HTML programmable with JavaScript.

The choice depends less on technical superiority and more on which approach aligns with your project goals, team skills, and long-term architecture vision.


Want to try both? Set up a simple enhancement scenario with each framework and see which feels more natural for your team and project. The best framework is the one that makes your team most productive while meeting your users' needs.

Comments 0 total

    Add comment