I recently stumbled upon something that made me question everything I thought I knew about DOM manipulation. It started with a simple request: "Can you help convert this to jQuery to see how long the code would be?"
What I discovered wasn't just about line count—it was about two fundamentally different philosophies of web development that couldn't be more different in their approach to the same problem.
The Original: Beauty in Simplicity
Here's the code I was asked to convert. It's written in a framework called Juris:
juris.enhance('.data-table', (ctx) => ({
// Header cells become sortable
'th[data-column]': {
className: (el) => () => {
const column = el.dataset.column;
const sortColumn = ctx.getState('sort.column');
const sortDirection = ctx.getState('sort.direction');
let classes = 'sortable';
if (sortColumn === column) {
classes += sortDirection === 'asc' ? ' sort-asc' : ' sort-desc';
}
return classes;
},
onclick: (el) => () => {
const column = el.dataset.column;
const currentColumn = ctx.getState('sort.column');
const currentDirection = ctx.getState('sort.direction');
let newDirection = 'asc';
if (currentColumn === column && currentDirection === 'asc') {
newDirection = 'desc';
}
ctx.setState('sort.column', column);
ctx.setState('sort.direction', newDirection);
sortTable(column, newDirection, el.dataset.type);
}
},
// Table rows get hover effects
'tbody tr': {
style: () => ({
transition: 'all 0.3s ease'
})
},
// Status badges within table cells
'.status': {
style: (el) => () => {
const status = el.textContent.toLowerCase();
return {
backgroundColor: status === 'active' ? '#28a745' :
status === 'pending' ? '#ffc107' : '#dc3545',
color: 'white',
padding: '4px 8px',
borderRadius: '4px'
};
}
}
}));
At first glance, this looked clean, but I didn't think much of it. "Sure," I thought, "I can convert this to jQuery. How hard could it be?"
The jQuery Reality Check
Here's what the equivalent jQuery code looked like:
function enhanceTable(selector) {
const $table = $(selector);
// Initialize state storage on the table element
$table.data('sort', { column: null, direction: 'asc' });
// Helper functions for state management
function getState(key) {
const sortData = $table.data('sort');
const keys = key.split('.');
let value = sortData;
for (let k of keys) {
value = value[k];
}
return value;
}
function setState(key, value) {
const sortData = $table.data('sort');
const keys = key.split('.');
let obj = sortData;
for (let i = 0; i < keys.length - 1; i++) {
obj = obj[keys[i]];
}
obj[keys[keys.length - 1]] = value;
$table.data('sort', sortData);
updateHeaderClasses();
}
// Function to update header classes based on current sort state
function updateHeaderClasses() {
$table.find('th[data-column]').each(function() {
const $th = $(this);
const column = $th.data('column');
const sortColumn = getState('sort.column');
const sortDirection = getState('sort.direction');
// Remove existing sort classes
$th.removeClass('sortable sort-asc sort-desc');
// Add base sortable class
$th.addClass('sortable');
// Add direction class if this is the active sort column
if (sortColumn === column) {
$th.addClass(sortDirection === 'asc' ? 'sort-asc' : 'sort-desc');
}
});
}
// Header cells become sortable
$table.find('th[data-column]').each(function() {
const $th = $(this);
const column = $th.data('column');
// Add click handler
$th.on('click', function() {
const currentColumn = getState('sort.column');
const currentDirection = getState('sort.direction');
let newDirection = 'asc';
if (currentColumn === column && currentDirection === 'asc') {
newDirection = 'desc';
}
setState('sort.column', column);
setState('sort.direction', newDirection);
// Sort the table
if (typeof sortTable === 'function') {
sortTable(column, newDirection, $th.data('type'));
}
});
});
// Table rows get hover effects and transitions
$table.find('tbody tr').css({
'transition': 'all 0.3s ease'
});
// Status badges within table cells
$table.find('.status').each(function() {
const $status = $(this);
const status = $status.text().toLowerCase();
let backgroundColor;
if (status === 'active') {
backgroundColor = '#28a745';
} else if (status === 'pending') {
backgroundColor = '#ffc107';
} else {
backgroundColor = '#dc3545';
}
$status.css({
'background-color': backgroundColor,
'color': 'white',
'padding': '4px 8px',
'border-radius': '4px'
});
});
// Initial setup of header classes
updateHeaderClasses();
}
The result? 40 lines became 85 lines. But the line count was the least shocking part.
What Hit Me Like a Truck
Looking at these two pieces of code side by side, I realized I was witnessing two completely different philosophies of programming:
The Declarative Approach (Juris)
- "Here's what I want" - The code reads like a specification
- Nested structure - Mirrors the HTML hierarchy you're working with
- Reactive by default - Classes and styles automatically update when state changes
- Pure functions - Each property is a function of current state, no side effects
The Imperative Approach (jQuery)
- "Here's how to do it" - The code reads like a step-by-step manual
- Scattered logic - State management, event handling, and styling are mixed together
- Manual coordination - You must remember to call update functions when state changes
- Side effects everywhere - Mutating DOM, managing state, coordinating updates
The Readability Gap
The Juris version reads almost like CSS-in-JS: "Table headers with data-column should have these classes based on sort state, and when clicked should update the sort state."
The jQuery version reads like assembly instructions: "Find all the headers, loop through them, attach click handlers, create state management functions, remember to update classes manually when state changes, don't forget to call the initial setup..."
In Juris, you focus on what you want. In jQuery, you focus on how to implement it.
The Maintenance Nightmare
But here's where it gets really interesting. In the jQuery version, if I want to add a new sort state (like "unsorted"), I need to:
- Update the state management logic
- Update the class calculation logic
- Update the click handler logic
- Remember to update the initial setup
- Make sure all the manual coordination still works
In the Juris version, I just update the className function. The reactive system handles the rest.
The jQuery approach doesn't just have more code—it has more places where bugs can hide.
What This Means for Modern Development
This comparison made me realize why modern frameworks like React, Vue, and Svelte have gained such traction. It's not just about performance or bundle size—it's about the fundamental shift from imperative to declarative programming.
But here's what's interesting about Juris: it brings declarative programming directly to DOM manipulation, without the overhead of a virtual DOM. It's like having the best parts of modern frameworks, but applied directly to the browser's native elements.
The Questions This Raises
Seeing this comparison left me with some burning questions:
- Have we been overcomplicating DOM manipulation all these years?
- Is there a middle ground between jQuery's imperative approach and full framework overhead?
- What if reactive DOM updates could be as simple as declaring what you want?
Final Thoughts
I went into this thinking I was just converting some code from one library to another. I came out questioning fundamental assumptions about how we build web interfaces.
The jQuery era taught us how to manipulate the DOM programmatically. Modern frameworks taught us to think declaratively. But maybe there's still room for innovation in how we bridge that gap.
Juris might be onto something here. The fact that it can express complex, stateful DOM interactions so concisely suggests there are still unexplored territories in web development tooling.
What do you think? Are we ready for a new approach to DOM manipulation, or is the jQuery-to-framework evolution already complete?
Have you experienced similar "aha moments" when comparing different approaches to the same problem? I'd love to hear about them in the comments.
Top comments (3)
Great insights.
If you'd like to go further down the rabbit hole, check out solidjs.com/.
This library is 100% reactive. An additional interesting feature is that it doesn't use a virtual DOM (like React does), which makes it incredibly fast.
Look at Juris site, one single file HTML rendered in around :
No bundler, no mess with nodejs modules, pure HTML/CSS/JS, personaly i don't need more.
Solid is great for what it is, it pioneered signal, but I find signal is not the future for state transparency and domain-based global state. Solid was not designed to no-build, non-blocking, lazy component rendering and the philosophy is more around Signal. while Juris is questioning all the complexity that all the Phd's from React had created. All framework that relies on JSX relies on build that needs 6-12 months adaptions before going mainstream. While juris can release features, bug fixes weekly.