DEV Community

lynphp
lynphp

Posted on

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

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:

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:

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.

Top comments (0)