handsontable / handsontable-skeleton

Handsontable Skeleton

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Simplify external plugin development with a plugin factory

aarondfrancis opened this issue · comments

I've found that when creating multiple external plugins, the process can get a bit tedious.

I've created an internal PluginFactory that makes it a lot easier. Not sure if this is useful enough to be used somewhere.

plugin-factory.js

import Handsontable from 'handsontable';

export default function (settingsKey) {
    /**
     * @param hotInstance
     * @constructor
     */
    let plugin = function (hotInstance) {
        // Call the BasePlugin constructor.
        Handsontable.plugins.BasePlugin.call(this, hotInstance);

        this.optionalCall('setup')

        this._superClass = Handsontable.plugins.BasePlugin;
    }

    // Inherit the BasePlugin prototype.
    plugin.prototype = Object.create(Handsontable.plugins.BasePlugin.prototype, {
        constructor: {
            writable: true,
            configurable: true,
            value: plugin
        },
    });

    plugin.prototype.optionalCall = function (name) {
        if (this[name] && typeof this[name] === 'function') {
            this[name]();
        }
    }

    /**
     * Checks if the plugin is enabled in the settings.
     */
    plugin.prototype.isEnabled = function () {
        return !!this.hot.getSettings()[settingsKey];
    }

    /**
     * The enablePlugin method is triggered on the beforeInit hook. It should contain your initial plugin setup, along with
     * the hook connections.
     * Note, that this method is run only if the statement in the isEnabled method is true.
     */
    plugin.prototype.enablePlugin = function () {
        this.optionalCall('enable');

        // The super class' method assigns the this.enabled property to true, which can be later used to check if plugin is already enabled.
        this._superClass.prototype.enablePlugin.call(this);
    };

    /**
     * The disablePlugin method is used to disable the plugin. Reset all of your classes properties to their default values here.
     */
    plugin.prototype.disablePlugin = function () {
        this.optionalCall('disable');

        // The super class' method takes care of clearing the hook connections and assigning the 'false' value to the 'this.enabled' property.
        this._superClass.prototype.disablePlugin.call(this);
    };

    /**
     * The destroy method should de-assign all of your properties.
     */
    plugin.prototype.destroy = function () {
        this.optionalCall('teardown');

        // The super method takes care of de-assigning the event callbacks, plugin hooks and clearing all the plugin properties.
        this._superClass.prototype.destroy.call(this);
    };

    return plugin;
}

custom-headers.js

import PluginFactory from './plugin-factory';

let CustomHeaders = PluginFactory('customHeaders');

CustomHeaders.prototype.enable = function () {
    this.hot.getSettings().afterGetColHeader = this.afterGetColHeader.bind(this);
    this.hot.getSettings().afterGetRowHeader = this.afterGetRowHeader.bind(this);
}

CustomHeaders.prototype.afterGetColHeader = function (col, th) {
    // Modify column header
}

CustomHeaders.prototype.afterGetRowHeader = function (row, th) {
    // Modify row header
}

export default CustomHeaders;

I've made a few changes to this over the past year, thought I would share the latest.

This one allows you to extend core plugins by passing a parent in.

import Handsontable from 'handsontable';

export default function (settingsKey, parent = Handsontable.plugins.BasePlugin) {
    /**
     * @param hotInstance
     * @constructor
     */
    let plugin = function (hotInstance) {
        // Call the BasePlugin constructor.
        parent.call(this, hotInstance);

        this.optionalCall('setup')

        this._superClass = parent;

        this.settings = this.getSettings();
    }

    // Inherit the BasePlugin prototype.
    plugin.prototype = Object.create(parent.prototype, {
        constructor: {
            writable: true,
            configurable: true,
            value: plugin
        },
    });

    /**
     * Register the plugin in the HoT registry.
     */
    plugin.prototype.register = function () {
        Handsontable.plugins.registerPlugin(settingsKey, plugin);
    }

    /**
     * Convenience method to call functions in the plugin if they exist.
     * @param name
     */
    plugin.prototype.optionalCall = function (name) {
        if (this[name] && typeof this[name] === 'function') {
            this[name]();
        }
    }

    /**
     * Checks if the plugin is enabled in the settings.
     */
    plugin.prototype.isEnabled = function () {
        return !!this.hot.getSettings()[settingsKey];
    }

    /**
     * Add a hook with a handler.
     *
     * @param hook
     */
    plugin.prototype.handleHook = function (hook, handler = hook, once = false) {
        if (!this[handler]) {
            throw new Error(`Unknown handler for hook ${hook}: ${handler}`);
        }

        let method = once ? 'addHookOnce' : 'addHook';

        this.hot[method](hook, this[handler].bind(this));
    }

    /**
     * Add a hookOnce with a handler.
     *
     * @param hook
     */
    plugin.prototype.handleHookOnce = function (hook, handler = hook) {
        this.handleHook(hook, handler, true);
    }

    /**
     * The enablePlugin method is triggered on the beforeInit hook. It should
     * contain your initial plugin setup, along with the hook connections.
     *
     * Note, that this method is run only if the statement in the isEnabled
     * method is true.
     */
    plugin.prototype.enablePlugin = function () {
        this.settings = this.getSettings();

        this.optionalCall('enable');

        // The super class' method assigns the this.enabled property to true,
        // which can be later used to check if plugin is already enabled.
        this._superClass.prototype.enablePlugin.call(this);

        // Add a hook to run once right before anything is rendered. This
        // will give the plugin a chance to initialize anything that
        // relies on the hot.view, amongst other things.
        this.hot.addHookOnce('beforeRenderer', () => {
            this.optionalCall('ready')
        });
    };

    /**
     * The disablePlugin method is used to disable the plugin. Reset all of
     * your classes properties to their default values here.
     */
    plugin.prototype.disablePlugin = function () {
        this.optionalCall('disable');

        // The super class' method takes care of clearing the hook connections
        // and assigning the 'false' value to the 'this.enabled' property.
        this._superClass.prototype.disablePlugin.call(this);
    };

    /**
     * The updatePlugin method is called on the afterUpdateSettings hook
     * (unless the updateSettings method turned the plugin off).
     *
     * It should contain all the stuff your plugin needs to do to work
     * properly after the Handsontable instance settings were modified.
     */
    plugin.prototype.updatePlugin = function () {
        this.optionalCall('update');

        // The updatePlugin method needs to contain all the code needed to
        // properly re-enable the plugin. In most cases simply disabling
        // and enabling the plugin should do the trick.
        this.disablePlugin();
        this.enablePlugin();

        this._superClass.prototype.updatePlugin.call(this);
    };

    /**
     * The destroy method should de-assign all of your properties.
     */
    plugin.prototype.destroy = function () {
        this.optionalCall('teardown');

        // The super method takes care of de-assigning the event callbacks,
        // plugin hooks and clearing all the plugin properties.
        this._superClass.prototype.destroy.call(this);
    };

    /**
     * Get the plugin settings
     */
    plugin.prototype.getSettings = function () {
        return this.hot.getSettings()[settingsKey];
    };

    return plugin;
}

export function registerPlugins(plugins) {
    for (let i = 0; i < plugins.length; i++) {
        plugins[i].prototype.register();
    }
}

Updated for 8.1.0

import Handsontable from 'handsontable';

export default function (settingsKey, parent = Handsontable.plugins.BasePlugin) {
    /**
     * @param hotInstance
     * @constructor
     */
    let plugin = function (hotInstance) {
        // Call the BasePlugin constructor.
        let _this = parent.call(this, hotInstance) || this;

        _this.optionalCall('setup')

        _this._superClass = parent;

        _this.settings = _this.getSettings();

        return _this;
    }

    // Inherit the BasePlugin prototype.
    plugin.prototype = Object.create(parent.prototype, {
        constructor: {
            writable: true,
            configurable: true,
            value: plugin
        },
    });

    /**
     * Register the plugin in the HoT registry.
     */
    plugin.prototype.register = function () {
        Handsontable.plugins.registerPlugin(settingsKey, plugin);
    }

    /**
     * Convenience method to call functions in the plugin if they exist.
     * @param name
     */
    plugin.prototype.optionalCall = function (name) {
        if (this[name] && typeof this[name] === 'function') {
            this[name]();
        }
    }

    /**
     * Checks if the plugin is enabled in the settings.
     */
    plugin.prototype.isEnabled = function () {
        return !!this.hot.getSettings()[settingsKey];
    }

    /**
     * Add a hook with a handler.
     *
     * @param hook
     */
    plugin.prototype.handleHook = function (hook, handler = hook, once = false) {
        if (!this[handler]) {
            throw new Error(`Unknown handler for hook ${hook}: ${handler}`);
        }

        let method = once ? 'addHookOnce' : 'addHook';

        this.hot[method](hook, this[handler].bind(this));
    }

    /**
     * Add a hookOnce with a handler.
     *
     * @param hook
     */
    plugin.prototype.handleHookOnce = function (hook, handler = hook) {
        this.handleHook(hook, handler, true);
    }

    /**
     * The enablePlugin method is triggered on the beforeInit hook. It should
     * contain your initial plugin setup, along with the hook connections.
     *
     * Note, that this method is run only if the statement in the isEnabled
     * method is true.
     */
    plugin.prototype.enablePlugin = function () {
        this.settings = this.getSettings();

        this.optionalCall('enable');

        // The super class' method assigns the this.enabled property to true,
        // which can be later used to check if plugin is already enabled.
        this._superClass.prototype.enablePlugin.call(this);

        // Add a hook to run once right before anything is rendered. This
        // will give the plugin a chance to initialize anything that
        // relies on the hot.view, amongst other things.
        this.hot.addHookOnce('beforeRenderer', () => {
            this.optionalCall('ready')
        });
    };

    /**
     * The disablePlugin method is used to disable the plugin. Reset all of
     * your classes properties to their default values here.
     */
    plugin.prototype.disablePlugin = function () {
        this.optionalCall('disable');

        // The super class' method takes care of clearing the hook connections
        // and assigning the 'false' value to the 'this.enabled' property.
        this._superClass.prototype.disablePlugin.call(this);
    };

    /**
     * The updatePlugin method is called on the afterUpdateSettings hook
     * (unless the updateSettings method turned the plugin off).
     *
     * It should contain all the stuff your plugin needs to do to work
     * properly after the Handsontable instance settings were modified.
     */
    plugin.prototype.updatePlugin = function () {
        this.optionalCall('update');

        // The updatePlugin method needs to contain all the code needed to
        // properly re-enable the plugin. In most cases simply disabling
        // and enabling the plugin should do the trick.
        this.disablePlugin();
        this.enablePlugin();

        this._superClass.prototype.updatePlugin.call(this);
    };

    /**
     * The destroy method should de-assign all of your properties.
     */
    plugin.prototype.destroy = function () {
        this.optionalCall('teardown');

        // The super method takes care of de-assigning the event callbacks,
        // plugin hooks and clearing all the plugin properties.
        this._superClass.prototype.destroy.call(this);
    };

    /**
     * Get the plugin settings
     */
    plugin.prototype.getSettings = function (hot = this.hot) {
        return hot.getSettings()[settingsKey];
    };

    return plugin;
}

export function registerPlugins(plugins) {
    for (let i = 0; i < plugins.length; i++) {
        plugins[i].prototype.register();
    }
}