src/element/element.builder.js
/**
* Class represents ElementBuilder
* @class ElementBuilder
*/
class ElementBuilder {
/**
* Create ElementBuilder instance
* @param {ElementSignature[]} signatures
* @param {function} templateEngine
* @param {function} schemaValidator
* @param {object} options
*/
constructor(signatures = [], templateEngine = null, schemaValidator = null, options = {}) {
this._schemaValidator = (
schemaValidator &&
typeof schemaValidator === 'function'
)
? new schemaValidator()
: null;
this._templateEngine = (typeof templateEngine === 'object')
? templateEngine
: null;
this._signatures = (signatures instanceof Array)
? this._transformToObject(signatures)
: {};
this._elements = {};
this._options = options;
}
/**
* transform signatures array to {name: signature} object
* @param {ElementSignature[]} signatures
* @returns {object}
* @private
*/
_transformToObject(signatures) {
let obj = {};
if(signatures instanceof Array) {
signatures.forEach((signature) => {
if(signature && typeof signature.name === 'string') {
obj[signature.name] = signature;
}
});
}
return obj;
}
/**
* checks if element exists
* @param {ElementSignature.<name>} name
* @return {boolean}
* @private
*/
_elementExists(name) {
return (
!!name &&
typeof name === 'string' &&
typeof this._elements[name] === 'object' &&
!!this._elements[name]
);
}
/**
* get registered element
* @param {ElementSignature.<name>} name
* @return {object|null}
*/
getElement(name) {
return (this._elementExists(name))
? this._elements[name]
: null;
}
/**
* get signature
* @param {ElementSignature.<name>} name
* @return {object|null}
*/
getSignature(name) {
return (this._signatureExists(name))
? this._signatures[name]
: null;
}
/**
* remove signature from registry
* @param {ElementSignature.<name>} name
* @return {ElementBuilder}
*/
removeSignature(name) {
if(this._signatureExists(name)) {
delete this._signatures[name];
}
return this;
}
/**
* checks if signature exists
* @param {ElementSignature.<name>} name
* @return {boolean}
* @private
*/
_signatureExists(name) {
return (
typeof this._signatures[name] === 'object' &&
!!this._signatures[name] &&
typeof this._signatures[name].name === 'string'
);
}
/**
* checks if signature is currently loading
* @param {ElementSignature.<name>} name
* @return {boolean}
*/
isBusySignature(name) {
let signature = this.getSignature(name);
return !!(signature && signature.busy);
}
/**
* sets busy flag to
* @param {ElementSignature.<name>} name
* @return {ElementBuilder}
*/
setBusySignature(name) {
let signature = this.getSignature(name);
if(signature) {
this._signatures[name].busy = true;
}
return this;
}
/**
* get template innter html
* @param {string} template
* @param {object} data
* @return {string}
*/
getTemplateInnerHtml(template, data) {
return (
this._templateEngine &&
typeof this._templateEngine === 'object' &&
typeof this._templateEngine.render === 'function'
)
? this._templateEngine.render(template, data)
: template;
}
/**
* get template without shadow dom support
* @param {string} template
* @param {object} data
* @return {Node}
* @private
*/
getTemplateElement(template, data) {
const templateFeature = ('content' in document.createElement('template'));
const templateElement = (templateFeature)
? document.createElement('template')
: document.createDocumentFragment();
templateElement.innerHTML = this.getTemplateInnerHtml(template, data);
return (templateFeature)
? templateElement.content
: templateElement;
}
/**
* get schema reference
* @param {ElementSignature.<name>} name
* @return {object}
*/
getSchema(name) {
const element = this.getElement(name);
return (
element &&
typeof element === 'object' &&
typeof element.schema !== 'undefined'
)
? element.schema
: null;
}
/**
* validate data against schema
* @param {ElementSignature.<name>} elementName
* @param {*} data
* @return {boolean}
* @private
*/
_validate(elementName, data) {
if(!this._elementExists(elementName)) {
return false;
}
const schema = this.getSchema(elementName);
if(
this._schemaValidator &&
schema
) {
return this._schemaValidator.validate(schema, data);
} else {
return true;
}
}
/**
* adds element to registry
* @param {ElementSignature.<name>} name
* @param {object} schema
* @param {string} template
* @param {ElementAbstract} module
* @return {ElementBuilder}
*/
addElement(name, schema, template, module) {
const templateView = (
this._templateEngine &&
typeof this._templateEngine === 'object'
)
? this._templateEngine.createView(template)
: template;
this._elements[name] = {
schema,
template: templateView,
module,
};
return this;
}
/**
* generate element instance
* @param {ElementSignature.<name>} name
* @param {object} data
* @private
*/
_generateElement(name, data) {
if(this._validate(name, data)) {
const element = this.getElement(name, data);
const elementInstance = new element.module(
data,
this.getTemplateElement(element.template, data),
);
return elementInstance.create();
} else {
throw new Error(`Create Element ${name} failed. Given data do not match given schema.`);
}
}
/**
* load element module
* @param {ElementSignature.<name>} name
* @param {object} data
* @return {Promise.<TResult>}
* @private
*/
async _loadElementModule(name, data) {
const signature = this.getSignature(name);
this.setBusySignature(name);
return await Promise.all([
signature.importSchema(),
signature.importTemplate(),
signature.importElement(),
])
.then((imports) => {
this.addElement(name, imports[0], imports[1], imports[2]);
if(this._elementExists(name)) {
this.removeSignature(name);
return this.create(name, data);
} else {
throw new Error(`Unfortunately Element ${name} could not have been instanciated.`);
}
})
.catch((err) => {
throw new Error(`Unfortunately Element ${name} could not have been instanciated. ${err}`);
});
}
/**
* retry create element loop when
* same element signature has to load
* multiple times at the same time
* @param {Elementsignature.<name>} name
* @param {object} data
* @return {Promise}
* @private
*/
_retryCreate(name, data) {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
try {
resolve(this.create(name, data));
} catch(err) {
reject(err);
}
}, 100);
});
}
/**
* loads dependency creates element by name and data
* @param {ElementSignature.<name>} name
* @param {*} data
* @return {Promise}
*/
async create(name, data) {
try {
const elementExists = this._elementExists(name);
const signatureExists = this._signatureExists(name);
const signatureIsBusy = this.isBusySignature(name);
if(!elementExists && !signatureExists) {
return null;
}
if(elementExists) {
return this._generateElement(name, data);
}
return (signatureExists && !signatureIsBusy)
? this._loadElementModule(name, data)
: this._retryCreate(name, data);
} catch(err) {
return null;
}
}
/**
* get options
* @return {Object}
*/
getOptions() {
return this._options;
}
/**
* get element ready class
* @return {string|null} elementReadyClass
*/
getElementReadyClass() {
return (typeof this._options.elementReadyClass === 'string')
? this._options.elementReadyClass
: null;
}
}
export {
ElementBuilder,
};