Showing posts with label namespace. Show all posts
Showing posts with label namespace. Show all posts

Thursday, February 26, 2015

How to structure you JS application

According to my reasearch which I was doing in past few months I got to some conclusions on how to structure your application for best code reusability.

Several patterns come to my mind, Module, Namespace, immediate functions, returning objects, using subobjects, using capitalized letters for functions/variables as "Constants", function scope and making accessor methods, JS constructors, using Yuidoc and Jasmine for better code quality.

Download the code from here: https://github.com/bluePlayer/practices/tree/master/AppStructure

Here is roughly what I am thinking about:

var externalJSLibrary = {},
    jquery = {},
    extJS = {},
    expressJS;

window.APP = window.APP || (function (wdo, lib1, lib2, lib3) {'use strict';
    var version = "1.0.0",
        config = {},
        parent = null,
        jqueryObj = lib1,
        extJSObj = lib2,
        expressJsObj = lib3,
        windowDocumentObject = wdo,
        MY_APP_NAME = "MyJSApplication",
        MY_EXCEPTION_MESSAGE = "My exception message",
        MY_APP_CONSTANT = "some message";

    return {
        Exceptions: {
            MyFirstException: function () {
                return {
                    message: MY_EXCEPTION_MESSAGE,
                    name: "MyFirstException"
                };
            }
        },

        Constants: {
            MY_APP_NAME: function () {
                return MY_APP_NAME;
            },
            MY_APP_CONSTANT: function () {
                return MY_APP_CONSTANT;
            }
        },

        Events: {
             keyDownEvent: function (event) {
                 // do some stuff when some key is pressed down on 'keydown' event.
             },

             keyUpEvent: function (event) {
                 // do some stuff when some key is released on 'keyup' event
             },

             keyPressEvent: function (event) {
                 // do some stuff when 'keypress' event was detected
             }
        },

        initApp: function (someObject, config) {
            var i;

            for (i in someObject) {
                if (someObject[i] !== null && typeof someObject[i] === 'object') {
                    if (someObject[i].hasOwnProperty('init')) {
                        someObject[i].init(config);
                    }
                    parent.initApp(someObject[i], config);
                }
            }
        },

        namespace: function (nsString, newObjectDefinition) {
            var parts = nsString.split('.'),
                helperObject = {},
                that = this,
                i = 0,
                field = {};

            if (parts[0] === that.Constants.MY_APP_NAME()) {
                parts = parts.slice(1);
            }

            for (i = 0; i < parts.length; i += 1) {
                if (that[parts[i]] === undefined) {
                    for (field in newObjectDefinition) {
                        if (newObjectDefinition.hasOwnProperty(field)) {
                            helperObject[field] = newObjectDefinition[field];
                        }
                    }
                    that[parts[i]] = helperObject;
                }
                that = that[parts[i]];
            }
            return that;
        },

        customFunction: function (a, b) {
            if (b < 0) {
                throw new parent.Exceptions.MyFirstException();
            } else {
                return 0;// do some stuff with a and b
            }
        },

        doSomeStuffWithJquery: function (someUrl) {
            jqueryObj.ajax({
                url: someUrl,
                xhrFields: {
                    withCredentials: true
                }
            });
        },

        doSomeStuffWithExtJS: function () {
            extJSObj.drawGraph();
        },

        doSomeStuffWithExpressJS: function () {
            expressJsObj.connectWithNodeJS();
        },

        init: function (configObject) {
            config = configObject;
            parent = this;
            parent.initApp(parent, config);
        }
    };
    }(window.document, jquery, extJS, expressJS));

window.APP.namespace('MySubObject', (function (lib) {'use strict';
    var config = {},
        parent = null,
        someLibrary = lib,
        MY_CONSTANT = "my constant",
        MY_EXCEPTION = "my exception";

    return {

        Exceptions: {
            MyNewException: function () {
                return {
                    message: MY_EXCEPTION,
                    name: "MyNewException"
                };
            }
        },

        Constants: {
            MY_CONSTANT: function () {
                return MY_CONSTANT;
            }
        },

        name: "MySubObject",

        myCustomFunction: function () {
            // do some stuff
        },

        init: function (configObj) {
            config = configObj;
            parent = this;
        }
    };
    }(externalJSLibrary)));

window.document.addEventListener("DOMContentLoaded", function (event) {'use strict';

    var myApp = window.APP;

    myApp.init({
        "event": event,
        otherConfig: {}
    });

    window.document.addEventListener('keydown', myApp.Events.keyDownEvent);
    window.document.addEventListener('keyup', myApp.Events.keyUpEvent);
    window.document.addEventListener('keypress', myApp.Events.keyPressEvent);

    });

Thursday, January 8, 2015

JavaScript Patterns - practice application

Hello. In this post I will explain some JavaScript patters using a script I made.
I have being reading this book: JavaScript Patterns by Stoyan Stefanov, and I covered half by now.

 You can download the script from this link https://github.com/bluePlayer/practices/tree/master/CalculatorJS

I also wrote comments for each property/method/object/module in YuiDoc format and generated in Java style, documentation using YuiDoc Node.js extension. For more info check this link: http://yui.github.io/yuidoc/

So the name of my application is APPLICATION and it is written with capitalized letters because it is global variable. Some JS coding convetions say that you should write constants and globals with capitalized letters because it is easier to distingush from other properties/variables.

It is always good to initialize main application object if it doesn't exist, or prevent initiazlization if it already exist:

var APPLICATION = {};
if (typeof APPLICATION === "undefined") {
    var APPLICATION = {};
}
// shorter
var APPLICATION = APPLICATION || {};

While I was reading "JavaScript Patterns" it mentioned a general namespace function which enables you to create objects and subobjects by just sending dot notated string, like this: "my.new.object".
I made some improvements to this function and added it as a part of the application's namespace, like this:

APPLICATION.namespace = function (nsString, newObjectDefinition) {
    var parts = nsString.split('.'),
        parent = this,
        newObject = {};
       
    newObject.Constants = new this.Constants();
    newObject.constMap = {};
   
    if (parts[0] === this.getName()) {
        parts = parts.slice(1);
    }
   
    for (var i = 0; i < parts.length; i += 1) {
        if (typeof parent[parts[i]] === "undefined") {           
            for (var property in newObjectDefinition) {
                if (property === property.toUpperCase()) {
                    newObject.Constants.set(property, newObjectDefinition[property]);
                    newObject.constMap[property] = property;
                } else {
                    newObject[property] = newObjectDefinition[property];
                }
            }
            parent[parts[i]] = newObject;
        }
        parent = parent[parts[i]];
    }   
   
    return parent;
};


I added a second parameter which adds functionality to the last object created in the tree. So if the fisrt parameter is string: "my.new.object", the function will create three objects, my, new and object in the APPLICATION's tree and object will be initialized with properties and functions from the second parameter. I also added two more subobjects inside every newly created object, so my, new, object will have Contants and constMap objects. Constants is object which gets created with Constants() constructor (from APPLICATION) and it contains, set(), get(), isDefined(), list(). On the other hand constMap contains the keys for calling constants using the get() function. So to get a constant value you should do this:

var myVar = APP.subModule.Constants.get(APP.subModule.constMap.CONST_NAME);

Note: This looks quite clumsy and big. Lot of letters to write. Perhaps it will be easier to write in some JS IDE like WebStrom. Anyway, at the time of writing this code I realised there is a place for improvement. Maybe there should be one general get() function for constants in the application.

As you can see from the application I use namespace pattern. All constructors, methods, events, properties, submodules should be part of one general object. Also I have submodules like, Main, Trigonometry, EventFunctions  and all related properties or methods are kept in its own submodule. This is called module pattern.

What if I want to add static properties? Well you can do it easily by just adding a new property to a given object or subobject. You can also use prototype part of the object and it will be inherited.
example:

var MyObject = function () {};
MyObject.name = "TunePHP";
var newObject = new MyObject();
typeof newObject.name === "undefined" //true
MyObject.prototype.name = MyObject.name;
console.log(newObject.name); // "TunePHP";

It is always useful to have a main init() method for the whole application, where you will initialize all necessary objects/variables/properties after the page has loaded.

Here is the complete code I wrote. Let me know your thoughts.

var APPLICATION = APPLICATION || {
    name: "Calculator",
    version: "1.0.0",
    Constants: function () {},
    namespace: function () {},
    getName: function () {
        return name;
    },
   
    init: function (config) {
        try {
            this.Calculator.Main.currentState = this.Calculator.Main.Constants.get(this.Calculator.Main.constMap.STANDARD);
            this.Calculator.Trigonometry.currentAngleFormat = this.Calculator.Trigonometry.Constants.get(this.Calculator.Trigonometry.constMap.DEGREES);
            return this;
        } catch (error) {
            console.log(error.message);
            return false;
        }
    }
};

APPLICATION.Constants = function (constMapObject) {
    var constants = {},
        ownProp = Object.prototype.hasOwnProperty,
        allowed = {
            string: 1,
            number: 1,
            boolean: 1
        },
        prefix = (Math.random() + "_").slice(2);
       
    return {
   
        set: function (name, value) {
            if (this.isDefined(name)) {
                return false;
            }
            if (!ownProp.call(allowed, typeof value)) {
                return false;
            }
                   
            constants[prefix + name] = value;
            /**
             * @todo use constMapObject to create new constant key in the given constants map object.
             */
            return true;
        },

        isDefined: function (name) {
            return ownProp.call(constants, prefix + name);
        },
       
        get: function (name) {
            if (this.isDefined(name)) {
                return constants[prefix + name];
            }
            return null;
        },
       
        list: function () {
            var index = 0,
                newStr = "";
               
            for (constant in constants) {
                index = constant.indexOf('_');
                newStr = constant.substr(index + 1);
                console.log(newStr + ": " + constants[constant]);
            }
        }
    };
};

APPLICATION.namespace = function (nsString, newObjectDefinition) {
    var parts = nsString.split('.'),
        parent = this,
        newObject = {};
       
    newObject.Constants = new this.Constants();
    newObject.constMap = {};
   
    if (parts[0] === this.getName()) {
        parts = parts.slice(1);
    }
   
    for (var i = 0; i < parts.length; i += 1) {
        if (typeof parent[parts[i]] === "undefined") {           
            for (var property in newObjectDefinition) {
                if (property === property.toUpperCase()) {
                    newObject.Constants.set(property, newObjectDefinition[property]);
                    newObject.constMap[property] = property;
                } else {
                    newObject[property] = newObjectDefinition[property];
                }
            }
            parent[parts[i]] = newObject;
        }
        parent = parent[parts[i]];
    }   
   
    return parent;
};

 APPLICATION.namespace('Calculator', {
   
});

APPLICATION.namespace('Calculator.Main', {
   
    STANDARD: 1,

    SCIENTIFIC: 2,

    PROGRAMMER: 3,
   
    currentState: null,

    currentValue: 0,

    memoValue: 0,
   
    digitGrouping: false,
   
    historyPanel: false,
   
    unitConversionPanel: false,

    dateCalculationPanel: false,

    changeState: function (event) {
        /**
         * @todo currentState = event.newState;
         */
    },
       
    showHistoryPanel: function (event) {
        /**
         * @todo
         */
    },
       
    showUnitConversionPanel: function (event) {
        /**
         * @todo
         */
    },

    showDateCalculationPanel: function (event) {
        /**
         * @todo
         */
    }
});

APPLICATION.namespace('Calculator.Trigonometry', {

    DEGREES: "degrees",
   
    RADIANS: "radians",

    GRADS: "grads",
   
    currentAngleFormat: null
});

APPLICATION.namespace('Calculator.EventFunctions', {

    pressZero: function (event) {
   
    },

    pressOne: function (event) {
   
    },

    pressTwo: function (event) {
   
    },
   
    pressSum: function (event) {
   
    },

    pressSubtract: function (event) {
   
    },
   
    pressMultiply: function (event) {
   
    },

    pressDivide: function (event) {
   
    }
});

APPLICATION.init({});