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:
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:
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 (0)