January 13, 2015 by Christoff Truter JavaScript Architecture
I think its a safe bet to say that most of you have used some kind of JavaScript framework by now - if not a few of them. If you have never heard of technologies like JQuery, Mootools or Prototype, it is seriously time to get out from under that rock you call home.
There are a lot of advantages to using these frameworks and they have greatly improved the JavaScript we write. In my opinion however, its not the ability to write better code that made these frameworks so popular, but rather the thousands of plugins written for them.
It made it a lot easier to put something awesome out there, without spending too much time writing it yourself. Just like anything and everything else however, this can obviously be abused if we don't think or care about our architecture.
Let me give you a quick scenario.
Project wood, needs plugin woodchuck, which relies on framework chuck, the developer ads framework chuck to the project, the next day the developer finds plugin banana, which relies on framework minion and he ads minion to project wood as well.
Now framework chuck and framework minion are very similar, with some differences, but in general most of their functionality intersects and we don't use any of the non-intersecting functionality - which doesn't warrant both of them being there. Before long the developer might even ad framework Madagascar and framework Frozen as well to the soup, for the sake of some plugin needing it.
Adding redundant infrastructure to our applications is obviously never a good idea, how do we address this properly?As a general rule, we must rather use as little dependencies as possible, choose one framework (wisely) and stick with it.
A while back (2013) I was part of a little discussion on stackoverflow that touched this subject briefly, my suggested solution involved some kind of mediation framework. Basically the idea was to write plugins that indirectly uses the available framework, as my proof of concept, I created a little tagging plugin that uses the same codebase to run on 7 different JavaScript frameworks (JQuery, Mootools, Prototype, YUI, Dojo, Ext and Zepto)
Now I must admit, this is probably not the correct way to approach this issue, especially since this isn't common practise at all.
I do however feel that it is an interesting approach that might be useful - plugins you write will require no additional dependencies but would survive in whatever habitat it lives in. I've included the mentioned proof of concept in the download at the bottom of the post.
Here is a quick rundown of how all of this works.
Firstly I created an empty object (that acts as an interface), that contains all the functions I want to use in my plugin (added new functions as I needed them):
var $M = { ById : function(id) { }, Sel : function(selector) { }, El : function(element) { return new $M.ElementResult(element); }, String : { Trim : function(value) { } }, Array : { In : function(value, array) { } } }; $M.EventArgs = function(e) { this.Which = e.which; this.Type = e.type; } $M.ElementResult = function(element) { this.element = element; } $M.ElementResult.prototype = new function() { this.Append = function(obj) {} this.Before = function(obj) {} this.Hide = function() {} this.Html = function(value) {} this.Attribute = function(id) {} this.Value = function(value) { } this.On = function(eventName, eventCallback) { } this.Off = function() { } }
Next like previously stated, I implemented this object for 7 different frameworks, obviously you don't need to do that every time yourself, just do it for whatever your framework you are planning to use - but leaving it open for someone else to write a mediator fitting their environment.
In the following snippet we use these mediated functions in the plugin, the full source code is included in the download at the end of the post.
var Tagger = function() { var tags = []; var element = $M.El(this); var id = element.Attribute('id'); var tagName = id + '_tagger'; var tagsElementName = id + '_tags'; var readOnly = !!(element.Attribute('readonly')); function CreateTags() { var tagsElement = $M.ById(tagsElementName); tagsElement.Html(''); $M.Sel('.tag.' + tagName).Off(); for (var i = 0; i < tags.length; i++) { tags[i] = $M.String.Trim(tags[i]); tagsElement.Append('<span class="tag ' + tagName + '">' + tags[i] + '</span>'); } if (!readOnly) { $M.Sel('.tag.' + tagName).On('click', function(e) { var value = $M.El(this).Html(); tags.splice($M.Array.In(value, tags), 1); CreateTags(); }); $M.ById(id).Value(tags); } } element.Hide(); element.Before('<div class="tags" id="' + tagsElementName + '"></div>'); if (element.Value().length > 0) { tags = element.Value().split(','); CreateTags(); } if (readOnly) { return; } element.Before('<input type="text" id="' + tagName + '" />'); $M.ById(tagName).On('keydown', function(e) { var tagInputElement = $M.El(this); if ((e.Which == 8) && (tagInputElement.Value().length == 0)) { tags.pop(); CreateTags(); } }); $M.ById(tagName).On('keypress', function(e) { var tagInputElement = $M.El(this); if (e.Which == 13) { var value = $M.String.Trim(tagInputElement.Value().toLowerCase()); if ((value.length > 0) && ($M.Array.In(value, tags) == -1)) { tags.push(value); CreateTags(); tagInputElement.Value(''); } } }); };