The last project I worked on revolved around a unique and feature rich screen.
After three releases and one year of work, it ended with around twenty thousand lines of JavaScript for that screen alone, and another five thousand for the rest of the application. The line count for the C# backend stopped at nineteen thousand.
25k lines of js, 19k lines of c#.
Well, the title of this post if probably missing a word, but I’m trying to keep this thing curse free.
This screen has one interesting aspect: almost no ajax calls are needed, so all the logic and state have to be handled by js alone.
Most things and practices approached on this project probably wouldn’t fit js on a smaller scale. If fifty lines of script solves your screens problem, it doesn’t require a domain model – and be grateful for that, trust me.
The team learned a few things as the project progressed, and I’d like to share some of these with you.
Don’t use global events
This one was hard. We started with a very basic event hub for all the events on the screen. That seemed fine at the beginning, but as any desktop developer knows, when screen state dictates what events should be fired, things start to get out of control.
In the end we favoured a simpler approach where the model itself fires high level events:
function Document() {
return $.mix(observable(), {
addFile: function (file) {
// logic ...
this.fire('fileAdded', file);
}
}
}
function Sidebar(document) {
document.on('fileAdded').execute(function (file) {
// rerender itself
});
return { … };
}
Use GUIDs wherever you can
One aspect of the screen that led to very poor code was the problem with entity identifiers.
When the user adds a new entity on the screen, we assign a negative unique integer ID to it so we can work with the object.
However, when the user saves the screen state, the database assigns positive, real IDs to all the entities.
On the way back to the screen we had to update all the stored state with the new IDs. Messy.
If we worked with GUIDs for all the entities, we could generate the final IDs on the client side, since the logic to create them is can be easily written in js as well.
Encapsulation will save you in the end
Don’t use public properties, encapsulate every bit of exposed data through functions, and exposed data should only be used for displaying it:
function Image(id, name, file) {
return {
id: function () { return id; },
name: function () { return name; }
}
}
This is one of the many practices that we as software developers are used to, but for some reason forget them when writing client side js.
Favor immutability
Dealing with state stored on the client side can become very messy very fast, immutability will help you a great deal.
If you have immutability you can do cool stuff like this:
_.prototype.get = function (v) {
return function () {
return v;
}
}
function Item(id, name) {
return {
id: _.get(id),
name: _.get(name)
}
}
If your values are mutable this wouldn’t work.
Bind when creating
Avoid this:
images.each(function (image) {
var li = $(' <li></li>').attr('data-id', this.id());
container.append(li);
});
$('li', container).click(function () {
var id = this.attr('data-id');
view.dialogs.openImage(id);
});
In favor of this:
images.each(function (image) {
var li = $(' <li></li>')
li.click(function () {
view.dialog.openImage(image);
});
container.append(li);
});
It looks obvious on this small example, but if you find yourself storing entity data on the markup, there’s a very good chance that your code isn’t being executed on the right scope.
An exception to this is when performance becomes an issue because of too many binds. $.delegate helps a bunch when this happens, but you’ll need to rely on data inside the markup.
Inheritance can hurt
function Entity(id, name) {
return {
changeName: function(newName) {
name = newName;
}
}
}
function Document(id, name, file) {
var api = {
extension: function () {
return file.split('\\.').last();
},
render: function (renderer) {
return renderer.render(id, name, file);
}
}
return $.extend(api, new Entity(id, name));
}
var doc = new Document(1, 'the document', 'file.pdf');
doc.changeName('the new document');
doc.render({
render: function (id, name, file) {
// BUG: receives the old name
}
});
When we call doc.changeName(), the parent function on Entity is executed.
However, the Entity function changes its scoped “name” variable.
When we call doc.render, its scoped “name” variable still contains the old value.
To fix this, we’d have to declare a getter for the name on the parent entity, and the “name” variable on Document is useless and dangerous.
Avoid small tricks with element identifiers
<input id="field-id" />
<input id="field-name" />
<input id="field-email" />
function validate(field) {
var val = $('#field-' + field).val();
// ...
}
validate('id');
validate('name');
validate('email');
That small concatenation of the inputs IDs is a very common example of programmers trying to avoid a bit of duplication.
Avoid small tricks.
On a large codebase a part of your day revolves around finding element identifiers on files.
A common operation:
1. Inspect element with firebug to find its id
2. Go to IDE and search all .js files for given id to see who’s working with it
Keep the DOM away from your domain
Yep.
Hope this helps.