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>
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!')
});
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>
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>
`;
}
}));
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>
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>
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();
}
})
}
});
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>
Juris enhance() Approach:
<div class="item-list">
<!-- Items will be dynamically rendered here by Juris -->
</div>
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);
}
}}
]
}
}));
}
}));
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;
});
}
}
})
<!-- Component usage -->
<script>
import { mapState, mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapState(['filter']),
...mapGetters(['filteredTodos'])
},
methods: {
...mapActions(['addTodo'])
}
}
</script>
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
}))
}));
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>
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.
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...