Autowire.js

/** This is an old version of the notes. You should view the docs instead.
 * 
 * This allows you to declare a class in Javascript, list events on an HTML node, and have functionality from the JS class automatically wired to the node.
 * 
 * 
 * 
 * The basic functionality is:
 *  - Extend Autowire.DomWire with your JS class
 *  - To auto-wire call YourClassName.autowire();
 *      - AutoWiring is done when the page is finished loading, NOT at the time you call autowire()
 *      - By Default looks for rb-YourNamespacedClassName on DOM Elements
 *      - To customize, you may: 
 *          - call YourClassName.autowire('DomeClass');
 *          - declare YourClassName.querySelector = 'DomClass';
 *          - declare YourClassName.getquerySelector = function(){return 'DomClass';};
 *      - NOTE: Precedence is given to autoWire('DomClass'), then YourClassName.querySelector, then YourClassName.getquerySelector()
 *  - Set Event methods in YourClass corresponding to JS Event Listeners.
 *      - Inside your class, set methods like `onclick(event)`, `mouseover`, `mouseout`, etc & they will be auto-wired to Dom Nodes
 *      - Ex: class YourClass extends Autowire.DomWire {  onclick(event){console.log(this);    console.log(event);   console.log(this.node)};
 *          - `this` refers to the instance of YourClass
 *          - `this.node` refers to the node which the listener was set on
 *          - `event` refers to the javascript event that was triggered
 *              - `event.target` may be different from `this.node` due to event propagation
 *  - Methods are assigned using theNode.addEventListener. 
 *      - You may declare this.eventMode = 'static' in your __construct() method to use theNode.onclick = YourClass.onclick;
 *  - You are encouraged to override the methods __construct() && __attach() in your subclass. The super method does not do anything
 *      - __construct() is called before attach()
 *      - __attach() is called AFTER attach()
 *      - You may override constructor() && attach() but it is NOT recommended & super method will need to be called
 *  - Set any additional methods and paramaters that you like, 
 *      - except for
 *          params: node, eventNode
 *          methods: getChildMethods, attachMethod (as well as the methods mentioned above)
 * 
 */
Autowire = class {

    constructor(node){
        this.context = 'default';
        Autowire.list.push({'node':node,'obj':this});
        this.name = this.name || this.constructor.name;
        this.eventMode = 'addEventListener';
        this.node = node;
        this.__construct();
        this.attach();
        this.__attach();
        this.didAttach();
    }
    __construct(){}
    __attach(){}
    attach(){
        const childMethods = this.getChildMethods();
        for (const methodName of childMethods){
            this.attachMethod(methodName);
        }
    }
    didAttach(){}   
    getChildMethods(){
        const methodNames = Autowire.Tools.getObjectMethodNames(this);
        const rootMethods = Autowire.Tools.getObjectMethodNames(Autowire.prototype);
        const childMethods = methodNames.filter(method => rootMethods.indexOf(method)<0);
        return childMethods;
    }
    attachMethod(methodName){
        if (methodName.substring(0,2)!=='on')return false;
        const method = this[methodName].bind(this);
        if (this.eventMode=='addEventListener'){
            const eventName = methodName.substring(2);
            this.node.addEventListener(eventName,method);
            return true;
        } else if (this.eventMode=='static'){
            this.node[methodName] = method;
            return true;
        } else {
            throw "event mode must be 'addEventListener' or 'static'";
        }
    }
    nodeContext(node,name='default'){
        return name+"-"+Autowire.Tools.objectId(this.node.parentNode); 
    }
    get(objectName,contextName=null){
        if (contextName==null)contextName = this.context.slice(0,1);
        const map = Autowire.contextMap[contextName];
        const values = map[objectName];
        if (values==null||values.length==0)return [];
        else return values;
    }
    didAttach(){
        const map = Autowire.contextMap || {};
        if (typeof [] != typeof this.context)this.context = [this.context];
        for (const name of this.context){
            map[name] = map[name] || {};
            map[name][this.name] = map[name][this.name] || [];
            map[name][this.name].push(this);
        }
        Autowire.contextMap = map;
        const proto = this.constructor.prototype;
        let keys = [];
        let obj = this.constructor.prototype;
        
        do keys = keys.concat(Object.getOwnPropertyNames(obj));
            while ((obj = Object.getPrototypeOf(obj))!=Object.prototype);
        const props = keys.filter(function(value,index,self){return self.indexOf(value)===index;});
        
        for (const prop of props){
            if (prop.charAt(0)=='_'&&prop.charAt(1)!='_'){
                const name = prop.slice(1);
                const ret = this[prop]() || name;
                Object.defineProperty( this, name, {
                            get : function(property){
                                if (typeof [] == typeof property){
                                    let i = -1;
                                    let output = '';
                                    for (const val of property){
                                        i++;
                                        if (val==null)continue;
                                        output = val;
                                        break;
                                    }
                                    const contextEntry = this.context[i];
                                    return this.get(output,contextEntry);
                                } else {
                                    return this.get(property);
                                }
                                
                            }.bind(this,ret)
                        } );
                const oneName = 'one'+name.charAt(0).toUpperCase() + name.slice(1);
                Object.defineProperty( this, oneName, {
                    get : function(property){
                        if (typeof [] == typeof property){
                            let i = -1;
                            let output = '';
                            for (const val of property){
                                i++;
                                if (val==null)continue;
                                output = val;
                                break;
                            }
                            const contextEntry = this.context[i];
                            const array = this.get(output,contextEntry);
                            if (array!=null&&array.length>0)return array[0];
                            else return null;
                        } else {
                            const array = this.get(property);
                            if (array!=null&&array.length>0)return array[0];
                            else return null;
                        }
                        
                    }.bind(this,ret)
                } );
            }
        }
    }
    send(url,data,responseMethod,method="POST"){
        var req = new Autowire.Request(url,method);
        req.handleJson(responseMethod.bind(this));
    }

    static autowire(querySelector=null) {
        Autowire.readyCount++;
        Autowire.ready = false;
        if (querySelector!=null)this.querySelector = querySelector;
        Autowire.Tools.onPageLoad(this.wire, this);
    }
    static wire(){
        const nodes = this.getNodes();
        const pending = [];
        for (const node of nodes){
            const obj = this.wireNode(node);
            pending.push(obj);
        }
        Autowire.readyCount--;
        Autowire.ready = Autowire.readyCount===0;
        if (Autowire.ready){
                for (const obj of pending){
                    if (obj.__readyCalled
                        ||typeof obj.__ready !== typeof function(){})continue;
                    obj.__readyCalled = true;
                    obj.__ready();
                }
                for (const obj of Autowire.readyPending){
                    if (obj.__readyCalled
                        ||typeof obj.__ready !== typeof function(){})continue;
                    obj.__readyCalled = true;
                    obj.__ready();
                }
                Autowire.readyPending = [];
        }
    }

    static fromNode(nodeObj){
        for (const row of this.list){
            if (row.node===nodeObj)return row.obj;
        }
    }
    static getQuerySelector(){
        return this.querySelector || '.'+(this.className || this.name);
    }
    
    static getNodes(){
        const querySelector = this.getQuerySelector(); 
        let nodes = [];
        try {
            nodes = document.querySelectorAll(querySelector);
        } catch (e){
            nodes = [];
        }
        return nodes;
    }
    
    static wireNode(node){
        const obj = new this(node);
        if (Autowire.ready
            &&(typeof obj.__ready === typeof function(){})){
            obj.__ready();
        } else if (!Autowire.ready){
            // should the readycount be incremented here???
            // I don't think so
            Autowire.readyPending.push(obj);
        }
        return obj;
    }
}
Autowire.readyCount = 0;
Autowire.ready = true;
Autowire.readyPending = [];

Autowire.list = [];