Vue.js vs Juris.js enhance(): A Framework Comparison
Pinoy Codie

Pinoy Codie @lynphp

About:

Joined:
Sep 2, 2023

Vue.js vs Juris.js enhance(): A Framework Comparison

Publish Date: Jun 21
3 6

An objective analysis of two different approaches to building reactive web applications


Introduction: Two Philosophies

Vue.js represents the traditional component-based framework approach with templates, build tools, and structured patterns. Juris enhance() takes a different path - progressive enhancement of existing DOM with reactive capabilities.

Let's examine how these two approaches handle common web development scenarios.


Comparison 1: Adding Interactive Elements

Vue.js Approach:

<template>
  <button @click="handleClick">{{ buttonText }}</button>
</template>

<script>
export default {
  data() {
    return {
      buttonText: 'Click me'
    }
  },
  methods: {
    handleClick() {
      this.buttonText = 'Clicked!';
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Vue.js Requirements:

  • Single File Components (SFC)
  • Build process (Vue CLI/Vite)
  • Template syntax
  • Component registration
  • Export/import structure

Juris enhance() Approach:

juris.enhance('button', {
    text: () => juris.getState('buttonText', 'Click me'),
    onclick: () => juris.setState('buttonText', 'Clicked!')
});
Enter fullscreen mode Exit fullscreen mode

Juris Requirements:

  • Single script tag inclusion
  • Works with existing HTML
  • No build step needed

Trade-offs:

  • Vue: More structure, IDE support, familiar patterns
  • Juris: Faster setup, works with legacy code, smaller footprint

Comparison 2: Computed Values and Derived State

Vue.js Approach:

<template>
  <div>
    <p>Total: {{ totalItems }}</p>
    <p>Completed: {{ completedItems }}</p>
    <p>Progress: {{ progressPercentage }}%</p>
    <p>Status: {{ currentStatus }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      todos: [
        { id: 1, text: 'Learn Vue', completed: true },
        { id: 2, text: 'Build app', completed: false }
      ]
    }
  },
  computed: {
    totalItems() {
      return this.todos.length;
    },
    completedItems() {
      return this.todos.filter(todo => todo.completed).length;
    },
    progressPercentage() {
      if (this.totalItems === 0) return 0;
      return Math.round((this.completedItems / this.totalItems) * 100);
    },
    currentStatus() {
      if (this.completedItems === 0) return 'Not started';
      if (this.completedItems === this.totalItems) return 'Completed';
      return 'In progress';
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Juris enhance() Approach:

juris.enhance('.stats-panel', ({ getState }) => ({
    innerHTML: () => {
        const todos = getState('todos', []);

        // Computed values as regular variables
        const totalItems = todos.length;
        const completedItems = todos.filter(todo => todo.completed).length;
        const progressPercentage = totalItems === 0 ? 0 : 
            Math.round((completedItems / totalItems) * 100);
        const currentStatus = 
            completedItems === 0 ? 'Not started' :
            completedItems === totalItems ? 'Completed' : 'In progress';

        return `
            <p>Total: ${totalItems}</p>
            <p>Completed: ${completedItems}</p>
            <p>Progress: ${progressPercentage}%</p>
            <p>Status: ${currentStatus}</p>
        `;
    }
}));
Enter fullscreen mode Exit fullscreen mode

Trade-offs:

  • Vue: Explicit computed section, reactive caching, clear separation
  • Juris: Inline calculations, standard JavaScript, co-located logic

Comparison 3: Form Handling and Validation

Vue.js Approach:

<template>
  <form @submit.prevent="handleSubmit">
    <input 
      v-model="form.name" 
      :class="{ error: errors.name }"
      @blur="validateName"
    />
    <span v-if="errors.name" class="error">{{ errors.name }}</span>

    <input 
      v-model="form.email" 
      :class="{ error: errors.email }"
      @blur="validateEmail"
    />
    <span v-if="errors.email" class="error">{{ errors.email }}</span>

    <button :disabled="!isFormValid" type="submit">
      {{ isSubmitting ? 'Submitting...' : 'Submit' }}
    </button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      form: { name: '', email: '' },
      errors: {},
      isSubmitting: false
    }
  },
  computed: {
    isFormValid() {
      return Object.keys(this.errors).length === 0 && 
             this.form.name && this.form.email;
    }
  },
  methods: {
    validateName() {
      if (!this.form.name) {
        this.$set(this.errors, 'name', 'Name is required');
      } else {
        this.$delete(this.errors, 'name');
      }
    },
    validateEmail() {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(this.form.email)) {
        this.$set(this.errors, 'email', 'Invalid email');
      } else {
        this.$delete(this.errors, 'email');
      }
    },
    async handleSubmit() {
      this.isSubmitting = true;
      try {
        await this.submitForm();
        this.resetForm();
      } catch (error) {
        this.handleError(error);
      } finally {
        this.isSubmitting = false;
      }
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Juris enhance() Approach:

<form>
   <h2>Contact Form</h2>

   <div class="form-group">
       <label for="name">Name:</label>
       <input type="text" name="name" id="name" />
       <div class="error-message" data-field="name"></div>
   </div>

   <div class="form-group">
       <label for="email">Email:</label>
       <input type="email" name="email" id="email" />
       <div class="error-message" data-field="email"></div>
   </div>

   <div class="form-group">
       <label for="message">Message:</label>
       <textarea name="message" id="message" rows="4"></textarea>
       <div class="error-message" data-field="message"></div>
   </div>

   <button type="submit">Submit</button>
</form>

Enter fullscreen mode Exit fullscreen mode
const juris = new Juris({
    services: {
        formValidator: {
            validateField: (field, value) => {
                let error = null;
                if (field === 'email' && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
                    error = 'Invalid email';
                } else if (!value) {
                    error = `${field} is required`;
                }
                juris.setState(`errors.${field}`, error);
                return !error;
            },

            isFormValid: () => {
                const errors = juris.getState('errors', {});
                const form = juris.getState('form', {});
                return Object.keys(errors).every(key => !errors[key]) && 
                       form.name && form.email;
            }
        }
    }
});

juris.enhance('form', {
    selectors: {
        'input[name]': (ctx) => {
            const field = ctx.element.name;
            return {
                value: () => juris.getState(`form.${field}`, ''),
                oninput: (e) => juris.setState(`form.${field}`, e.target.value),
                onblur: ({ formValidator }) => formValidator.validateField(field, ctx.element.value),
                style: () => ({
                    borderColor: juris.getState(`errors.${field}`) ? 'red' : '#ddd'
                })
            };
        },

        '.error-message': (ctx) => {
            const field = ctx.element.dataset.field;
            return {
                text: () => juris.getState(`errors.${field}`, ''),
                style: () => ({
                    display: juris.getState(`errors.${field}`) ? 'block' : 'none'
                })
            };
        },

        'button[type="submit"]': ({ formValidator }) => ({
            disabled: () => !formValidator.isFormValid(),
            text: () => juris.getState('form.submitting') ? 'Submitting...' : 'Submit',
            onclick: async (e) => {
                e.preventDefault();
                await submitForm();
            }
        })
    }
});
Enter fullscreen mode Exit fullscreen mode

Trade-offs:

  • Vue: Template-driven validation, v-model convenience, structured component
  • Juris: Service-based validation, selector targeting, works with any HTML structure

Comparison 4: Component Composition

Vue.js Approach:

<!-- ParentComponent.vue -->
<template>
  <div class="parent">
    <child-component 
      v-for="item in items"
      :key="item.id"
      :item="item"
      @item-clicked="handleItemClick"
      @item-deleted="handleItemDelete"
    />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: { ChildComponent },
  data() {
    return {
      items: [
        { id: 1, name: 'Item 1', status: 'active' },
        { id: 2, name: 'Item 2', status: 'inactive' }
      ]
    }
  },
  methods: {
    handleItemClick(item) {
      this.updateItemStatus(item.id);
    },
    handleItemDelete(item) {
      this.items = this.items.filter(i => i.id !== item.id);
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Juris enhance() Approach:

<div class="item-list">
   <!-- Items will be dynamically rendered here by Juris -->
</div>

Enter fullscreen mode Exit fullscreen mode
const juris = new Juris({
    services: {
        itemManager: {
            updateStatus: (id) => {
                const items = juris.getState('items', []);
                const updated = items.map(item => 
                    item.id === id 
                        ? { ...item, status: item.status === 'active' ? 'inactive' : 'active' }
                        : item
                );
                juris.setState('items', updated);
            },

            delete: (id) => {
                const items = juris.getState('items', []);
                juris.setState('items', items.filter(item => item.id !== id));
            }
        }
    }
});

juris.enhance('.item-list', ({ getState, itemManager }) => ({
    children: () => {
        const items = getState('items', []);
        return items.map(item => ({
            div: {
                className: `item ${item.status === 'active' ? 'active' : ''}`,
                onclick: () => itemManager.updateStatus(item.id),
                children: [
                    { span: { text: item.name } },
                    { button: { 
                        text: 'Delete',
                        onclick: (e) => {
                            e.stopPropagation();
                            itemManager.delete(item.id);
                        }
                    }}
                ]
            }
        }));
    }
}));
Enter fullscreen mode Exit fullscreen mode

Trade-offs:

  • Vue: Clear parent-child relationships, props/events pattern, separate files for organization
  • Juris: Service injection, single enhancement definition, inline composition

Comparison 5: State Management

Vue.js Approach (with Vuex):

// store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    todos: [],
    filter: 'all'
  },
  mutations: {
    ADD_TODO(state, todo) {
      state.todos.push(todo);
    },
    TOGGLE_TODO(state, id) {
      const todo = state.todos.find(t => t.id === id);
      todo.completed = !todo.completed;
    },
    SET_FILTER(state, filter) {
      state.filter = filter;
    }
  },
  actions: {
    async addTodo({ commit }, text) {
      const todo = await api.createTodo(text);
      commit('ADD_TODO', todo);
    }
  },
  getters: {
    filteredTodos: state => {
      return state.todos.filter(todo => {
        if (state.filter === 'completed') return todo.completed;
        if (state.filter === 'active') return !todo.completed;
        return true;
      });
    }
  }
})
Enter fullscreen mode Exit fullscreen mode
<!-- Component usage -->
<script>
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['filter']),
    ...mapGetters(['filteredTodos'])
  },
  methods: {
    ...mapActions(['addTodo'])
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Juris enhance() Approach:

const juris = new Juris({
    states: {
        todos: [],
        filter: 'all'
    },
    services: {
        todoService: {
            add: async (text) => {
                const todo = await api.createTodo(text);
                const todos = juris.getState('todos', []);
                juris.setState('todos', [...todos, todo]);
            },

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

            setFilter: (filter) => {
                juris.setState('filter', filter);
            },

            getFiltered: () => {
                const todos = juris.getState('todos', []);
                const filter = juris.getState('filter');

                return todos.filter(todo => {
                    if (filter === 'completed') return todo.completed;
                    if (filter === 'active') return !todo.completed;
                    return true;
                });
            }
        }
    }
});

// Usage in any enhancement
juris.enhance('.todo-list', ({ todoService }) => ({
    children: () => todoService.getFiltered().map(todo => ({
        // Component definition
    }))
}));
Enter fullscreen mode Exit fullscreen mode

Trade-offs:

  • Vue + Vuex: Structured state flow, time-travel debugging, clear mutations pattern
  • Juris: Direct state access, service-based organization, built-in reactive system

Technical Comparison

Bundle Size and Performance

Vue.js:

  • Core framework: ~130KB (minified + gzipped)
  • Additional tooling: Build system, CLI tools
  • Runtime: Virtual DOM reconciliation, component instances

Juris:

  • Framework: ~25KB (minified + gzipped)
  • No build tools required
  • Runtime: Direct DOM manipulation, efficient subscriptions

Learning Curve

Vue.js:

  • Template syntax (directives, interpolation)
  • Component lifecycle hooks
  • Props/events system
  • Vuex patterns (mutations, actions, getters)
  • Build tooling setup

Juris enhance():

  • JavaScript functions and objects
  • State management concepts
  • Selector-based targeting
  • Service injection pattern

Use Case Suitability

Vue.js excels at:

  • New applications built from scratch
  • Teams familiar with component-based architecture
  • Projects requiring extensive IDE support
  • Applications needing advanced debugging tools

Juris enhance() excels at:

  • Progressive enhancement of existing sites
  • Rapid prototyping and small projects
  • Legacy application modernization
  • Teams preferring minimal tooling

Real-World Example: Todo Application

Vue.js Implementation:

Structure: 6 separate files
Lines of code: ~240 lines
Setup time: 10-15 minutes (CLI setup, dependencies)
Build process: Required

Juris enhance() Implementation:

<!DOCTYPE html>
<html>
<head>
    <title>Todo App</title>
    <script src="juris.js"></script>
</head>
<body>
    <div class="todo-app">
        <input class="todo-input" placeholder="Add todo...">
        <div class="filter-buttons"></div>
        <div class="todo-list"></div>
        <div class="todo-stats"></div>
    </div>

    <script>
        const juris = new Juris({
            states: { todos: [], filter: 'all' },
            services: {
                todoService: {
                    add: (text) => {
                        const todos = juris.getState('todos', []);
                        juris.setState('todos', [...todos, {
                            id: Date.now(), text, completed: false
                        }]);
                    },
                    toggle: (id) => {
                        const todos = juris.getState('todos');
                        juris.setState('todos', todos.map(t =>
                            t.id === id ? {...t, completed: !t.completed} : t
                        ));
                    },
                    delete: (id) => {
                        const todos = juris.getState('todos');
                        juris.setState('todos', todos.filter(t => t.id !== id));
                    },
                    setFilter: (filter) => juris.setState('filter', filter),
                    getFiltered: () => {
                        const todos = juris.getState('todos', []);
                        const filter = juris.getState('filter');
                        return todos.filter(todo => {
                            if (filter === 'completed') return todo.completed;
                            if (filter === 'active') return !todo.completed;
                            return true;
                        });
                    },
                    getActiveCount: () => {
                        return juris.getState('todos', []).filter(t => !t.completed).length;
                    }
                }
            }
        });

        juris.enhance('.todo-input', ({ todoService }) => ({
            onkeypress: (e) => {
                if (e.key === 'Enter' && e.target.value.trim()) {
                    todoService.add(e.target.value.trim());
                    e.target.value = '';
                }
            }
        }));

        juris.enhance('.filter-buttons', ({ getState, todoService }) => ({
            children: () => ['all', 'active', 'completed'].map(filter => ({
                button: {
                    text: filter,
                    className: getState('filter') === filter ? 'active' : '',
                    onclick: () => todoService.setFilter(filter)
                }
            }))
        }));

        juris.enhance('.todo-list', ({ todoService }) => ({
            children: () => todoService.getFiltered().map(todo => ({
                div: {
                    className: `todo ${todo.completed ? 'completed' : ''}`,
                    children: [
                        { input: {
                            type: 'checkbox',
                            checked: todo.completed,
                            onchange: () => todoService.toggle(todo.id)
                        }},
                        { span: { text: todo.text } },
                        { button: { text: '×', onclick: () => todoService.delete(todo.id) }}
                    ]
                }
            }))
        }));

        juris.enhance('.todo-stats', ({ todoService }) => ({
            text: () => `${todoService.getActiveCount()} items left`
        }));
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Structure: Single HTML file
Lines of code: ~80 lines
Setup time: 30 seconds (include script tag)
Build process: None required


Summary

Both Vue.js and Juris enhance() are capable tools for building reactive web applications, but they represent fundamentally different philosophies:

Vue.js provides a comprehensive, opinionated framework with strong conventions, extensive tooling, and a mature ecosystem. It's well-suited for teams building new applications who want structured patterns and extensive IDE support.

Juris enhance() offers a lightweight, flexible approach that works with existing HTML and requires minimal setup. It's ideal for progressive enhancement, rapid prototyping, and situations where you want reactive capabilities without the overhead of a full framework.

The choice between them depends on your project requirements, team preferences, and existing constraints. Vue.js excels in structured, team-based development environments, while Juris enhance() shines in scenarios requiring flexibility, minimal tooling, and gradual adoption.

Both approaches can build the same applications - the difference lies in how you get there and what trade-offs you're willing to make along the way.

Comments 6 total

  • asedas
    asedasJun 27, 2025

    Thanks for the article. Imho a bit odd to compare Vue option api, while current is composition one. The Juris examples are also incomplete because misses the attachment html markups (it may not be clear for not so experienced devs).

    In general you compare Vue framework with Juris library which is not exactly fair. Once you don't need the builder for Juris you need still some for prod tbh because I don't imagine you will pit this raw code for production.

    Another issue is that reusability of Juris code will be significantly less, so in fact this tool - as explained - is nice, but only as jquery replacement...

    • Pinoy Codie
      Pinoy CodieJun 27, 2025

      Thanks for taking the time to read and comment! You raise some great points that deserve a thoughtful response.

      On Vue Composition API vs Options API: You're absolutely right that Composition API is more current. I chose Options API for the comparison because it's often what beginners encounter first, but I'd be happy to update with Composition API examples to show both approaches.
      On HTML markup: Great feedback! You're correct that complete HTML examples would make it much clearer, especially for newer developers. I'll add those to make the examples more complete and practical.

      On framework vs library comparison: This is a fair point about scope differences. However, I'd respectfully suggest that Juris is more than a jQuery replacement. At just 2,400 lines of code, Juris delivers full reactive state management, component systems, async handling, and non-blocking rendering - features that typically require much larger frameworks or multiple libraries.

      On build tools: You're right that production apps usually need build processes. The beauty of Juris is that it works both ways - you can start without any build tools (great for prototyping or enhancing existing sites) and add them later when needed. This flexibility is actually a strength, not a limitation.

      On reusability: I'd argue that Juris components are quite reusable - they're just JavaScript functions that return objects. You can share them across projects, publish them as NPM packages, or compose them just like any other JavaScript code. The syntax might be different from Vue SFCs, but the reusability is definitely there.

      The real strength of Juris isn't replacing Vue for large SPAs, but offering a gentler learning curve and better performance for many common use cases. Sometimes 2,400 lines beats 34,000+ lines when you don't need the extra complexity.

      Would love to hear your thoughts on an updated comparison with Composition API and complete examples!

      • asedas
        asedasJun 27, 2025

        Fair reply, thanks for the update!

    • Pinoy Codie
      Pinoy CodieJun 27, 2025

      Hello Asedas,
      I made a follow-up post about comparing Juris's enhance() and Vue Composition API so you may check that out.

      Live Demo Links for you to verify:
      Vue Demo: jurisjs.com/demos/vue_trading_dashboard.html
      Juris Demo: jurisjs.com/demos/juris_trading_dashboard.html

  • Dotallio
    DotallioJun 27, 2025

    This side-by-side breakdown is super helpful, especially with real code for each scenario. Has anyone here found success mixing Juris style enhancements inside larger Vue apps for progressive migration?

    • Pinoy Codie
      Pinoy CodieJun 27, 2025

      Great question! Progressive migration is actually one of Juris's strongest use cases.

      As an author of Juris, here is the undocumented behavior of enhance() API.

      1. It will auto-enhance any element after mount within the 10ms timeframe.
      2. If Vue removes the element, Juris will not care and will just unload the reference cleanup and wait for next element that matches the selector.
      3. enhance() can also fully takeover and remove all the references that Vue to the element but not sure yet if there are some side effects. I can do a study on this behavior.
Add comment