constructors/js-template-component.js

//@
/**
 * @example
 *  Application.require('js-template-component')
 *      .then(function (
 *          // @type {JSTemplateComponent}
 *          JSTemplateComponentConstructor
 *      ) {
 *          new JSTemplateComponentConstructor(
 *              'my-custom-tagname',
 *              {
 *                  context: {
 *                      numberOfClicks: 0
 *                  },
 *                  templateCode: `
 *                      <div>
 *                          {{ this.numberOfClicks }}
 *                          <button (click)="this.increaseClicksNumber()">
 *                          </button>
 *                      </div>
 *                  `,
 *                  sharedPrototypeMethods: {
 *                      increaseClicksNumber: function () {
 *                          this.numberOfClicks += 1;
 *                      }
 *                  },
 *                  sharedReferences: {},
 *                  cssStyles: [
 *                      '/some/styles/style.css'
 *                  ]
 *              },
 *              function (err) {
 *                  if (err) console.error(err);
 *              }
 *          );
 *      }).catch(console.error);
 * @class
 * @name JSTemplateComponent
 * @param {string} tagName
 * @param {JSTemplateComponent.options} options
 * @param {JSTemplateComponent.constructorCallback} callback
 */;

/**
 * @memberof JSTemplateComponent
 * @callback Builder
 * @param {string} tagName
 * @param {JSTemplateComponent.options} options
 * @param {JSTemplateComponent.constructorCallback} callback
 * @returns {JSTemplateComponent}
 */

/**
 * @memberof JSTemplateComponent
 * @interface contextInstance
 */
/**
 * @memberof JSTemplateComponent.contextInstance
 * @method redraw
 */
/**
 * @memberof JSTemplateComponent.contextInstance
 * @method redrawForce
 */
/**
 * @memberof JSTemplateComponent.contextInstance
 * @name references
 * @type {Object<string,any>}
 */
/**
 * @memberof JSTemplateComponent.contextInstance
 * @name node
 * @type {HTMLElement}
 */

/**
 * @memberof JSTemplateComponent
 * @typedef {object} contextLifeCycle
 * @property {JSTemplateComponent.lifeCycleCallback} [init] callbacks on init
 * @property {JSTemplateComponent.lifeCycleCallbackGetReferences} [getReferences] returns
 * @property {JSTemplateComponent.lifeCycleCallback} [contentChange] callback on content change
 * @property {JSTemplateComponent.lifeCycleCallback} [attrChange] context object default is Node
 * @property {JSTemplateComponent.lifeCycleCallback} [remove] context object default is Node
 */

/**
 * @memberof JSTemplateComponent
 * @interface contextWithInstance
 * @implements JSTemplateComponent.contextInstance
 */

/**
 * @memberof JSTemplateComponent.contextWithInstance
 * @name __instance
 * @type {JSTemplateComponent.contextInstance}
 */
/**
 * @memberof JSTemplateComponent.contextWithInstance
 * @description context object default is Node
 * @name __lifeCycle
 * @type {JSTemplateComponent.contextLifeCycle}
 */
/**
 * @memberof JSTemplateComponent.contextWithInstance
 * @description context object default is Node
 * @name state
 * @type {Object<string,(string|number|Object<string,any>|null|any[])>}
 */

/**
 * @memberof JSTemplateComponent
 * @interface contextWithoutInstance
 */
/**
 * @memberof JSTemplateComponent.contextWithoutInstance
 * @description context object default is Node
 * @name __lifeCycle
 * @type {JSTemplateComponent.contextLifeCycle}
 */
/**
 * @memberof JSTemplateComponent.contextWithoutInstance
 * @description context object default is Node
 * @name state
 * @type {Object<string,(string|number|Object<string,any>|null|any[])>}
 */

/**
 * @memberof JSTemplateComponent
 * @typedef {object} options
 * @property {function ():JSTemplateComponent.contextWithoutInstance} [context] context object default is Node
 * @property {JSTemplateComponent.contextLifeCycle} [__lifeCycle] context object default is Node
 * @property {string} [templateCode] template Code
 * @property {string} [templateUrl] template URL
 * @property {Array<string>} [cssStyles] list of URLs that point to styles
 * @property {object} [sharedReferences] references that will be same for all created components
 * @property {object} [sharedPrototypeMethods] methods that will be on all components under method "methods"
 * @property {boolean} [__flag_RejectOnStylesError=false]
 */

/**
 * @memberof JSTemplateComponent
 * @callback lifeCycleCallbackGetReferences
 * @param {JSTemplateComponent.contextWithInstance} context
 * @param {object} sharedReferences
 * @param {object} sharedPrototypeMethods
 * @returns {object}
 */

/**
 * @memberof JSTemplateComponent
 * @callback lifeCycleCallback
 * @param {JSTemplateComponent.contextWithInstance} context
 * @param {Object<string,any>} references
 * @param {Object<string,any>} methods
 */

/**
 * @memberof JSTemplateComponent
 * @callback constructorCallback
 * @param {Error} err
 */

 function JSTemplateComponentConstructor(
	/**
	 * @private
	 * @type {string}
	 */
	tagName,
	/**
	 * @private
	 * @type {JSTemplateComponent.options}
	 */
	options,
	/**
	 * @private
	 * @type {JSTemplateComponent.constructorCallback}
	 */
	callback
) {
	//@ts-ignore
	Application.require([
		"uriLoad	:: uri-load",
		"customElements :: custom-elements",
		"JSTemplate	 :: js-template",
		"request"
	]).then(function (lib) {
			/**
			 * @private
			 * @var {object} __instance
			 * @property {string} templateCode
			 */
			var __instance  = {
				templateCode : ''
			};

			function __getTemplate() {
				if (options.templateCode) {
					return Promise.resolve(options.templateCode);
				} else if (options.templateUrl) {
					return new Promise(function (resolve, reject) {
						new lib.request().url(options.templateUrl)
							.response("text")
							.then(function (templateCode) {
								__instance.templateCode = templateCode;
								resolve();
							}).catch(reject);
					});
				} else {
					return Promise.reject(Error('[OPTIONS_ERROR]: should be defined `templateCode` or `templateUrl`'));
				}
			}

			function __loadCssStyles() {
				if (!options.cssStyles.length) {
					return Promise.resolve();
				}
				return new Promise(function (resolve, reject) {
					lib.uriLoad.link(
						options.cssStyles,
						function (err) {
							if (err) {
								if (options.__flag_RejectOnStylesError) {
									reject(err);
								} else {
									resolve();
								}
							} else {
								resolve();
							}
						}
					);
				});
			}

			__getTemplate().then(
				function () {
					__loadCssStyles().then(
						function () {
							var sharedPrototypeMethods = options.sharedPrototypeMethods || {};
							lib.customElements.registerMethods(
								tagName,
								Object.assign(
									sharedPrototypeMethods,
									{
										"__onInit"	:
										/**
										 * @private
										 * @this {HTMLElement}
										 */
										function () {
											/**
											 * @private
											 * @type {HTMLElement}
											 */
											var node = this;
											/**
											 * @private
											 * @type {JSTemplateComponent.contextWithInstance}
											 */
											var _contextDefault = {
													setState   : function (state, callback) {
														ComponentContext.state = Object.assign(
															ComponentContext.state || {},
															state || {}
														);
														
														ComponentContext.__requireRender = true;
														ComponentContext.__instance.redraw();

														if (callback) callback();
													},
													__requireRender: true,
													__instance : {
														node   : node,
														redraw : function () {
															if (!ComponentContext.__requireRender) return;
															//@ts-ignore
															if (node.attrdata.JSRenderer) {
																//@ts-ignore
																ComponentContext.__requireRender = false;
																
																node.attrdata.JSRenderer.content.redraw();
																// /**
																//  * @deprecated
																//  */
																// node.attrdata.JSRenderer.attr.redraw();
															}
														},
														redrawForce: function () {
															ComponentContext.__requireRender = true;
															ComponentContext.__instance.redraw();
														}
													},
													__lifeCycle: {
														init: function () {
															if (options.__lifeCycle && options.__lifeCycle.init) {
																options.__lifeCycle.init(
																	ComponentContext,
																	envReferences,
																	sharedPrototypeMethods
																);
															}
														},
														attrChange: function () {
															if (options.__lifeCycle && options.__lifeCycle.attrChange) {
																options.__lifeCycle.attrChange(
																	ComponentContext,
																	envReferences,
																	sharedPrototypeMethods
																);
															}
														},
														getReferences: function () {
															// dummy function
														},
														contentChange: function () {
															if (options.__lifeCycle && options.__lifeCycle.contentChange) {
																options.__lifeCycle.contentChange(
																	ComponentContext,
																	envReferences,
																	sharedPrototypeMethods
																);
															}
														},
														remove: function () {
															if (options.__lifeCycle && options.__lifeCycle.remove) {
																options.__lifeCycle.remove(
																	ComponentContext,
																	envReferences,
																	sharedPrototypeMethods
																);
															}
														}
													}
												};

											var sharedReferences = options.sharedReferences || {};

											/**
											 * @private
											 * @type {JSTemplateComponent.contextWithInstance}
											 */
											var ComponentContext = Object.assign(
												options.context ? (options.context(_contextDefault, sharedReferences, sharedPrototypeMethods) || {}) : {},
												_contextDefault
											);
	
											var envReferences = Object.assign({}, sharedReferences);

											if (options.__lifeCycle && options.__lifeCycle.getReferences) {
												Object.assign(
													envReferences,
													options.__lifeCycle.getReferences(
														ComponentContext,
														sharedReferences,
														sharedPrototypeMethods
													) || {}
												);
											}
						
											ComponentContext.__instance.references = envReferences;

											node.innerHTML	= __instance.templateCode;

											// @ts-ignore
											node.attrdata.JSRenderer	= {
												ComponentContext: ComponentContext,
												envReferences: envReferences,
												content : lib.JSTemplate.parseContent(node, function () {}, {
													context : ComponentContext,
													args	 : envReferences
												}),
												attr	: lib.JSTemplate.parseAttributes(node, function () {}, {
													context : ComponentContext,
													args	 : envReferences
												})
											};
											
											if (options.__lifeCycle && options.__lifeCycle.init) {
												options.__lifeCycle.init.apply(
													ComponentContext,
													[
														ComponentContext,
														envReferences,
														sharedPrototypeMethods
													]
												);
											}

											ComponentContext.__instance.redraw();
										},
										"__onContentChange" :
										/**
										 * @private
										 * @this {HTMLElement}
										 */
										function () {
											if (this.attrdata.JSRenderer && this.attrdata.JSRenderer.ComponentContext) {

												if (
													this.attrdata.JSRenderer.ComponentContext.__requireRender
												// 	!this.attrdata.JSRenderer.ComponentContext.__instance.__renderStarted
												) {
													if (options.__lifeCycle && options.__lifeCycle.contentChange) {
														options.__lifeCycle.contentChange.apply(
															this.attrdata.JSRenderer.ComponentContext,
															[
																this.attrdata.JSRenderer.ComponentContext,
																this.attrdata.JSRenderer.envReferences,
																this.methods
															]
														);
													}

													this.attrdata.JSRenderer.ComponentContext.__instance.redraw();
												}
											}
										},
										"__onAttrChange" :
										/**
										 * @private
										 * @this {HTMLElement}
										 */
										function () {
											if (this.attrdata.JSRenderer && this.attrdata.JSRenderer.ComponentContext) {

												if (
													this.attrdata.JSRenderer.ComponentContext.__requireRender
												// 	!this.attrdata.JSRenderer.ComponentContext.__instance.__renderStarted
												) {

													if (options.__lifeCycle && options.__lifeCycle.attrChange) {
														options.__lifeCycle.attrChange.apply(
															this.attrdata.JSRenderer.ComponentContext,
															[
																this.attrdata.JSRenderer.ComponentContext,
																this.attrdata.JSRenderer.envReferences,
																this.methods
															]
														);
													}
													this.attrdata.JSRenderer.ComponentContext.__instance.redraw();
												}
											}
										},
										"__onRemove" :
										/**
										 * @private
										 * @this {HTMLElement}
										 */
										function () {
											if (this.attrdata.JSRenderer) {
												if (options.__lifeCycle && options.__lifeCycle.remove) {
													options.__lifeCycle.remove.apply(
														this.attrdata.JSRenderer.ComponentContext,
														[
															this.attrdata.JSRenderer.ComponentContext,
															this.attrdata.JSRenderer.envReferences,
															this.methods
														]
													);
												}
											}
										}
									}
								)
							);
						}
					).catch(function (err) {
						callback(err);
					});
				}
			).catch(function (err) {
				callback(err);
			});
	}).catch(function (err) {
		callback(err);
	});

	return this;
}

module.exports = JSTemplateComponentConstructor;