import Observable from './observ';
export default class RootModule extends Observable {
/**
* Класс является родителем для главного модуля (Root Module). </br>
* Наследуется от класса Observable для реализации паттерна [Наблюдатель (Observer)]{@link https://refactoring.guru/ru/design-patterns/observer}. </br>
* Объект Root модуля реализует паттерн [Посредник (Mediator)]{@link https://refactoring.guru/ru/design-patterns/mediator}. </br>
* @Module RootModule
*
* @example
* import Onedeck from 'onedeck';
*
* export default class Root extends Onedeck.RootModule { ... }
*
* @param {Object} config - конфиг приложения (пример конфига в README.md)
*/
constructor(config) {
super();
// this object contains config object
// конфиг, доступен в каждом модуле
// Присваиваем функцию import и удаляем ее из конфига
this._import = config.import;
config.import = null;
this.$$config = config;
// this object contains current module
// объект с текущим модулем
this.$$currentModule = {};
// this object contains current layout
// объект с текущим макетом
this.$$currentLayout = {};
// this object contains all modules
// объек содержит все модули приложения которые были инициализированы
this._modules = {};
// this object contains all layouts
// объек содержит все макеты приложения которые были инициализированы
this._layouts = {};
// состояние по указоннму url (если не используем history api)
this._urlState = {};
// инициализируем глобальные модули
this._initGlobalModules().then(() => {
// вызываем глобаьное событие popstate
this._eventHandler();
// получаем массив с данными из url
const module = this._getModuleFromUrl(
this.$$config.historyApi ? document.location.pathname : document.location.hash,
);
// вызывам метод init для модуля root
this.init(module);
// current module initialization
this._initModule({
module,
path: document.location.pathname,
});
}).catch((e) => {
console.error('Error init global module', e);
});
}
/**
* Абстрактный метод. Инициализация приложения. </br>
* В этом методе должна быть описана инициализация приложения. </br>
* Метод вызывается 1 раз при инициализации всего приложения. </br>
*
* @example
* init (path) {
* console.log('init', this.constructor.name, path);
*
* // Вызываем обработчик событий
* this.eventHandler();
* }
*
*
* @param {Array} path - массив с элементами url адреса.
* @abstract
*/
init () { }
/**
* Абстрактный метод. Обработчик событий. </br>
* В этом методе должны быть описаны все события уровня приложения, которые будут доступны в каждом модуле. </br>
*
* @example
* eventHandler () {
* // Обработка ошибок http запросов
* axios.interceptors.response.use(undefined, (error) => {
* this.ajaxError(error.response.data);
* return Promise.reject(error);
* });
*
* // Событие открывает окно
* this.$$on('showGlobalWnd', () => {
* const wnd = new ExampleGlobalWnd();
* wnd.show();
* });
*
* // Событие показывает уведомление
* this.$$on('notify', (text) => {
* const notifyObj = new ExampleNotification();
* notifyObj.notify(text);
* });
* }
*
* @abstract
*/
eventHandler () { }
/**
* Абстрактный метод. Монитирование модуля. </br>
* Метод автоматически вызывается для каждого модуля при изменении url адреса. </br>
* В методе доступны объекты currentModule и currentLayout. </br>
* Вызывается в следующем порядке: </br>
* - mounted Root модуля </br>
* - mounted всех Global модулей в произвольном порядке </br>
* - mounted Layout модуля </br>
* - mounted Page модуля </br>
* - mounted Embed модулей в произвольном порядке </br>
* @example
* mounted (module, layout) {
* console.log('mounted', this.constructor.name, module, layout);
* }
*
* @param {Object} currentModule - текущий Page модуль.
* @param {Object} currentLayout - текущий Layout модуль.
* @abstract
*/
mounted () { }
/**
* Абстрактный метод. Диспетчер. </br>
* В этом методе должна быть описана логика модуля связанная с маршрутизацией. </br>
* Метод автоматически вызывается для каждого модуля при изменении url адреса. </br>
* Вызывается в следующем порядке: </br>
* - dispatcher Root модуля </br>
* - dispatcher всех Global модулей в произвольном порядке</br>
* - dispatcher Layout модуля </br>
* - dispatcher Page модуля </br>
* - dispatcher Embed модулей в произвольном порядке </br>
*
* @example
* dispatcher (path, state) {
* console.log('dispatcher', this.constructor.name, path, state);
* // Если путь my.site.com/moduleName/item/3
* if (path[1] === 'item') this.showItem(state, path[2]);
* }
*
* @param {string} path - массив с элементами url адреса.
* @param {Object} state - данные переданные с url.
* @abstract
*/
dispatcher () { }
/**
* В каждом модуле содержиться метод $$rout. </br>
* Метод необходим для реализации маршрутизации, так же может передавать данные.
* @example <caption>Создания события для роутинга</caption>
* this.$$on('onRout', (data) => this.$$rout({
* path: `/module_name/item/${data.id}`,
* state: data
* })
* @example <caption>Переход на другую страницу</caption>
* import Module from 'ModuleName/module.js'
* const module = new Module()
*
* module.$$rout({
* path: '/module_name/item/1',
* state: {
* id: 1,
* name: 'Example',
* ...
* },
* })
*
* @param {Object} routData - Объек содержит url и state.
* @param {string} routData.path - url, first element module name.
* @param {Object} routData.state - state passed from the module.
*/
$$rout (routData) {
let path = this.$$config.rootPath ? this.$$config.rootPath + routData.path : routData.path;
// Удалем двойные '//'
path = path.replace(/\/\//, '/');
if (this.$$config.historyApi) {
// Если используем history Api - вызываем инициализацию модуля
this._initModule({
module: this._getModuleFromUrl(path),
path,
state: routData.state,
pushState: true,
});
} else {
// Если не используем - то сохраняем состояние, и переходим по нужному пути
// Далее вызовится событие "hashchange" - в котором и произойдет вызов метода initModule
this._urlState[path] = routData.state;
document.location.hash = path;
}
}
/**
* Приватный метод вызывает методы mounted всех модулей (см описание метода mounted)
* @private
*/
_mounted () {
this.mounted(this.$$currentModule, this.$$currentLayout);
Object.keys(this._modules)
.filter((moduleName) => this._modules[moduleName].$$global)
.forEach((moduleName) => {
this._modules[moduleName]
.mounted(this.$$currentModule, this.$$currentLayout)
// Встроенные модули
Object.keys(this._modules[moduleName].$$embed)
.forEach((name) => this._modules[moduleName].$$embed[name]
.mounted(this.$$currentModule, this.$$currentLayout));
});
if (this.$$currentLayout.obj) {
this.$$currentLayout.obj.mounted(this.$$currentModule, this.$$currentLayout);
}
if (this.$$currentModule.obj) {
this.$$currentModule.obj.mounted(this.$$currentModule, this.$$currentLayout);
Object
.keys(this.$$currentModule.obj.$$embed)
.forEach((name) => this.$$currentModule.obj.$$embed[name]
.mounted(this.$$currentModule, this.$$currentLayout));
}
}
/**
* Приватный метод вызывает метд destroy Page модуля и Embed модуле (см описание метода destroy в классе Module)
* @private
*/
_destroyModule () {
// Если переход на новый макет то чистим модуль а потом макет
if (this.$$currentModule.obj) {
this.$$currentModule.obj.destroy();
Object
.keys(this.$$currentModule.obj.$$embed)
.forEach((name) => this.$$currentModule.obj.$$embed[name].destroy());
this.$$currentModule = {};
}
}
/**
* Приватный метод вызывает метд destroy Layout модуля (см описание метода destroy в классе Module)
* @private
*/
_destroyLayout () {
if (this.$$currentLayout.obj) {
this.$$currentLayout.obj.destroy();
this.$$currentLayout = {};
}
}
/**
* Приватный метод вызывает методы dispatcher всех модулей (см описание метода dispatcher)
* @param {Array} path - url array
* @param {Object} state - current state
* @private
*/
_dispatcherModule (path, state) {
this.dispatcher(path, state);
// Вызываем диспатчеры для глобальных модулей
Object.keys(this._modules)
.filter((moduleName) => this._modules[moduleName].$$global)
.forEach((moduleName) => {
this._modules[moduleName].dispatcher(path, state);
// Встроенные модули
Object.keys(this._modules[moduleName].$$embed)
.forEach((name) => this._modules[moduleName].$$embed[name]
.dispatcher(path, state));
});
// Если переход на новый макет то чистим модуль а потом макет
if (this.$$currentModule.obj) {
// Вызываем диспатчер для текущего модуля
this.$$currentModule.obj.dispatcher(path, state);
// Вызываем диспатчеры для всторенных модулей
Object.keys(this.$$currentModule.obj.$$embed)
.forEach((name) => this.$$currentModule.obj.$$embed[name].dispatcher(path, state));
}
}
/**
* Приватный метод парсить текущий урл
* получаем название модуля и данные модуля url адреса,
* @param {String} url - url
* @returns {Arrat} массив сторк (разбитый урл адрес через / )
* @private
*/
_getModuleFromUrl (url) {
// Удалем ненужный нам путь
if (this.$$config.rootPath) {
url = url.replace(this.$$config.rootPath, '');
}
// Удалем первый '/' и #
url = url.replace(/^[\/, #]/, '');
return url.split('/');
}
_initGlobalModules = async () => {
const globalNames = Object.keys(this.$$config.modules)
.filter((moduleName) => this.$$config.modules[moduleName].global);
for (let i = 0; i < globalNames.length; i++) {
// eslint-disable-next-line
await this._createModule(globalNames[i], this.$$config.modules[globalNames[i]])
}
}
/**
* Приватный метод. Cоздает объект Page модуля и Embed модуля
* получаем название модуля и данные модуля url адреса,
* @param {String} moduleName - название модуля (в конфиге параметр module)
* @param {Object} moduleConf - настройки модуля
* @private
*/
_createModule = async (moduleName, moduleConf) => {
// Если уже подгрузили module - выходим
if (this._modules[moduleName]) return;
let ModuleClass = await this._import(moduleConf.module);
if (!ModuleClass || !ModuleClass.default) {
throw new SyntaxError(`Error load module: ${moduleName}`);
}
ModuleClass = ModuleClass.default;
// создаем модуль
this._modules[moduleName] = new ModuleClass();
// глобальный модуль
this._modules[moduleName].$$global = moduleConf.global;
// добавляем метод rout для маршрутизации
this._modules[moduleName].$$rout = this.$$rout.bind(this);
// добавляем метод publish для публикации глобальных событий
this._modules[moduleName].$$gemit = this.$$emit.bind(this);
// конфиг
this._modules[moduleName].$$config = this.$$config;
// макет модуля
this._modules[moduleName].$$layoutName = moduleConf.layout;
// встраиваемые модули
this._modules[moduleName].$$embed = {};
if (moduleConf.embed) {
const embedNames = Object.keys(moduleConf.embed);
for (let i = 0; i < embedNames.length; i++) {
// eslint-disable-next-line
let EmbedClass = await this._import(moduleConf.embed[embedNames[i]].module);
if (!EmbedClass || !EmbedClass.default) {
throw new SyntaxError(`Error load module: ${embedNames[i]}`);
}
EmbedClass = EmbedClass.default;
this._modules[moduleName].$$embed[embedNames[i]] = new EmbedClass();
this._modules[moduleName].$$embed[embedNames[i]].$$rout = this.$$rout.bind(this);
this._modules[moduleName].$$embed[embedNames[i]].$$gemit = this.$$emit.bind(this);
this._modules[moduleName].$$embed[embedNames[i]].$$config = this.$$config;
}
}
// Если модуль глобальный - сразу его инициализируем
if (this._modules[moduleName].$$global) {
this._modules[moduleName].init(moduleName);
// Инициализируем встроенные модули
Object.keys(this._modules[moduleName].$$embed)
.forEach((name) => this._modules[moduleName].$$embed[name]
.init(moduleName));
}
}
/**
* Приватный метод. Cоздает объект Layout модуля
* получаем название модуля и данные модуля url адреса,
* @param {String} layoutName - название модуля (в конфиге параметр layout)
* @private
*/
_createLayout = async (layoutName) => {
// Если уже подгрузили layout - выходим
if (this._layouts[layoutName]) return;
let LayoutClass = await this._import(layoutName);
if (!LayoutClass || !LayoutClass.default) {
throw new SyntaxError(`Error load module: ${layoutName}`);
}
LayoutClass = LayoutClass.default;
// создаем макет
this._layouts[layoutName] = new LayoutClass();
// добавляем метод rout для маршрутизации
this._layouts[layoutName].$$rout = this.$$rout.bind(this);
// добавляем метод publish для публикации глобальных событий
this._layouts[layoutName].$$gemit = this.$$emit.bind(this);
// конфиг
this._layouts[layoutName].$$config = this.$$config;
}
/**
* Приватный метод. Содержит обработку событий popstate или hashchange. </br>
* Обработка события popstate или hashchange зависит от параметра в конфиге historyApi
* @private
*/
_eventHandler () {
if (this.$$config.historyApi) {
window.addEventListener('popstate', (event) => this._initModule({
module: this._getModuleFromUrl(document.location.pathname),
path: document.location.pathname,
state: event.state,
}));
} else {
window.addEventListener('hashchange', () => this._initModule({
module: this._getModuleFromUrl(document.location.hash),
path: document.location.hash,
state: this._urlState[document.location.hash.replace(/^#/, '')],
}));
}
}
/**
* Приватный метод инициализации модуля. </br>
* Инициализируем Page Layout Embed модули в зависимости от url адресв
* @param {Object} moduleData - initn module data.
* @param {Array} moduleData.module - массив url адреса. В нулевом элементе module[0] содержиться имя модлуя.
* @param {string} moduleData.path - url.
* @param {Object} moduleData.state - current state.
* @param {boolean} moduleData.pushState - flag indicates save to history api.
* @private
*/
_initModule = async (moduleData) => {
const moduleName = moduleData.module[0];
const mudules = this.$$config.modules;
if (!moduleName) {
this.$$rout({
path: this.$$config.mainModule,
});
return;
}
if (!mudules[moduleName]) {
this.$$rout({
path: this.$$config.module404,
});
console.error('no such module:', moduleName);
return;
}
// Если это глобальный или встраиваемый модуль - они не учавствует в роутинге
if (mudules[moduleName].global) {
this.$$rout({
path: this.$$config.module404,
});
console.error('global module:', moduleName);
return;
}
// Создаем макет если он есть
if (
mudules[moduleName].layout
&& mudules[moduleName].layout === this.$$currentLayout.name
) {
// Если переход внутри текущего макета
this.$$currentLayout.obj.dispatcher(moduleData.module, moduleData.state);
} else if (mudules[moduleName].layout) {
this._destroyModule();
this._destroyLayout();
// Если переход на новый макет то чистим модуль а потом макет
try {
await this._createLayout(mudules[moduleName].layout);
} catch (e) {
console.error(e);
}
// Cохраняем новый модуль в объекте currentModule
this.$$currentLayout = {
name: mudules[moduleName].layout,
obj: this._layouts[mudules[moduleName].layout],
};
// Инициализируем новый макет (вызываем метод init)
this.$$currentLayout.obj.init(moduleData.module, moduleData.state);
} else {
// Если у модуля нет макета - уничтожаем текущий макет
this._destroyModule();
this._destroyLayout();
}
// Если переход внутри текущего модуля - вызываем диспатчер модуля
if (
this.$$currentModule.name
&& this.$$currentModule.name === moduleName
) {
// Если переход внутри текущего модуля - вызываем диспатчер модуля (метода dispatcher)
this._dispatcherModule(moduleData.module, moduleData.state);
} else {
// Если переход на новый модуль - вызываем деструктор текущего модуля (метод destroy)
this._destroyModule();
// Если переход на новый макет то чистим модуль а потом макет
try {
await this._createModule(moduleName, mudules[moduleName]);
} catch (e) {
console.error(e);
}
// Cохраняем новый модуль в объекте currentModule
this.$$currentModule = {
name: moduleName,
obj: this._modules[moduleName],
};
// Инициализируем новый модуль (вызываем метод init)
this.$$currentModule.obj.init(moduleData.module, moduleData.state);
// Инициализируем встроенные модули
Object.keys(this.$$currentModule.obj.$$embed)
.forEach((name) => this.$$currentModule.obj.$$embed[name]
.init(moduleData.module, moduleData.state));
this._dispatcherModule(moduleData.module, moduleData.state);
}
// Если используем history api - сохраняем новое состояние в истоии браузера
if (moduleData.pushState && this.$$config.historyApi) {
window.history.pushState(
moduleData.state,
moduleName,
moduleData.path,
);
}
// Вызываем методы жизненого цикла
this._mounted();
}
}