/**
* Attach javascript classes to HTML Nodes by subclassing Autowire.
*
* Some convenience features (like easy fetching) are included.
*
*/
Autowire = class {
/**
*
* @param node a node to attach to or querySelector to create a new node from an existing template node
* @param args to pass to onCreate(){} and onAttach(){}
*/
constructor(node, ...args){
this.readyCalled = false;
/**
* Class name
*/
this.name = this.constructor.name;
/**
* Node this class is attached to
*/
this.node = node;
/**
* Alias for this.node
*/
this.n = node;
this.onCreate(...args);
this.constructor.attach(this);
this.onAttach(...args);
}
/**
* shorthand for this.node.innerHTML;
* @tag shorthand
*/
get innerHTML(){
return this.node.innerHTML;
}
/**
* shorthand for this.node.innerHTML = value;
* @tag shorthand
*/
set innerHTML(value){
this.node.innerHTML = value;
}
/**
* Initialize your class BEFORE listeners are setup, AFTER this.node is set
*
* @param ...args any args passed to the constructor
*/
onCreate(){}
/**
* Initialize your class AFTER listeners are setup.
*
* @param ...args any args passed to the constructor
*/
onAttach(){}
/**
* Called when all pending Autowire attachments have been completed.
* This is NOT called if you instantiate directly (`new MyAutowireSubclass(node)`)
*
* @param ...args any args that were passed to the constructor
*/
onReady(){}
/**
*
* Get an object that's a descendent of this object.
* If this object contains multiple descendents, use `getAll()`
*
* @param object_class_name string class name of the autowire sub-class
*
* @return Autowire subclass object or null
*/
getObject(object_class_name){
return this.getAll(object_class_name)[0] || null;
}
/**
*
* Get all objects descending from this object.
*
* @param object_class_name string class name of the autowire sub-class
*
* @return array of autowire subclass objects. May be empty array.
*/
getObjects(object_class_name){
return this.constructor.getObjectsFromName(object_class_name,this);
}
/**
* Get objects anywhere in the dom (not necessarily children)
*
* @param object_class_name string the class name of an autowire subclass
* @return array an array of autowire objects
*/
getGlobalObjects(object_class_name){
return this.constructor.getObjectsFromName(object_class_name);
}
/**
* Wire the called class class to the target nodes. (one instance for each node)
*
* @param querySelector string query selector of one or multiple nodes.
* @param onPageLoad bool true to attach after page has loaded. Will attach immediately if page has already loaded. default is true
* @param ...args array arguments to pass to onCreate(...args) and onReady(...args)
*
* @return void
*/
static autowire(querySelector=null, onPageLoad=true, ...args) {
Autowire.readyCount++;
if (onPageLoad){
this.onPageLoad(this.attachSelector,this,querySelector,true,...args);
} else {
this.attachSelector(querySelector,false,...args);
}
}
/**
* Immediately attach this class to the target nodes.
*
* Calls `onReady()` if the autowire queue is empty.
*
* @param querySelector a query selector
* @param ...args array arguments to pass to onCreate(...args) and onReady(...args)
*
* @return void
*/
static attachSelector(querySelector, ...args){
Autowire.readyCount--;
let aw = null;
for (const node of document.querySelectorAll(querySelector)){
if (this.getObjectFromNode(node)!==null)continue;
aw = new this(node, ...args);
Autowire.waitingForReady.push(aw);
}
if (Autowire.readyCount==0){
this.readyUpAllObjects();
}
}
/**
* Clone a template and attach a new instance of this class to it.
*
* If non-template: Simply clone the node
* If template: Clone the template's content node. If there are multiple root nodes, they'll be wrapped in a <div>
*
* @param querySelector string query selector to find the template
*
* @return Autowire instance/subclass
*/
static attachTemplate(querySelector, ...args){
let node = null;
const template = document.querySelector(querySelector);
if (template == null){
console.error(querySelector + " is not a valid query selector");
return null;
}
if (template.tagName.toUpperCase()=='TEMPLATE'){
if (template.content.children.length>1){
node = document.createElement('div');
node.appendChild(template.content.cloneNode(true));
} else {
node = template.content.children[0].cloneNode(true);
}
} else {
node = template.cloneNode(true);
}
const Obj = new this(node, ...args);
return Obj;
}
/**
* Fetch json from a web request
*
* @param url string a url
* @param params array<string key, mixed value> of paramaters
* @param method the http verb, like POST, GET, PUT, etc. POST is default
*
* @return object any object representable by json.
*/
async fetchJson(url, params=[], method="POST"){
const res = await this.fetch(url,params,method);
const json = await res.json();
return json;
}
/**
* Fetch text from a web request
*
* @param url string a url
* @param params array<string key, mixed value> of paramaters
* @param method the http verb, like POST, GET, PUT, etc. POST is default
*
* @return string text from the response
*/
async fetchText(url, params=[], method="POST"){
const res = await this.fetch(url,params,method);
const text = await res.text();
return text;
}
/**
* Fetch a url & get a Response
*
* @param url string a url
* @param params array<string key, mixed value> of paramaters
* @param method the http verb, like POST, GET, PUT, etc. POST is default
*
* @return Response object
*/
async fetch(url, params, method="POST"){
var formData = new FormData();
this.constructor.addParamsToFormData(formData, params);
const result = await fetch(url, {
method: method,
mode: "cors",
body: formData
})
return result;
}
/**
* Add Params to a FormData object for submission
*
* @param formData FormData object
* @param params array<string key, mixed value> of paramaters
*/
static addParamsToFormData(formData, params){
for(var key in params){
const param = params[key];
// if (typeof param == typeof []){
if (Array.isArray(param)){
key = `${key}[]`;
for(const val of param){
if (typeof [] === typeof val){
} else {
formData.append(key,val);
}
}
} else formData.append(key,params[key]);
}
}
/**
* Get all objects for the given name
*
* @param object_class_name string class name or null to use the called-class's name.
* @param parentObj An Autowire object (not a node). Its a reference object to only get children, or null to search full DOM
*
* @return array<int index, Autowire autowire_subclass>
*/
static getObjects(object_class_name=null,parentObject=null){
object_class_name = object_class_name || this.prototype.constructor.name;
const all = [];
for (const item of Autowire.wiredList){
if (item.obj.constructor.name == object_class_name){
all.push(item.obj);
}
}
if (parentObject==null)return all;
const parentNode = parentObject.node;
const filtered = [];
for (const child of all){
let check = child.node;
do {
check = check.parentNode;
if (check==parentNode){
filtered.push(child);
}
}
while (check!=document.body&&check!=parentNode);
}
return filtered;
}
/**
* Get the autowire object attached to a node
*
* @param node Node a DOM Node
* @return Autowire object or null
*/
static getObjectFromNode(node){
for (const row of Autowire.wiredList){
if (row.node===node)return row.obj;
}
return null;
}
/**
* Call onReady() on all wired nodes that have not previously readied up
*
*/
static readyUpAllObjects(){
const waiting = Autowire.waitingForReady;
Autowire.waitingForReady = [];
for (const aw of waiting){
if (aw.readyCalled)continue;
aw.readyCalled = true;
aw.onReady();
}
}
/**
* Setup event listeners on the attached node.
*
* @param autowire_object Autowire subclass instance
*/
static attach(autowire_object){
Autowire.wiredList.push({'node':obj.node,'obj':obj});
for (const methodName of this.getMethods(obj)){
if (typeof obj[methodName] !== 'function'
||methodName.substring(0,2)!=='on'
||methodName=='onCreate'
||methodName=='onAttach'
||methodName=='onReady')continue;
const method = obj[methodName].bind(obj);
const eventName = methodName.substring(2);
obj.node.addEventListener(eventName,method);
}
}
/**
* Run the function on page load, or immediately if page is already loaded
*
* @param func a function
* @param thisArg what `this` should refer to inside `func`
* @pram ...args array of args to pass to func
*
* @return void
*/
static onPageLoad(func, thisArg, ...args) {
if (document.readyState == 'interactive' || document.readyState == 'complete') {
func.apply(thisArg, args);
} else if (document.addEventListener != null) {
document.addEventListener("DOMContentLoaded", function () {
if (document.readyState == "interactive") {
func.apply(thisArg, args);
}
});
} else {
window.onload = function () {
func.apply(thisArg, args);
};
}
}
/**
* Get an array of methods from a javascript object
*
* @param javascript_object object
* @return array<int index, string method_name>
*/
static getMethods(javascript_object){
const properties = new Set();
let currentObj = javascript_object;
do {
Object.getOwnPropertyNames(currentObj).map(item => properties.add(item))
} while ((currentObj = Object.getPrototypeOf(currentObj)))
return [...properties.keys()].filter(item => typeof javascript_object[item] === 'function')
}
}
/** int number of querySelectors waiting to be executed & wired to nodes */
Autowire.readyCount = 0;
/** array<int index, Autowire autowire_subclass> objects waiting for onReady() to be called. */
Autowire.waitingForReady = [];
/** array<int index, array wired_item> where wired_item contains key 'node' referring to DOM Node and 'obj' referring to Autowire subclass object. */
Autowire.wiredList = [];