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.
Top comments (6)
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...
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!
Fair reply, thanks for the update!
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
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?
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.