scripts/libs/module.js
/* globals MutationObserver, HTMLElement */
'use strict'
import { loop } from './../utils/loop.js'
import { Router } from './router.js'
import { store } from '@crispcode/pushstore'
/**
* The attribute name used to determine if an HTMLElement is a component
* @type {String}
* @private
*/
const _attrComponent = 'data-modux-component'
/**
* The attribute name used to determine if an HTMLElement ( usually an anchor tag ) is a state change component. This will prevent server reload on anchor tags.
* @type {String}
* @private
*/
const _attrLink = 'data-modux-link'
/**
* A shorthand for the link element handler
* @param {Event} e
* @private
*/
const linkHandler = function ( e ) {
let url = this.getAttribute( 'href' )
if ( url ) {
Router.redirect( url )
e.preventDefault()
}
}
/**
* The main class, used to create a Modux module. You can think of this as the main application
*/
export class Module {
/**
* A method used to add components to the Module
* @param {String} name The name under which the Component will be known as
* @param {Component} dependency The Component to be added
* @return {Module} The instance of Module
*/
addDependency ( name, dependency ) {
this.__dependencies[ name ] = dependency
return this
}
/**
* A method used to remove components from the Module
* @param {String} name The name of the Component to be removed
* @return {Module} The instance of Module
*/
removeDependency ( name ) {
if ( this.__dependencies[ name ] ) {
delete this.__dependencies[ name ]
}
return this
}
/**
* Creates an instance of Module
* @param {String} [name] A unique name for the module, for easier management
*/
constructor ( name ) {
/**
* A unique name for the module, for easier management
* @type {String}
* @private
*/
this.__name = name
/**
* Contains all the components added to the module
* @type {Object}
* @private
*/
this.__dependencies = {}
/**
* Contains a new instance of @crispcode/pushstore which is passed on to all components. See https://www.npmjs.com/package/@crispcode/pushstore
* @type {Store}
* @public
*/
this.store = store.create()
}
/**
* Creates a component on an HTMLElement if it doesn't have one already
* @param {HTMLElement} element The HTMLElement to bind the Component to
* @param {Component} Component The Component to be bound
* @private
*/
__createComponent ( element, Component ) {
if ( !element.moduxComponent ) {
element.moduxComponent = new Component( element, this, this.store )
element.moduxComponent.execute()
}
}
/**
* Removes a component from an HTMLElement if it has one, and removes all subcomponents
* @param {HTMLElement} element The HTMLElement for which we want to remove the Component
* @private
*/
__removeComponent ( element ) {
if ( element.moduxComponent ) {
// Destroy all children to prevent memory leak
const _destroyChildren = ( parent ) => {
loop( parent.children, ( child ) => {
if ( child.children.length > 0 ) {
_destroyChildren( child )
}
if ( child.moduxComponent ) {
child.moduxComponent.__destroy()
}
} )
}
_destroyChildren( element )
element.moduxComponent.__destroy()
}
}
/**
* Creates a link component on an HTMLElement if it doesn't have one already
* @param {HTMLElement} element The HTMLElement to bind the link to
* @private
*/
__createComponentLink ( element ) {
if ( !element.moduxLink ) {
element.addEventListener( 'click', linkHandler )
element.moduxLink = true
}
}
/**
* Removes a link component from an HTMLElement if it has one
* @param {HTMLElement} element The HTMLElement for which we want to remove the link
* @private
*/
__removeComponentLink ( element ) {
if ( element.moduxLink ) {
element.removeEventListener( 'click', linkHandler )
delete element.moduxLink
}
}
/**
* Checks if the current element needs a Component instance
* @param {HTMLElement} node HTMLElement what we are checking
* @param {String} attr The attribute to watch out for
* @param {Function} handler The callback function which will be called after the checks are made
* @private
*/
__loopOnElements ( node, attr, handler ) {
if ( !( node instanceof HTMLElement ) ) {
return
}
let name = node.getAttribute( attr )
if ( name ) {
handler( node, name )
}
loop( node.querySelectorAll( '*[' + attr + ']' ), ( element ) => {
handler( element, element.getAttribute( attr ) )
} )
}
/**
* Renders an HTMLElement to a Component as if it was a part of this module
* @param {HTMLElement} node The HTMLElement to be converted to a Component
*/
createComponent ( node ) {
this.__loopOnElements( node, _attrComponent, ( e, attr ) => {
if ( this.__dependencies[ attr ] ) {
this.__createComponent( e, this.__dependencies[ attr ] )
}
} )
this.__loopOnElements( node, _attrLink, ( e ) => {
this.__createComponentLink( e )
} )
}
/**
* Initializes the Module on a specific HTMLElement and loads the specified Component for it
* @param {HTMLElement} element The HTMLElement used as the wrapper for the Module
* @param {Component} component The Component to be used as the main Component
*/
bootstrap ( element, component ) {
/**
* Holds the MutationObserver which is used to check for changes in the DOM
*/
this.__htmlWatcher = new MutationObserver( ( mutations ) => {
mutations.forEach( ( mutation ) => {
if ( mutation.type === 'attributes' ) {
if ( mutation.attributeName === _attrComponent ) {
this.__removeComponent( mutation.target )
this.__createComponent( mutation.target, this.__dependencies[ mutation.target.getAttribute( _attrComponent ) ] )
}
if ( mutation.attributeName === _attrLink ) {
this.__removeComponentLink( mutation.target )
this.__createComponentLink( mutation.target )
}
}
if ( mutation.type === 'childList' ) {
if ( mutation.addedNodes.length > 0 ) {
// Nodes that were added
loop( mutation.addedNodes, ( node ) => {
this.__loopOnElements( node, _attrComponent, ( e, attr ) => {
if ( this.__dependencies[ attr ] ) {
this.__createComponent( e, this.__dependencies[ attr ] )
}
} )
this.__loopOnElements( node, _attrLink, ( e ) => {
this.__createComponentLink( e )
} )
} )
}
if ( mutation.removedNodes.length > 0 ) {
// Nodes that were removed
loop( mutation.removedNodes, ( node ) => {
this.__loopOnElements( node, _attrComponent, ( e ) => {
this.__removeComponent( e )
} )
this.__loopOnElements( node, _attrLink, ( e ) => {
this.__removeComponentLink( e )
} )
} )
}
}
} )
} )
this.__htmlWatcher.observe( element, { attributes: true, childList: true, characterData: false, subtree: true } )
if ( !this.__dependencies[ component ] ) {
throw new Error( 'Initial component cannot be found in dependency list' )
}
/**
* Holds the main Component which is used for the Module.
*/
this.__component = new this.__dependencies[ component ]( element, this, this.store )
element.moduxComponent = this.__component
element.moduxComponent.execute()
}
/**
* Destroy the current module. This will also destroy all the components created and disconnect the MutationObserver
*/
destroy () {
this.__component.__destroy()
this.__htmlWatcher.disconnect()
}
}