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();
}
}