StructureJS
0.15.2A class based utility library for building modular and scalable web platform applications. Features opt-in classes and utilities which provide a solid foundation and toolset to build your next project.
import DisplayObjectContainer from './DisplayObjectContainer';
import BaseEvent from '../event/BaseEvent';
import TemplateFactory from '../util/TemplateFactory';
import ComponentFactory from '../util/ComponentFactory';
import jQuery from '../plugin/jquery.eventListener';
/**
* The {{#crossLink "DOMElement"}}{{/crossLink}} class is the base view class for all objects that can be placed into the HTML DOM.
*
* @class DOMElement
* @param type [any=null] Either a jQuery object or JavaScript template string reference you want to use as the view. Check out the examples below.
* @param params [any=null] Any data you would like to pass into the jQuery element or template that is being created.
* @extends DisplayObjectContainer
* @module StructureJS
* @submodule view
* @requires Extend
* @requires DisplayObjectContainer
* @requires BaseEvent
* @requires TemplateFactory
* @requires ComponentFactory
* @requires jQuery
* @constructor
* @author Robert S. (www.codeBelt.com)
* @example
* // Example: Using DOMElement without extending it.
* let aLink = new DOMElement('a', {text: 'Google', href: 'http://www.google.com', 'class': 'externalLink'});
* this.addChild(aLink);
*
* // Example: A view passing in a jQuery object.
* let view = new CustomView($('.selector'));
* this.addChild(view);
*
* // Example: A view extending DOMElement while passing in a jQuery object.
* class ClassName extends DOMElement {
*
* constructor($element) {
* super($element);
* }
*
* create() {
* super.create();
*
* // Create and add your child objects to this parent class.
* }
*
* enable() {
* if (this.isEnabled === true) { return; }
*
* // Enable the child objects and add any event listeners.
*
* super.enable();
* }
*
* disable() {
* if (this.isEnabled === false) { return; }
*
* // Disable the child objects and remove any event listeners.
*
* super.disable();
* }
*
* layout() {
* // Layout or update the child objects in this parent class.
* }
*
* destroy() {
* this.disable();
*
* // Destroy the child objects and references in this parent class to prevent memory leaks.
*
* super.destroy();
* }
*
* }
*
* // Example: A view extending DOMElement with a precompiled JavaScript template reference passed in.
* class ClassName extends DOMElement {
*
* constructor() {
* _super();
* }
*
* create() {
* super.create('templates/home/homeTemplate', {data: 'some data'});
*
* // Create and add your child objects to this parent class.
* }
*
* enable() {
* if (this.isEnabled === true) { return; }
*
* // Enable the child objects and add any event listeners.
*
* super.enable();
* }
*
* disable() {
* if (this.isEnabled === false) { return; }
*
* // Disable the child objects and remove any event listeners.
*
* super.disable();
* }
*
* layout() {
* // Layout or update the child objects in this parent class.
* }
*
* destroy() {
* this.disable();
*
* // Destroy the child objects and references in this parent class to prepare for garbage collection.
*
* super.destroy();
* }
*
* }
*/
class DOMElement extends DisplayObjectContainer
{
/**
* Tracks number of times an element's width has been checked
* in order to determine if the element has been added
* to the DOM.
*
* @property checkCount
* @type {number}
* @public
*/
public checkCount:number = 0;
/**
* A cached reference to the DOM Element
*
* @property element
* @type {HTMLElement}
* @default null
* @public
*/
public element:HTMLElement = null;
/**
* A cached reference to the jQuery DOM element
*
* @property $element
* @type {JQuery}
* @default null
* @public
*/
public $element:JQuery = null;
/**
* If a jQuery object was passed into the constructor this will be set as true and
* this class will not try to add the view to the DOM since it already exists.
*
* @property _isReference
* @type {boolean}
* @protected
*/
protected _isReference:boolean = false;
/**
* Holds onto the value passed into the constructor.
*
* @property _type
* @type {string}
* @default null
* @protected
*/
protected _type:string = null;
/**
* Holds onto the value passed into the constructor.
*
* @property _params
* @type {any}
* @default null
* @protected
*/
protected _params:any = null;
constructor(type:any = null, params:any = null)
{
super();
if (type instanceof jQuery)
{
this.$element = type;
this.element = this.$element[0];
this._isReference = true;
}
else if (type)
{
this._type = type;
this._params = params;
}
}
/**
* The create function is intended to provide a consistent place for the creation and adding
* of children to the view. It will automatically be called the first time that the view is added
* to another DisplayObjectContainer. It is critical that all subclasses call the super for this function in
* their overridden methods.
*
* This method gets called once when the child view is added to another view. If the child view is removed
* and added to another view the create method will not be called again.
*
* @method create
* @param type [string=div] The HTML tag you want to create or the id/class selector of the template or the pre-compiled path to a template.
* @param params [any=null] Any data you would like to pass into the jQuery element or template that is being created.
* @returns {any} Returns an instance of itself.
* @public
* @chainable
* @example
* // EXAMPLE 1: By default your view class will be a div element:
* create() {
* super.create();
*
* this._childInstance = new DOMElement();
* this.addChild(this._childInstance);
* }
*
* // EXAMPLE 2: But lets say you wanted the view to be a ul element:
* create() {
* super.create('ul');
* }
*
* // Then you could nest other elements inside this base view/element.
* create() {
* super.create('ul', {id: 'myId', 'class': 'myClass anotherClass'});
*
* let li = new DOMElement('li', {text: 'Robert is cool'});
* this.addChild(li);
* }
*
* // EXAMPLE 3: So that's cool but what if you wanted a block of html to be your view. Let's say you had the below
* // inline Handlebar template in your html file.
* <script id="todoTemplate" type="text/template">
* <div id="htmlTemplate" class="js-todo">
* <div id="input-wrapper">
* <input type="text" class="list-input" placeholder="{{ data.text }}">
* <input type="button" class="list-item-submit" value="Add">
* </div>
* </div>
* </script>
*
* // You would just pass in the id or class selector of the template which in this case is "#todoTemplate".
* // There is a second optional argument where you can pass data for the Handlebar template to use.
* create() {
* super.create('#todoTemplate', { data: this.viewData });
*
* }
*
* // EXAMPLE 4: Or maybe you're using grunt-contrib-handlebars, or similar, to precompile hbs templates
* create() {
* super.create('templates/HomeTemplate', {data: "some data"});
*
* }
*/
public create(type:string = 'div', params:any = null):any
{
// Use the data passed into the constructor first else use the arguments from create.
type = this._type || type;
params = this._params || params;
if (this.isCreated === true)
{
throw new Error('[' + this.getQualifiedClassName() + '] You cannot call the create method manually. It is only called once automatically during the view lifecycle and should only be called once.');
}
if (this.$element == null)
{
const html:string = TemplateFactory.create(type, params);
if (html)
{
this.$element = jQuery(html);
}
else
{
this.$element = jQuery("<" + type + "/>", params);
}
}
this.element = this.$element[0];
this.width = this.$element.width();
this.height = this.$element.height();
this.setSize(this.width, this.height);
return this;
}
/**
* @overridden DisplayObjectContainer.addChild
* @method addChild
* @param child {DOMElement} The DOMElement instance to add as a child of this object instance.
* @returns {any} Returns an instance of itself.
* @chainable
* @example
* this.addChild(domElementInstance);
*/
public addChild(child:DOMElement):any
{
if (this.$element == null)
{
throw new Error('[' + this.getQualifiedClassName() + '] You cannot use the addChild method if the parent object is not added to the DOM.');
}
super.addChild(child);
// If an empty jQuery object is passed into the constructor then don't run the code below.
if (child._isReference === true && child.$element.length === 0)
{
return this;
}
if (child.isCreated === false)
{
child.create();// Render the item before adding to the DOM
child.isCreated = true;
}
// If the child object is not a reference of a jQuery object in the DOM then append it.
if (child._isReference === false)
{
this.$element.append(child.$element);
}
this._onAddedToDom(child);
return this;
}
/**
* Adds the sjsId to the DOM element so we can know what what Class object the HTMLElement belongs too.
*
* @method _addClientSideId
* @param child {DOMElement} The DOMElement instance to add the sjsId too.
* @protected
*/
protected _addClientSideId(child:DOMElement):void
{
let type:any = child.$element.attr('data-sjs-type');
let id:any = child.$element.attr('data-sjs-id');
if (type === void 0) {
// Make them array's so the join method will work.
type = [child.getQualifiedClassName()];
id = [child.sjsId];
} else {
// Split them so we can push/add the new values.
type = type.split(',');
id = id.split(',');
type.push(child.getQualifiedClassName());
id.push(child.sjsId);
}
// Updated list of id's and types
child.$element.attr('data-sjs-id', id.join(','));
child.$element.attr('data-sjs-type', type.join(','));
}
/**
* Removes the sjsId and class type from the HTMLElement.
*
* @method _removeClientSideId
* @param child {DOMElement} The DOMElement instance to add the sjsId too.
* @protected
* @return {boolean}
*/
protected _removeClientSideId(child):boolean
{
const type:string = child.$element.attr('data-sjs-type');
const id:string = child.$element.attr('data-sjs-id');
// Split them so we can remove the child sjsId and type.
const typeList:Array<string> = type.split(',');
const idList:Array<number> = id.split(',').map(Number);// Convert each item into a number.
const index:number = idList.indexOf(child.sjsId);
if (index > -1) {
// Remove the id and type from the array.
typeList.splice(index, 1);
idList.splice(index, 1);
// Updated list of id's and types
child.$element.attr('data-sjs-type', typeList.join(','));
child.$element.attr('data-sjs-id', idList.join(','));
}
return idList.length === 0;
}
/**
* Called when the child object is added to the DOM.
* The method will call {{#crossLink "DOMElement/layout:method"}}{{/crossLink}} and dispatch the BaseEvent.ADDED_TO_STAGE event.
*
* @method _onAddedToDom
* @protected
*/
protected _onAddedToDom(child:DOMElement)
{
child.checkCount++;
if (child.$element.width() === 0 && child.checkCount < 5)
{
setTimeout(() =>
{
this._onAddedToDom(child);
}, 100);
return;
}
this._addClientSideId(child);
child.width = child.$element.width();
child.height = child.$element.height();
child.setSize(child.width, child.height);
child.enable();
child.layout();
child.dispatchEvent(new BaseEvent(BaseEvent.ADDED_TO_STAGE));
}
/**
* @overridden DisplayObjectContainer.addChildAt
*/
public addChildAt(child:DOMElement, index:number):any
{
const children = this.$element.children();
const length = children.length;
// If an empty jQuery object is passed into the constructor then don't run the code below.
if (child._isReference === true && child.$element.length === 0)
{
return this;
}
if (index < 0 || index >= length)
{
// If the index passed in is less than 0 and greater than the total number of children then place the item at the end.
this.addChild(child);
}
else
{
// Else get the child in the children array by the index passed in and place the item before that child.
if (child.isCreated === false)
{
child.create();// Render the item before adding to the DOM
child.isCreated = true;
}
// Adds the child at a specific index but also will remove the child from another parent object if one exists.
if (child.parent) {
child.parent.removeChild(child, false);
}
this.children.splice(index, 0, child);
this.numChildren = this.children.length;
child.parent = this;
// Adds the child before any child already added in the DOM.
jQuery(children.get(index)).before(child.$element);
this._onAddedToDom(child);
}
return this;
}
/**
* @overridden DisplayObjectContainer.swapChildren
*/
public swapChildren(child1:DOMElement, child2:DOMElement):any
{
const child1Index = child1.$element.index();
const child2Index = child2.$element.index();
this.addChildAt(child1, child2Index);
this.addChildAt(child2, child1Index);
return this;
}
/**
* @overridden DisplayObjectContainer.getChildAt
*/
public getChildAt(index:number):DOMElement
{
return <DOMElement>super.getChildAt(index);
}
/**
* Returns a DOMElement object with the first found DOM element by the passed in selector.
*
* @method getChild
* @param selector {string} DOM id name, DOM class name or a DOM tag name.
* @returns {DOMElement}
* @public
*/
public getChild(selector:string):DOMElement
{
// Get the first match from the selector passed in.
const jQueryElement:JQuery = this.$element.find(selector).first();
if (jQueryElement.length === 0)
{
throw new TypeError('[' + this.getQualifiedClassName() + '] getChild(' + selector + ') Cannot find DOM $element');
}
// Check to see if the element has a sjsId value and is a child of this parent object.
const sjsId:number = parseInt(jQueryElement.attr('data-sjs-id'));
let domElement:DOMElement = <DOMElement>this.getChildByCid(sjsId);
// Creates a DOMElement from the jQueryElement.
if (domElement == null)
{
// Create a new DOMElement and assign the jQuery element to it.
domElement = new DOMElement();
domElement.$element = jQueryElement;
this._addClientSideId(domElement);
domElement.element = jQueryElement[0];
domElement.isCreated = true;
// Added to the super addChild method because we don't need to append the element to the DOM.
// At this point it already exists and we are just getting a reference to the DOM element.
super.addChild(domElement);
}
return domElement;
}
/**
* Gets all the HTML elements children of this object.
*
* @method getChildren
* @param [selector] {string} You can pass in any type of jQuery selector. If there is no selector passed in it will get all the children of this parent element.
* @returns {Array.<DOMElement>} Returns a list of DOMElement's. It will grab all children HTML DOM elements of this object and will create a DOMElement for each DOM child.
* If the 'data-sjs-id' property exists is on an HTML element a DOMElement will not be created for that element because it will be assumed it already exists as a DOMElement.
* @public
*/
public getChildren(selector:string = ''):Array<DOMElement>
{
//TODO: Make sure the index of the children added is the same as the what is in the actual DOM.
let $child:JQuery;
let domElement:DOMElement;
const $list:JQuery = this.$element.children(selector);
const listLength:number = $list.length;
for (let i:number = 0; i < listLength; i++)
{
$child = $list.eq(i);
// If the jQuery element already has sjsId data property then it must be an existing DisplayObjectContainer (DOMElement) in the children array.
if ($child.attr('data-sjs-id') === void 0)
{
domElement = new DOMElement();
domElement.$element = $child;
this._addClientSideId(domElement);
domElement.element = $child.get(0);
domElement.isCreated = true;
// Added to the super addChild method because we don't need to append the element to the DOM.
// At this point it already exists and we are just getting a reference to the DOM element.
super.addChild(domElement);
}
}
return <Array<DOMElement>>this.children;
}
/**
* Removes the specified child object instance from the child list of the parent object instance.
* The parent property of the removed child is set to null and the object is garbage collected if there are no other references
* to the child. The index positions of any objects above the child in the parent object are decreased by 1.
*
* @method removeChild
* @param child {DOMElement} The DisplayObjectContainer instance to remove.
* @returns {any} Returns an instance of itself.
* @override
* @public
* @chainable
*/
public removeChild(child:DOMElement, destroy:boolean = true):any
{
const remove:boolean = this._removeClientSideId(child);
child.disable();
// Checks if destroy was called before removeChild so it doesn't error.
if (remove === true && child.$element != null) {
child.$element.unbind();
child.$element.remove();
}
if (destroy === true) {
child.destroy();
}
super.removeChild(child);
return this;
}
/**
* Removes the child display object instance that exists at the specified index.
*
* @method removeChildAt
* @param index {int} The index position of the child object.
* @public
* @chainable
*/
public removeChildAt(index:number, destroy:boolean = true):any
{
this.removeChild(this.getChildAt(index), destroy);
return this;
}
/**
* Removes all child object instances from the child list of the parent object instance.
* The parent property of the removed children is set to null and the objects are garbage collected if no other
* references to the children exist.
*
* @method removeChildren
* @returns {DOMElement} Returns an instance of itself.
* @override
* @public
* @chainable
*/
public removeChildren(destroy:boolean = true):any
{
while (this.children.length > 0)
{
this.removeChild(<DOMElement>this.children.pop(), destroy);
}
this.$element.empty();
return this;
}
/**
* @overridden DisplayObjectContainer.destroy
*/
public destroy():void
{
// Note: we can't just call destroy to remove the HTMLElement because there could be other views managing the same HTMLElement.
/*if (this.$element != null) {
this.$element.unbind();
this.$element.remove();
}*/
super.destroy();
}
/**
* A way to instantiate view classes by found html selectors.
*
* Example: It will find all children elements of the {{#crossLink "DOMElement/$element:property"}}{{/crossLink}} property with the 'js-shareEmail' selector.
* If any selectors are found the EmailShareComponent class will be instantiated and pass the found jQuery element into the contructor.
*
* @method createComponents
* @param componentList (Array.<{ selector: string; component: DOMElement }>
* @return {Array.<DOMElement>} Returns all the items created from this createComponents method.
* @public
* @chainable
* @example
* create() {
* super.create();
*
* this.createComponents([
* {selector: '.js-shareEmail', component: EmailShareComponent},
* {selector: '.js-pagination', component: PaginationComponent},
* {selector: '.js-carousel', component: CarouselComponent}
* ]);
* }
*/
public createComponents(componentList:Array<any>):Array<DOMElement>
{
let list:Array<DOMElement>;
let createdChildren:Array<DOMElement> = [];
const length:number = componentList.length;
let obj:any;
for (let i = 0; i < length; i++)
{
obj = componentList[i];
list = <Array<DOMElement>>ComponentFactory.create(this.$element.find(obj.selector), obj.component, this);
createdChildren = createdChildren.concat(list);
}
return createdChildren;
}
/**
* Only use this once per application and use used on your main application Class.
* This selects HTML element that you want the application to have control over.
* This method starts the lifecycle of the application.
*
* @method appendTo
* @param type {any} A string value where your application will be appended. This can be an element id (#some-id), element class (.some-class) or a element tag (body).
* @param [enabled=true] {boolean} Sets the enabled state of the object.
* @example
* <b>Instantiation Example</b><br>
* This example illustrates how to instantiate your main application or root class.
*
* const app = new MainClass();
* app.appendTo('body');
*
*/
public appendTo(type:any, enabled:boolean = true):any
{
this.$element = (type instanceof jQuery) ? type : jQuery(type);
this._addClientSideId(this);
if (this.isCreated === false)
{
this.create();
this.isCreated = true;
if (enabled === false)
{
this.disable();
}
else
{
this.enable();
}
this.layout();
}
return this;
}
}
export default DOMElement;