In my never ending quest to write better code, I'm revisiting overall JS code structure and class patterns. I think I've come to a structure that I'm happy with, but would love to get some feedback from the community.
I'll include 4 JS code samples (global.js, class.Base.js, class.Overlay.js, and class.Modal.js) below, but here's a summary:
- global.js handles setting up the namespace, a runner list (used to execute page load code in one DOM ready), list of available modules, etc. it's mainly just a wrapper json to hold all the pieces together.
- class.Base.js sets up a base class that all other classes are derived from. It contains a set of methods that I would want on most other classes
- class.Overlay.js is an example class that inherits from Base directly
- class.Modal.js inherits from Overlay (and hence Base), overrides the "show" method
My initial goal was to do things the JS way. I've tinkered with (pseudo)classical inheritance to a certain extent, and I'm more in the camp of, why force JS to do something it wasn't meant to do?
So I wanted to use prototypal inheritance and because portability of the code is fairly important to me, I wanted a dynamic namespace. I also wanted private variables but I can rarely get everything I want.
In the end, my questions are, is there a way for me to use prototypal inheritance, and have real private variables (not a debate on whether or not I truly NEED privates, but it's something I'd like to know how to address if I ever decided I wanted/needed one)? And ultimately, is there any advice on the code samples below? Bad pattern? Better optimization?
Thanks in advance!
global.js
// ability to rename namespace easily
var AXS_NS = 'App';
// this is the only DOM ready event
// all other page load code is added to the runner
jQuery(function($){
window[AXS_NS]._initialize();
});
window[AXS_NS] = (function (app, $, Modernizr) {
// app.runList is used to run all init functions on a single dom ready
app.runList = {};
// app.modules keeps a list of all available modules
app.modules = {};
app._initialize = function() {
this.$html = $('html').data(AXS_NS, this);
this.run();
};
// executes all init functions attached to this.runList
app.run = function() {
$.each(this.runList, function(runItemName, json) {
if (json.init && $.isFunction(json.init)) json.init();
});
};
app.augment = function(json) {
$.implement(json, app);
};
app.listen = function(evt, fn) {
$(this).bind(evt, fn);
};
app.unlisten = function(evt) {
$(this).unbind(evt);
};
app.dispatch = function(evt, args) {
$(this).trigger(evt, args);
};
app.add = {
"runItem": function(runItemName, json) {
app.runList[runItemName] = json;
}
,"module": function(moduleName, module) {
app.modules[moduleName] = module;
$.plugin(moduleName, module);
}
};
app.remove = {
"runItem": function(runItemName) {
delete app.runList[runItemName];
}
,"module": function(moduleName) {
delete app.modules[moduleName];
}
};
return app;
})(window[AXS_NS] || {}, jQuery, Modernizr);
class.Base.js:
// Base class used to derive ALL other classes
window[AXS_NS] = (function (app, $, Modernizr) {
function Base () {};
Base.prototype = {
// takes options passed in and sets them as properties of the instance
// e.g this.setProperties('$elem', 'name');
"setProperties": function() {
var self = this;
$.each(arguments, function(k, prop) {
if (self.cfg[prop]) self[prop] = self.cfg[prop];
});
}
// update configuration json of the instance
,"updateCfg": function(options) {
this.cfg = $.extend(true, this.cfg, options);
}
// creates a dom bridge between the instance and a dom element for later access
// will only work if the instance has a property of $elem (obviously?)
// try to keep the bridgeName the same as the instance Class name
,"domBridge": function(bridgeName) {
this.$elem.data(bridgeName, this);
}
// shortcut for $.implement
,"augment": function(json) {
$.implement(json, this);
}
};
app.add.module('Base', Base);
return app;
})(window[AXS_NS] || {}, jQuery, Modernizr);
class.Overlay.js:
// inherits from Base
window[AXS_NS] = (function (app, $, Modernizr) {
function Overlay (options) {
this.cfg = $.extend(true, {}, this.defaults, options);
// set autoInit to false to manually initialize the class
if (this.cfg.autoInit) this._initialize();
};
var prototype = {
"defaults": {
$elem: null, $target: null, width: 100, height: 50, autoShow: true, autoCenter: true, autoInit: true
}
,"_initialize": function() {
console.log('overlay._initialize()');
this.setProperties('$elem');
if (this.init) this.init();
}
,"init": function() {
console.log('overlay.init()');
}
,"show": function() {
console.log('overlay.show()');
this.$elem.show();
}
,"hide": function() {
this.$elem.hide();
}
};
// inherit from Base
Overlay.prototype = $.extend(true, {}, app.modules.Base.prototype, prototype);
app.add.module('Overlay', Overlay);
return app;
})(window[AXS_NS] || {}, jQuery, Modernizr);
class.Modal.js:
// inherits from Overlay (which inherits from Base)
window[AXS_NS] = (function (app, $, Modernizr) {
function Modal (options) {
this.cfg = $.extend(true, {}, this.defaults, options);
// set autoInit to false to manually initialize the class
if (this.cfg.autoInit) this._initialize();
};
// create reference to parent (Overlay) show method
var overlayShow = app.modules.Overlay.prototype.show;
var prototype = {
"defaults": {
$elem: null, $target: null, width: 100, height: 50, autoShow: true, autoCenter: true, autoInit: true
}
,"init": function() {
console.log('modal.init()');
this.setProperties('$elem');
}
// overrides app.modules.Overlay.prototype.show
,"show": function() {
// use apply for proper this binding (if not, this = window in Overlay.prototype.show)
overlayShow.apply(this);
console.log('modal.show()');
this.$elem.show();
}
};
// inherit from Overlay
Modal.prototype = $.extend(true, {}, app.modules.Overlay.prototype, prototype);
app.add.module('Modal', Modal);
return app;
})(window[AXS_NS] || {}, jQuery, Modernizr);