UNPKG

blinx

Version:

The Scalable JavaScript Application Framework

390 lines (331 loc) 9.35 kB
import {isEqual} from 'lodash/fp'; import Utils from '../helpers/utils'; import {moduleS, middleWareFns, eventQ} from './store'; import PubSub from './pubsub'; import {createInstance} from '../blinx'; /** * @module Module */ const Module = (function () { const modulePrivateData = new WeakMap(); /** * @class * All the modules created by this framework will be extended by this Module. * @extends {@link PubSub} */ class Module extends PubSub { static isObject(x) { return x != null && typeof x === 'object'; } static getDependencies(fnStr) { // Return if ._ is used directly if (fnStr.match(/\._[^.]/g)) { return '*'; } let matched = fnStr.match(/\._\.((\S)*[a-zA-Z0-9_$])/g), matchAll = '*'; // If any dependency is not found, trigger all the time. if (!matched || matched.length === 0) { return matchAll; } matched = matched.map((match) => { const splitted = match.split('._.'); return splitted[1] || splitted[0]; }); // If any evaluated dependency is present at root level const evaluatedDependencyAtRoot = matched.find(match => match.startsWith('._[')); if (evaluatedDependencyAtRoot) { return matchAll; } matched = matched.map(match => match.split('[')[0]); return matched; } // *** // Observer $proxyHandler() { const ctx = this; const _callObservingMethods = function () { setTimeout(() => { ctx.$_observerFns.forEach((fnObj) => { if (Array.isArray(fnObj.deps)) { // Dont trigger if adjacent node/sibling node has changed const pathArray = path.split('='); const depsMatched = fnObj.deps.find((deps) => { const depsArr = deps.split('.'); if (isEqual(depsArr, pathArray)) return true; if (pathArray.length < depsArr.length) { const pathLastIndex = pathArray.length - 1; if (isEqual(pathArray[pathLastIndex], depsArr[pathLastIndex])) return true; } if (pathArray.length <= depsArr.length) { return pathArray.find((keyItem, index) => depsArr[index] !== keyItem); } }); depsMatched ? fnObj.fn.call(ctx) : undefined; } else { fnObj.fn.call(ctx); } }); }); }; return { get(target, prop, receiver) { try { return (Module.isObject(target[prop]) && '__value' in target[prop]) ? target[prop].__value : target[prop]; } catch (err) { return undefined; } }, set(target, name, value) { if (Array.isArray(target) && name === 'length') { target.length = value; return target; } let path; // Dont set meta data for meta fields if (name === '__path' || name === '__value') { target[name] = value; return target; } path = target.__path ? `${target.__path}=${name}` : name; // Set values if (Module.isObject(value)) { target[name] = new Proxy(value, ctx.$proxyHandler()); } else { target[name] = { __value: value, }; } Object.defineProperty(target[name], '__path', { enumerable: false, value: path, }); // Call _callObservingMethods(); return target; }, deleteProperty(target, property) { let x; if (Module.isObject(target) || Array.isArray(target)) { x = delete target[property]; } _callObservingMethods(); return x; }, has(target, prop) { try { return ((Module.isObject(target) || Array.isArray(target)) && target[prop]); } catch (err) { return false; } }, }; } /** * @constructor * @param name * @param moduleName {string} the name of the module * @param lifeCycleFlags {lifeCycleFlags} the initial value of the lifecycle flags * @param instanceConfig the configuration of the module passed * @param instanceData It is the reference of module * @param meta */ constructor(name, moduleName, lifeCycleFlags, instanceConfig, instanceData, meta) { super(); this.moduleName = moduleName; this.name = name; this.lifeCycleFlags = Object.assign({}, lifeCycleFlags); this.instanceConfig = instanceConfig; this.modulePlaceholders = this.instanceConfig.placeholders; this.createChildInstance = createInstance.bind(this); this.meta = meta; for (const key in instanceData) { this[key] = instanceData[key]; } modulePrivateData.set(this, { moduleSubscriptions: [], uniqueId: meta.id, }); // Apply middleware, PRE:_Create middleWareFns.forEach((middlewareFn) => { Object.assign(this, middlewareFn(this)); }); // Observable proxy setup this.$_observerFns = []; this.observe_For && this.observe_For.forEach((fnName) => { if (!this[fnName] || typeof this[fnName] !== 'function') { console.error('{fnName} is not available over module. Can be observed.'); return; } const fnObj = { fn: this[fnName], deps: Module.getDependencies(String(this[fnName])), }; this.$_observerFns.push(fnObj); }); this._ = new Proxy({}, this.$proxyHandler()); } /** * @method * renders the template using placeholder * @param placeholderData : The placeholder data for creation of template */ render(placeholderData) { const containerSelector = this.getUniqueId(); const placeholders = placeholderData || this.instanceConfig.placeholders; if (!this.template) return; document.querySelector(`#${containerSelector}`).innerHTML = this.template(placeholders); } /** * @method * gets all the events of all types subscribed by the module * @returns {array} array of subscriptions */ getAllSubscriptions() { return modulePrivateData.get(this).moduleSubscriptions; } /** * @method * gets the unique id of the module * @returns {string} */ getUniqueId() { return modulePrivateData.get(this).uniqueId; } /** * @method * gets the unique id of the parent element * @returns {string} */ getParentInstanceId() { if (this.meta.parent) { return this.meta.parent.id; } return ''; } /** * @method * * @returns {string} */ getModuleContainer() { return `#${this.getUniqueId()}`; } /** * * @method * @returns {string|*} */ getModuleName() { return this.moduleName; } /** * * @method * @returns {*} */ getInstanceConfig() { return this.instanceConfig.placeholders; } /** * * @method * @returns {*} */ getCSSSelector() { return Utils.getCSSSelector(this, moduleS); } /** * * @method */ destroy() { } /** * * @method * @param subscription * @param eventName */ subscribe(subscription, eventName = subscription.eventName) { subscription.eventSubscriber = this.getModuleContainer(); modulePrivateData.get(this).moduleSubscriptions.push(subscription); super.subscribe(subscription, eventName); } /** * * @method * @param eventName * @param message */ publish(eventName, message) { super.publish(eventName, message); } /** * * @method */ dequeueEvents() { const moduleSubscriptions = this.getAllSubscriptions(); eventQ.store.forEach((evt) => { const queuedEvent = moduleSubscriptions.filter((event) => { if (evt.eventName === event.eventName && event.type === 'RE_PLAY') { return event; } }); queuedEvent.forEach((event) => { event.callback && event.callback.call((event.context ? event.context : null), evt.message); }); }); } /** * * @method * @param eventName * @param callback */ unsubscribe(eventName, callback) { if (typeof eventName === 'object') { callback = eventName.callback; eventName = eventName.eventName; } super.unsubscribe(this.getModuleContainer(), eventName, callback); } /** * @method * actual rendering happens here. Puts the wrapper for the module and adds it to the container. * @param module {Object} * @param compiledHTML * @returns {*} */ static createModuleArena(module, compiledHTML) { // If compiledHTML is not provided, start creating dom element progressively. let themeClass = ''; if (module.instanceConfig.moduleClassName) { themeClass = module.instanceConfig.theme ? `${module.instanceConfig.moduleClassName}-${module.instanceConfig.theme}` : `${module.instanceConfig.moduleClassName}-default`; } else { themeClass = module.instanceConfig.theme ? `${module.moduleName}-${module.instanceConfig.theme}` : `${module.moduleName}-default`; } if (typeof compiledHTML !== 'string') { document.querySelector(module.instanceConfig.container).innerHTML = `<div id="${module.getUniqueId()}" class="${themeClass} play-arena"></div>`; return; } // If compiledHTML is provided, create page string. if (compiledHTML.trim() === '') { compiledHTML = `<div id="${module.getUniqueId()}"></div>`; } else { } return compiledHTML; } /** * For internal use * This method is currently used to check is the event occured via Pub sub or a module * @returns {string} */ getInstanceName() { return 'MODULE'; } } return Module; }()); export default Module;