@veams/component
Version:
Component Class for Components in Veams for static page apps
426 lines (365 loc) • 10.1 kB
text/typescript
;
/**
* Represents a component constructor which supports
* options merging,
* binding and unbinding of events and subscriptions with template strings,
* rendering of templates
* and a destroy behaviour.
*
* @module @veams/component
* @author Sebastian Fitzner
*/
/**
* Imports
*/
import Base, { BaseConfig } from '@veams/base';
import { Collection } from './helpers/collection';
import getStringValue from './helpers/get-string-value';
import tplEngine from './helpers/template-engine';
import eventHandler from './helpers/event-handler';
export interface ComponentConfig extends BaseConfig {
context?: any; // @TODO: Check type
}
export interface Subscriber {
id?: string;
delegate?: any;
type: any;
event: any;
handler: any;
}
const DEFAULT_LIFECYCLES = {
create: true,
willMount: true,
didMount: true,
render: false
};
/**
* Custom Functions
*/
function buildEvtId(evtKeyArr, fnName) {
return evtKeyArr.join('_') + '_' + fnName;
}
/**
* Hidden variables
*/
// Custom event handler element which will be used in `events()` and `subscribe`
let eventElement = null;
abstract class Component extends Base {
context: any;
_events: {
[key: string]: string
};
_subscribe: {
[key: string]: string
};
__subscribers: Collection<Subscriber>;
private __eventElement: any; // TODO :: Create definition
/**
* Constructor
*
* to save standard elements like el and options and
* execute initialize as default method.
*
* @param {Object} obj [{}] - Object which contains el, options from the DOM and namespace.
* @param {Object} options [{}] - Object which contains options of the extended class.
*/
constructor(obj: ComponentConfig, options = {}) {
super(obj, options);
this.context = obj.context || window['Veams'];
this.__eventElement = eventHandler(this.el);
if (!this.context) {
console.info('@veams/component :: There is no context defined! When you want to use @veams/plugin-vent or any other singleton shared by your Veams instance provide the Veams object as context!');
}
this.initialize(obj, options);
}
// ----------------------------------------------------------
// GETTER & SETTERS
// ----------------------------------------------------------
/**
* Get and set events object
*/
set events(obj) {
this._events = obj;
}
get events() {
return this._events;
}
/**
* Get and set subscribe object
*/
set subscribe(obj) {
this._subscribe = obj;
}
get subscribe() {
return this._subscribe;
}
addSubscriber(obj: Subscriber) {
if (!this.__subscribers) {
this.__subscribers = {};
}
this.__subscribers[obj.id] = {
delegate: obj.delegate,
type: obj.type,
event: obj.event,
handler: obj.handler
};
}
get _subscribers(): Collection<Subscriber> {
return this.__subscribers;
}
// ----------------------------------------------------------
// STANDARD METHODS
// ----------------------------------------------------------
initialize(...args) {
}
/**
* Private method to create all necessary elements and bindings.
*
* @private
*/
create() {
this.preRender();
this.registerEvents(this.events, false);
this.registerEvents(this.subscribe, true);
this.bindEvents();
}
/**
* Bind local and global events
*
* @public
*/
bindEvents() {
}
/**
* Unbind events
*
* @public
*/
unbindEvents() {
}
/**
* Pre-Render templates
* which can be used to render content into it
*
* @public
*/
preRender() {
return this;
}
/**
* Render your module
*
* @public
*/
render() {
return this;
}
/**
* Destroy component by unbinding events and
* removing element from DOM
*/
destroy() {
this.unregisterEvents();
this.unbindEvents();
this.el.remove();
}
/**
* Render template with data
*
* @param {String} tplName - Template name which gets returned as rendered element.
* @param {Object} data - Data which gets handled by the template.
*/
renderTemplate(tplName: string, data: object) {
if (!this.context.templater) {
console.error(`
@veams/component :: It seems that you haven\'t added the @veams/plugin-templater. In order to work with 'renderTemplate()' you need to add it!
`);
} else {
return this.context.templater.render(tplName, data);
}
}
// ----------------------------------------------------------
// MOUNT PROCESS METHODS
// Mount process methods will be handled by the VeamsModules plugin
// ----------------------------------------------------------
/**
* This method will be executed after initialise
*/
public willMount(): void {
};
/**
* This method will be executed before unregistering events
*/
public willUnmount(): void {
};
/**
* This method will be executed after render
*/
public didMount(): void {
};
/**
* This method will be executed after unregistering events
*/
public didUnmount(): void {
};
// ----------------------------------------------------------
// EVENTS METHODS
// ----------------------------------------------------------
/**
* Register multiple events which are saved in an object.
*
* @param {Object} evts - Events object which contains an object with events as key and functions as value.
* @param {Boolean} global - Flag to switch between global and local events.
*
* @private
*/
registerEvents(evts: object, global = false) {
if (evts) {
Object.keys(evts).forEach((key) => {
this.registerEvent(key, evts[key], global);
});
}
}
/**
* Register an event by using a simple template engine and
* a key/value pair.
*
* @param {String} evtKey - Event key which contains event and additionally a delegated element.
* @param {String} fn - Function defined as string which will be bound to this.
* @param {Boolean} global - Flag if global or local event .
*
* @public
*
* @example
* this.registerEvent('click .btn', 'render');
* this.registerEvent('click {{this.options.btn}}', 'render');
* this.registerEvent('{{App.EVENTS.custom.event', 'render');
* this.registerEvent('{{App.EVENTS.resize', 'render', true);
*/
registerEvent(evtKey: string, fn: string, global = false) {
if (typeof evtKey !== 'string') {
console.error('@veams/component :: Your event is not a string!');
return;
}
if (typeof fn !== 'string') {
console.error('@veams/component :: Your event handler function is not a string!');
return;
}
let evtKeyArr = evtKey.split(' ');
let arrlen = evtKeyArr.length;
let evtType = getStringValue.apply(this, [tplEngine(evtKeyArr[0]), this.context]);
let bindFn = this[fn].bind(this);
let id = buildEvtId(evtKeyArr, fn);
if (arrlen > 2) {
throw new Error('@veams/component :: It seems like you have more than two strings in your events object!');
}
// Bind on this.el
if (arrlen === 1 && !global) {
this.__eventElement.on(evtType, bindFn);
this.addSubscriber({
type: 'event',
id: id,
event: evtType,
handler: bindFn
});
} else if (arrlen === 1 && global) {
if (!this.context && !this.context.Vent) {
console.warn('@veams/component :: There is no context or the Vent object is missing. Subscribing to global events will not work without it!');
return;
}
this.context.Vent.subscribe(evtType, bindFn);
this.addSubscriber({
type: 'globalEvent',
id: id,
event: evtType,
handler: bindFn
});
} else {
let delegate = getStringValue.apply(this, [tplEngine(evtKeyArr[1])]);
this.__eventElement.on(evtType, delegate, bindFn);
this.addSubscriber({
type: 'delegatedEvent',
delegate: delegate,
id: id,
event: evtType,
handler: bindFn
});
}
}
/**
* Delete all registered events.
*/
unregisterEvents() {
for (let key in this._subscribers) {
if (this._subscribers.hasOwnProperty(key)) {
let obj = this._subscribers[key];
if (obj.type === 'globalEvent') {
if (!this.context && !this.context.Vent) {
console.warn('@veams/component :: There is no context or the Vent object is missing. Subscribing to global events will not work without it!');
return;
}
this.context.Vent.unsubscribe(obj.event, obj.handler);
} else if (obj.type === 'delegatedEvent') {
this.__eventElement.off(obj.event, obj.delegate, obj.handler);
} else {
this.__eventElement.off(obj.event, obj.handler);
}
}
}
}
/**
* Unregister an event by using the saved subscribers and
* a key/value pair.
*
* @param {String} evtKey - Event key which contains event and additionally a delegated element.
* @param {String} fn - Function defined as string which will be unbound to this.
*
* @public
*
* @example
* this.unregisterEvent('click .btn', 'render');
* this.unregisterEvent('click {{this.options.btn}}', 'render');
* this.unregisterEvent('{{App.EVENTS.custom.event', 'render');
* this.unregisterEvent('{{App.EVENTS.resize', 'render');
*/
unregisterEvent(evtKey: string, fn: string) {
let evtKeyArr = evtKey.split(' ');
let id = buildEvtId(evtKeyArr, fn);
if (this._subscribers[id]) {
let obj = this._subscribers[id];
if (obj.type === 'globalEvent') {
if (!this.context && !this.context.Vent) {
console.warn('@veams/component :: There is no context or the Vent object is missing. Subscribing to global events will not work without it!');
return;
}
this.context.Vent.unsubscribe(obj.event, obj.handler);
} else if (obj.type === 'delegatedEvent') {
this.__eventElement.off(obj.event, obj.delegate, obj.handler);
} else {
this.__eventElement.off(obj.event, obj.handler);
}
}
}
}
function autocreate(lifecycles) {
const mergedLifeCycles = {...DEFAULT_LIFECYCLES, ...lifecycles};
return function creatInstance(instance: Component) {
if (mergedLifeCycles.create) {
instance.create();
}
if (mergedLifeCycles.willMount) {
instance.willMount();
}
if (mergedLifeCycles.didMount) {
instance.didMount();
}
if (mergedLifeCycles.render) {
instance.render();
}
return instance;
}
}
export default Component;
export {
autocreate
}