jQuery vs the Frameworks, or the Lost Art of Application Architecture

One of the reasons people choose JavaScript frameworks over simpler libraries like jQuery is because frameworks help you structure your code, and that jQuery results in indecipherable messes.

I read an comparison of frontend approaches that demonstrates this premise well, implementing a simple note taking application with several front end technologies, including jQuery. The jQuery code was indeed a mess, and the various frameworks resulted in much cleaner code. But is this how it has to be?

The Lost Art of Application Architecture

jQuery is just a library, it doesn’t give you any guidance about how to structure your application. This leaves it up to you to come up with an application architecture. Following the example from the previously mentioned comparison, can we avoid this problem:

This is where we start to see some difficulties with jQuery — while the DOM manipulation code itself is fairly straightforward, it becomes more and more complicated to remember the different parts of the DOM that need to be updated with each change.

This isn’t a jQuery problem, but a general programming problem. If you have shared state, how do you reduce the complexity of remembering all the different parts of that data that need to be updated with each change? In OOP terms, the answer is encapsulation.

So can we come up with something comprehensible with jQuery and ordinary JavaScript OOP?

For this demonstration, I’ll be borrowing the HTML and CSS template from linked article.

Here is the completed example in the OOP style, with all DOM updates encapsulated into single areas of responsibility. To increase the challenge, I’ve implemented the code in ES5. ES6 and Typescript are very helpful and pleasant to use (especially Typescript), but are not required for proper architecture.

I’ve used this approach to develop sites of small to medium complexity without things getting out of hand.

Does this mean we should abandon frameworks? Not at all! For more complex applications, frameworks offer very good tools for managing data flow, change propagation, dependency injection, etc.

But, usage of libraries does not mean spaghetti code, and you shouldn’t depend on frameworks to properly organize your application. The art of application architecture should not be underestimated!

Here’s how I developed the architecture step by step.

Step 1: Display A List of Notes

Let’s start with the basic definition of a note:

var Note = (function() {
var id = 0;
return function(body) {
this.id = id++;
this.body = body;
this.ts = new Date();
}
})();

This trick lets us encapsulate an id value that gets automatically incremented with each new note that we create, so that every note has a unique ID.

This lets us create a simple set of notes to start:

var notes = [ new Note("Note 1"), new Note("Note 2") ];

Let’s start with the following basic structure:

The NotesApp class has the responsibility of coordinating with other classes and managing the parent DOM node holding the list of notes.

The NoteSelector class has the responsibility of managing the DOM nodes for the title and timestamp.

var NotesApp = function(node, notes) {
// Remove the list item element to use it as a template
this.noteSelectorTpl = node.find('.note-selector').remove();
  // NotesApp will manage the list of notes and the parent container
this.noteSelectorContainer = node.find('.note-selectors');
this.noteSelectors = [];
  $.each(notes, (function(i, note) { this.addNote(note); })
.bind(this));
}
NotesApp.prototype.addNote = function(note) {
// Clone the list item template and create a new NoteSelector
// instance to manage it
this.noteSelectors.unshift(new NoteSelector(
this.noteSelectorTpl.clone().prependTo(this.noteSelectorHolder),
note));
}
var NoteSelector = function(node) {
this.node = node;
this.title = node.find('.note-selector-title');
this.timestamp = node.find('.note-selector-timestamp');
  // Poor man's data binding
this.title.text(this.note.title());
this.timestamp.text(this.note.timestamp());
}
Note.prototype.title = function() {
return this.body.length < 15 ? this.body :
this.body.slice(0, 15) + '...';
}
Note.prototype.timestamp = function() {
return this.ts.toUTCString();
}

The application is bootstrapped as follows:

$(function() { new NotesApp($('#app'), notes) });

The JavaScript maps to the following HTML structure:

<div id="app">
<div class="note-selectors">
<div class="note-selector">
<p class="note-selector-title">First note...</p>
<p class="note-selector-timestamp">Timestamp here...</p>
</div>
</div>
</div>

Step 2: Select a Note

To select a note, we need to add another collaborator, the NoteEditor, which manages the DOM state of the editor controls.

Note that all of the collaborators are tightly coupled together. The NoteSelector needs to know about both the NoteApp and the NoteEditor, to tell the NoteApp to update the selection and the NoteEditor to update its contents with the selected note. But at least DOM changes are contained to single areas of responsibility and we are constrained to coordinating between collaborators at a high level, so the situation has been improved.

We can address this issue by decoupling, by introducing events on observable states, which allow collaborators to register generic interest, so that they don’t have to know about each other specifically. That’s where observables and reactive components are helpful in improving the architecture. For simplicity, for the purpose of demonstration, I’m sticking with tighter coupling which isn’t a huge problem with a small number of collaborators.

var NotesApp = function(node, notes) {
...
  // Let NoteEditor manage the note editor controls
this.editor = new NoteEditor(node.find('.note-editor'));
}
NotesApp.prototype.setSelected = function(selection) {
this.selectedNote = selection;
}
NotesApp.prototype.deactivateSelection = function() {
$.each(this.noteSelectors, function(i, sel) {
sel.deactivate();
});
}
var NoteSelector = function(node, app, editor) {
...
  // Communicate with other instances OOP style, not through 
// the DOM
this.app = app;
this.editor = editor;
  // Poor man's data binding
this.title.text(this.note.title());
this.timestamp.text(this.note.timestamp());
  this.node.click(this.select.bind(this));
}
NoteSelector.prototype.select = function() {
// Tell the app to deactivate the selection (not worried about
// how the DOM will be changed)
this.app.deactivateSelection();
  // This is the DOM change we are interested in
this.node.addClass('active');
  // Communicate the selection change to the editor (not worried
// about how the DOM will be changed)
this.editor.editNote(this.note, this);
this.app.setSelected(this);
}
NoteSelector.prototype.deactivate = function() {
this.node.removeClass('active');
}
var NoteEditor = function(node) {
this.timestamp = node.find('.note-editor-info').text('');
this.editor = node.find('.note-editor-input').text('')
}
NoteEditor.prototype.editNote = function(note) {
this.timestamp.text(note.timestamp());
this.editor.val(note.body);
}

Step 3: Dynamically Update Note Title As Note Is Edited

var NoteEditor = function(node) {
...
  this.editor.on('input', this.onUpdate.bind(this));
}
NoteEditor.prototype.editNote = function(note, selector) {
...
  // Keep track of collaborator objects, not DOM nodes
this.note = note;
this.selector = selector;
}
NoteEditor.prototype.onUpdate = function() {
// poor man's two way binding
if(this.note) {
this.note.body = this.editor.val();
    // Notify the selector about the change (not worried about the 
// DOM change)
this.selector.refresh();
}
}
NoteSelector.prototype.refresh = function() {
this.title.text(this.note.title());
this.timestamp.text(this.note.timestamp());
}

Step 4: Add And Delete Notes

var NotesApp = function(node, notes) {
...
  node.find('.note-new').click(this.onNewNote.bind(this));  
node.find('.note-delete').click(this.onDeleteNote.bind(this));
}
NotesApp.prototype.onNewNote = function() {
this.addNote(new Note('New Note'));
}
NotesApp.prototype.onDeleteNote = function() {
if(this.selectedNote) {
this.selectedNote.remove();
this.noteSelectors = $.grep(
this.noteSelectors,
(function(note) {
return !this.selectedNote.equals(note);
}).bind(this));
this.resetSelection();
}
}
NotesApp.prototype.resetSelection = function() {
this.selectedNote = this.noteSelectors && this.noteSelectors[0];
if(this.selectedNote) {
this.selectedNote.activate();
}
}
NoteSelector.prototype.equals = function(that) {
return this.note.id === that.note.id;
}
NoteSelector.prototype.remove = function() {
this.node.remove();
}

Step 5: Searching For Notes

var NotesApp = function(node, notes) {
...
  this.search = node.find('.note-search').on('input',
this.onSearch.bind(this));
}
NotesApp.prototype.onSearch = function() {
// NotesApp lets each NoteSelector know that the search criteria
// changed (not worried about matching contents and
// showing/hiding DOM nodes. Good OOP is the art of passing
// the buck!)
$.each(this.noteSelectors, (function(i, note) {
note.showIfNotMatches(this.search.val());
}).bind(this));
}
NoteSelector.prototype.showIfNotMatches = function(criteria) {
if(criteria && this.note.body.toLowerCase().indexOf(
criteria.toLowerCase()) < 0) {
this.node.hide();
}
else {
this.node.show();
}
}