@koga73/oop

This project when combined with design patterns adds common OOP functionality to JavaScript

Usage no npm install needed!

<script type="module">
  import koga73Oop from 'https://cdn.skypack.dev/@koga73/oop';
</script>

README

Build Status Support: IE8+ Dependencies: None License: MIT

OOP JS

This project when combined with design patterns adds common OOP functionality to JavaScript

Goals:

  • Provide OOP like functionality including namespacing, classes, inheritance, public / private scope
  • Provide a cross-browser event model that can be added to any object
  • Provide methods for cloning and extending objects
  • Provide methods for type checking

Install:

npm i @koga73/oop

Run unit tests:

Run NodeJS unit test:
npm test
Run HTML/JS unit test:
tests/test.html

examples/pong snippet:

(function(){
    //Imports from Pong example
    var Models = Pong.Models;
    var DomRenderer = Pong.Renderers.DomRenderer;
    var InputManager = Pong.Managers.InputManager;
    var NormalTimer = Utils.NormalTimer;
    
    //This returns a reference to the class. We are using the word "_class" as a shortcut to easily access static members
    var _class =
    namespace("Foo.Bar.Pong",
    construct({
    
        //Constants
        static:{
            ID_BALL:"ball",
            SPEED_BALL:400, //Pixels-per-second
            
            singleton:null,
            getSingleton:function(){
                if (!_class.singleton){
                    _class.singleton = new Foo.Bar.Pong(
                        _class.ID_BALL
                    );
                }
                return _class.singleton;
            }
        },

        //In general _public is optional since it's the same as "this" (except in the scope of a private method)
        //Properties and methods starting with an underscore '_' are private
        instance:function(_private, _public){
            return {
                ball:null,

                normalTimer:null,
                inputManager:null,

                _renderer:null,
                _renderQueue:[],

                __construct:function(ballId){
                    //Init game objects
                    this.ball = new Models.Object2D(ballId);

                    //Init game timer
                    this.normalTimer = new NormalTimer({
                        paused:false,
                        onTick:_private._onTick
                    });
                    this.inputManager = InputManager.getSingleton();

                    //Init renderer
                    _private._renderer = new DomRenderer();
                    _private._renderQueue = [
                        this.ball
                    ];

                    //--- LISTEN FOR EVENTS ---

                    //Start
                    this.resetBall();
                },

                //Delta is the time elapsed between ticks
                //Multiplying a movement value by delta makes the movement timebased rather than based on the clock cycle
                _onTick:function(delta){
                    //Move
                    var queueLen = _private._renderQueue.length;
                    for (var i = 0; i < queueLen; i++){
                        var obj2d = _private._renderQueue[i];
                        obj2d.x += obj2d.moveX * delta;
                        obj2d.y += obj2d.moveY * delta;
                    }
                    
                    //--- DO LOGIC ---
                    //Do something with _class.SPEED_BALL

                    //Render
                    _private._renderer.render(_private._renderQueue);
                },

                resetBall:function(){
                    //--- DO LOGIC ---
                }
            };
        }
        
    }));
})();

Simple class:

OOP.namespace("foo.bar.Shape", OOP.construct({
    instance:{
        width:100,
        height:200
    }
}));

var defaultShape = new foo.bar.Shape();
console.log(defaultShape);

Change scope:

This allows you to change the scope of OOP methods so don't have to put "OOP" all over the place.

//Add OOP methods to window/global by default or to any object passed in
OOP.changeScope();

namespace("foo.bar.Shape", construct({
    instance:{
        width:100,
        height:200
    }
}));

var defaultShape = new foo.bar.Shape();
console.log(defaultShape);

Constructor:

OOP.namespace("foo.bar.Shape", OOP.construct({
    instance:{
        width:100,
        height:200,

        __construct:function(newWidth, newHeight){
            this.width = newWidth || this.width;
            this.height = newHeight || this.height;
            console.log("This gets called when a new instance is created", this.width, this.height);
        }
    }
}));

//Note that if we pass in non-object parameters they will get sent to the constructor
//Object type parameters will apply value overrides to the instance
var customShape = new foo.bar.Shape(300, 400, {
    test:"this prop gets added to the instance"
});
console.log(customShape);

Static:

OOP.namespace("foo.bar.Shape", OOP.construct({
    instance:{
        width:100,
        height:200,
    },

    static:{
        SOME_CONST:123,

        getArea:function(obj){
            return obj.width * obj.height;
        }
    }
}));

var instance = new foo.bar.Shape({
    width:300,
    height:400
});
console.log(foo.bar.Shape.SOME_CONST);
console.log(foo.bar.Shape.getArea(instance));

Public / Private scope:

OOP.namespace("foo.bar.Shape", OOP.construct({
    instance:function(_private, _public){
        return {
            width:100,
            height:200
            _thisIsPrivate:"some message",

            getThisIsPrivate:function(){
                return _private._thisIsPrivate;
            }
        };
    }
}));

var defaultShape = new foo.bar.Shape();
console.log(defaultShape);

Note that when a function is passed to "instance" a public and private scope is created. Anything starting with an underscore '_' is private. You can use the _private and _public references passed into the function to call between scopes. Optionally "this" refers to the _public or _private scope respectively. You can also use these _public and _private variables to avoid creating event delegates for "this".


Inheritance:

OOP.namespace("foo.bar.Shape", OOP.construct({
    instance:{
        width:100,
        height:200
    },

    static:{
        getArea:function(obj){
            return obj.width * obj.height;
        }
    }
}));

OOP.namespace("foo.bar.Triangle", OOP.inherit(foo.bar.Shape, OOP.construct({
    instance:{
        width:300,
        angles:[30, 60, 90]
    },

    static:{
        getArea:function(obj){
            return obj.width * obj.height * 0.5;
        }
    }
})));

var triangle = new foo.bar.Triangle();
console.log(OOP.isType(triangle, foo.bar.Triangle)); //true
console.log(OOP.isType(triangle, "foo.bar.Triangle")); //true
console.log(OOP.isType(triangle, foo.bar.Shape)); //true
console.log(triangle._interface); //Triangle instance
console.log(triangle._super); //Shape instance
console.log(triangle._super._interface); //Triangle instance
console.log(triangle._type); //"foo.bar.Triangle"
console.log(triangle._super._type); //"foo.bar.Shape"

Events:

OOP.namespace("foo.bar.Shape", OOP.construct({
    instance:{
        width:100,
        height:200
    },
    events:true,

    static:{
        getArea:function(obj){
            return obj.width * obj.height;
        }
    }
}));

var instance = new foo.bar.Shape();
instance.addEventListener("test-event", function(evt, data){
    console.log("Got event", evt, data);
});

instance.dispatchEvent(new OOP.Event("test-event", 123));

Note that events fired from inherited classes (_super) will bubble up (they share the same _eventHandlers)

Add events to any object:

var myObj = {};
OOP.addEvents(myObj);

myObj.addEventListener("test-event", function(evt, data){
    console.log("Got event", evt, data);
});

myObj.dispatchEvent(new OOP.Event("test-event", 123));

Clone:

var obj = OOP.clone({foo:{bar:"foobar"}}); //Makes a deep copy - The foo objects will be different
var obj = OOP.clone({foo:{bar:"foobar"}}, false); //Makes a shallow copy - The foo objects will be the same reference

Extend:

var foo = {abc:123};
var bar = {def:{ghi:456}};

//Extend bar onto foo - deep by default meaning foo.def will not equal bar.def
OOP.extend(foo, bar);

//Extend bar onto foo - shallow meaning foo.def and bar.def will be the same reference
OOP.extend(foo, false, bar);

//Extend bar onto foo - shallow meaning foo.def and bar.def will be the same reference but the third object is deep again
OOP.extend(foo, false, bar, true, {jkl:{mno:789}});

Note the number of arguments is unlimited. When a boolean is encountered it sets the "deep" flag for subsequent objects. The first object found in the arguments is what gets extended (you could pass true/false as the first argument).

Type checks:

OOP.isType
OOP.isFunction
OOP.isArray
OOP.isObject
OOP.isString
OOP.isBoolean
OOP.isRegExp

Full API:

init:_methods.init,

//Class
namespace:_methods.namespace,
inherit:_methods.inherit,
createClass:_methods.createClass,
construct:_methods.construct,

//Core
clone:_methods.clone,
extend:_methods.extend,

//Type checks
isType:_methods.isType,
isFunction:_methods.isFunction,
isArray:_methods.isArray,
isObject:_methods.isObject,
isString:_methods.isString,
isBoolean:_methods.isBoolean,
isRegExp:_methods.isRegExp,

//Events
Event:_methods.event,

addEvents:_methods.addEvents,
removeEvents:_methods.removeEvents,

addEventListener:_methods.addEventListener,
on:_methods.addEventListener, //Alias

removeEventListener:_methods.removeEventListener,
off:_methods.removeEventListener, //Alias

dispatchEvent:_methods.dispatchEvent,
trigger:_methods.dispatchEvent, //Alias
emit:_methods.dispatchEvent //Alias