core/Controller.js

/*
 *  Controller.js
 *  PureMVC JavaScript Multicore
 *
 *  Copyright(c) 2023 Saad Shams <saad.shams@puremvc.org>
 *  Your reuse is governed by the BSD License
*/

import {View} from "./View.js"
import {Observer} from "../patterns/observer/Observer.js";

/**
 * A Multiton `Controller` implementation.
 *
 * <P>In PureMVC, the `Controller` class follows the
 * 'Command and Controller' strategy, and assumes these
 * responsibilities:</P>
 *
 * <UL>
 * <LI> Remembering which `Command`s
 * are intended to handle which `Notifications`.</LI>
 * <LI> Registering itself as an `Observer` with
 * the `View` for each `Notification`
 * that it has a `Command` mapping for.</LI>
 * <LI> Creating a new instance of the proper `Command`
 * to handle a given `Notification` when notified by the `View`.</LI>
 * <LI> Calling the `Command`'s `execute`
 * method, passing in the `Notification`.</LI>
 * </UL>
 *
 * <P>Your application must register `Commands` with the
 * Controller.</P>
 *
 * <P>The simplest way is to subclass `Facade`,
 * and use its `initializeController` method to add your
 * registrations.</P>
 *
 * @see View View
 * @see Observer Observer
 * @see Notification Notification
 * @see SimpleCommand SimpleCommand
 * @see MacroCommand MacroCommand
 *
 * @class Controller
 */
class Controller {

    /**
     * Constructor.
     *
     * <P>This `Controller` implementation is a Multiton,
     * so you should not call the constructor
     * directly, but instead call the static Factory method,
     * passing the unique key for this instance
     * `Controller.getInstance( multitonKey )`</P>
     *
     * @throws {Error} Error if instance for this Multiton key has already been constructed
     *
     * @constructor
     * @param {string} key
     */
    constructor(key) {
        if (Controller.instanceMap[key] != null) throw new Error(Controller.MULTITON_MSG);
        /** @protected
         * @type {string} */
        this.multitonKey = key;
        Controller.instanceMap.set(this.multitonKey, this);
        /** @protected
         * @type {Map<string, function():SimpleCommand>} */
        this.commandMap = new Map();
        this.initializeController();
    }

    /**
     * Initialize the Multiton `Controller` instance.
     *
     * <P>Called automatically by the constructor.</P>
     *
     * <P>Note that if you are using a subclass of `View`
     * in your application, you should <i>also</i> subclass `Controller`
     * and override the `initializeController` method in the
     * following way:</P>
     *
     * <pre>`
     *		// ensure that the Controller is talking to my View implementation
     *		initializeController( )
     *		{
     *			this.view = MyView.getInstance(this.multitonKey, (key) => new MyView(key));
     *		}
     * `</pre>
     *
     */
    initializeController() {
        /** @protected
         * @type {View} **/
        this.view = View.getInstance(this.multitonKey, (key) => new View(key));
    }

    /**
     * `Controller` Multiton Factory method.
     *
     * @static
     * @param {string} key
     * @param {function(string):Controller} factory
     * @returns {Controller} the Multiton instance of `Controller`
     */
    static getInstance(key, factory) {
        if (Controller.instanceMap == null)
            /** @static
             @type {Map<string, Controller>} */
            Controller.instanceMap = new Map();
        if (Controller.instanceMap.get(key) == null) Controller.instanceMap.set(key, factory(key));
        return Controller.instanceMap.get(key);
    }

    /**
     * <P>If a `Command` has previously been registered
     * to handle the given `Notification`, then it is executed.</P>
     *
     * @param {Notification} notification a `Notification`
     */
    executeCommand(notification) {
        let factory = this.commandMap.get(notification.name);
        if (factory == null) return;

        let commandInstance = factory();
        commandInstance.initializeNotifier(this.multitonKey);
        commandInstance.execute(notification);
    }

    /**
     * <P>Register a particular `Command` class as the handler
     * for a particular `Notification`.</P>
     *
     * <P>If an `Command` has already been registered to
     * handle `Notification`s with this name, it is no longer
     * used, the new `Command` is used instead.</P>
     *
     * <P>The Observer for the new Command is only created if this the
     * first time a Command has been registered for this Notification name.</P>
     *
     * @param notificationName the name of the `Notification`
     * @param {function():SimpleCommand} factory
     */
    registerCommand(notificationName, factory) {
        if (this.commandMap.get(notificationName) == null) {
            this.view.registerObserver(notificationName, new Observer(this.executeCommand, this));
        }
        this.commandMap.set(notificationName, factory);
    }

    /**
     * Check if a Command is registered for a given Notification
     *
     * @param {string} notificationName
     * @return {boolean} whether a Command is currently registered for the given `notificationName`.
     */
    hasCommand(notificationName) {
        return this.commandMap.has(notificationName);
    }

    /**
     * Remove a previously registered `Command` to `Notification` mapping.
     *
     * @param {string} notificationName the name of the `Notification` to remove the `Command` mapping for
     */
    removeCommand(notificationName) {
        // if the Command is registered...
        if(this.hasCommand(notificationName)) {
            // remove the observer
            this.view.removeObserver(notificationName, this);

            // remove the command
            this.commandMap.delete(notificationName)
        }
    }

    /**
     * Remove a Controller instance
     *
     * @static
     * @param {string} key of Controller instance to remove
     */
    static removeController(key) {
        Controller.instanceMap.delete(key);
    }

    /**
     * Message Constants
     *
     * @static
     * @type {string}
     */
    static get MULTITON_MSG() { return "Controller instance for this Multiton key already constructed!" };
}
export { Controller }