Skip to content
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
docs
src
.gitignore
LICENSE
README.md
package-lock.json
package.json
tsconfig.json
webpack.config.js

README.md

Modular Design Pattern Example

A JavaScript example written in TypeScript for the Modular Design Pattern.

Project goals:

  • Manage modules via the main runtime application class
  • Handle lazy loaded module classes
  • Showcase module communication between parents, children, and static modules
  • Showcase dynamically appending and destroying modules

Class Breakdown

Runtime Application Class

/** Global module instantiation object */
var modules = {};

class Application {
    
    /** Instance (static) variables */
    public static modules : Array<Module>;
    
    /** Class constructor */
    constructor();

    /** Class (static) functions */
    public static mountModules(): void;
    public static manageLazyParents(): void;
    public static createModule(view:HTMLElement, uuid:string): Module;
    public static destroyModule(uuid:string): void;
    public static getModuleByUUID(uuid:string): Module;
}

new Application();
Application.mountModules();

The runtime application class is instantiated as soon as the script loads. It then attempts to mount the modules. Since each module class is loaded individually it's possible that no modules will be mounted during this initial mounting attempt.

Example Module Class

export class Example extends Module{

    public static readonly index:string = 'Example';

    constructor(view:HTMLElement, uuid:string){
        super(view, uuid);
    }

    afterMount(){
        console.log(`${ Example.index } ${ this.uuid } has been mounted`);
    }

    beforeDestroy(){
        console.log(`${ Example.index } ${ this.uuid } has been destroyed`);
    }

}

modules[Example.index] = User;
Application.mountModules();

Whenever a module class has finished loading it registers itself with the global modules object using the classes public static read only index string as the objects key. It then tells the Application class to attempt to mount any unmounted modules again.

Base Module Class

class Module {
    /** Instance (public) variables */
    public readonly uuid : string;
    public readonly view : HTMLElement;
    public parent : any;
    public futureParent : any;
    public submodules : Array<Module>;

    /** Class constructor */
    constructor(view:HTMLElement, uuid:string);

    /** Class (public) functions */
    public mount(): void;
    public afterMount(): void;
    public seppuku(): void;
    public beforeDestroy(): void;
    public destroy(triggeredByParent:boolean): void;
    public register(submodule:Module): void;
}

All modules extend the base module class. This classes handles registering submodules along with tracking the existence of a parent module. The only public functions that should be used within a module are seppuku(), afterMount(), and beforeDestroy(). The remaining classes are important utility classes that should only be called by the runtime application.

Note: seppuku is the modules self-destruct method.

How It Works

TLDR:

  • Not all modules will be loaded at the same time
  • A module will attempt to locate a parent module and register itself with the parent as a submodule
  • Whenever a new module is loaded the application will attempt to mount all unmounted modules
  • Whenever a new module is loaded the application will attempt to connect submoudles with their future parents
  • Modules can have an infinite number of submodules
  • A modules hierarchy can contain an infinite number of branches and levels
  • Modules can only communicate with the controller of their parent, submodules, static modules, or an external server
  • When a module is destroyed it triggers all its submodules to be destroyed
  • When a module is destroyed it is removed from the DOM

Full Breakdown:

First the global modules object must be declared. This object should not exist within a script since we can't guarantee any specific script will be loaded before the others.

In the example below a single User module is requested. Modules are requested using the data-module attribute. The string value will be used as the key for the global modules object. The module attribute should be attached to the top level node within the document since that node will be passed to the module as its view variable.

<!doctype html>
<html class="no-js" lang="en">

<head>
    <meta charset="utf-8">
    <title>Modular Design Pattern - JavaScript Example</title>

    <script>
        var modules = {};
    </script>
</head>

<body>
    <article>
        <user data-module="User">
            <div>
                <span>Username: </span>
                <user-name>Player 1</user-name>
                <br/>
                <span>Life: </span>
                <user-life-total class="js-life-total">&nbsp;</user-life-total>
            </div>
            <div>
                <button class="js-subtract-button -round"><span>-</span></button>
                <button class="js-add-button -round"><span>+</span></button>
            </div>
        </user>
    </article>
    <script async src="assets/runtime.js"></script>
    <script async src="assets/globals.js"></script>
    <script async src="assets/npm.uuid.js"></script>
    <script async src="assets/User.js"></script>
    <script defer src="assets/Application.js"></script>
</body>
</html>

Once the runtime application has finished loading the class will be instantiated and the public static mountModules() method will be called.

/** Starts the runtime application */
new Application();

/** Mount the initial modules */
Application.mountModules();

The mountModules() method creates an array of any HTML elements that have a data-module attribute but only if they don't already have data-uuid attribute. The data-uuid attribute is set after the module has already been mounted.

const pendingModules:Array<HTMLElement> = Array.from(document.body.querySelectorAll('[data-module]:not([data-uuid])'));

If the page contains pending modules the runtime application gets the modules index from the data-module attribute before calling the createModule() method.

The createModule() method attempts to mount the module to the element using the modules index. If a new module has been created the modules mount() method is called. This method sets the data-uuid attribute and tries to locate a parent module.

try{
    /** Create the module */
    newModule = new modules[index].prototype.constructor(view, id);

    /** Attempt to mount the module */
    newModule.mount();
}catch(e){
    /** If the error wasn't caused by an undefined module report it */
    if(modules[index] !== undefined){
        console.error('Failed to create module', e);
    }
}

/** Check if the module was successfully created */
if(newModule){
    Application.modules.push(newModule);

    /** Trigger the modules after mount event */
    newModule.afterMount();
}

All modules that are successfully mounted are pushed into the runtime applications modules array before the modules afterMount() method is called. The afterMount() method should be used by the module class to call any initial methods or add any event listeners.

It is possible that a parent module is pending but the child's class was loaded and instantiated first. When this happens the child saves a reference to the parent. Whenever the runtime applications mountModules() method is called the manageLazyParents() method is also called. This method checks with all the modules to see if the future parent variable is set, if it is the application attempts to find the modules parent base on the parents data-uuid attribute. If the parent module is found the child is registered with the parent, otherwise the application assumes the parents class is still loading and does nothing.

The final stage of a modules life cycle is it's destruction. When a module is destroyed the runtime applications destroyModule() method is called. The module to destroy is obtained via its UUID variable. Before the module is destroyed its beforeDestroy() method is called. At that time all final functions should run and any event listeners should be removed.

/** Called by the runtime application when the module needs to be destroyed. */
public destroy():void{
    if(this.submodules.length){
        for(let i = this.submodules.length - 1; i >= 0; i--){
            Application.destroyModule(this.submodules[i].uuid);
        }
    }
    this.view.remove();
}

When the modules destroy() method is called the module checks if it has any submodules. If the module has submodules it tells the application to destroy them too. Once all the submodules have been removed the node that the module was mounted to is removed from the DOM and the module is spliced from the runtime applications modules array.

You can’t perform that action at this time.