-
Notifications
You must be signed in to change notification settings - Fork 143
Plug in Architecture
Plug-ins (or "add-ons") are basically APIs. Once a plug-in is installed, it can be referenced as myGrid.plugins.myPlugin
.
During plugin installation, the plugin can perform setup chores, which may or may not include mixing code into the Hypergrid
instance or its prototype, and/or the instances or prototypes of its daughter objects.
A given plug-in, like Hypergrid itself, can be either or both of the following:
- An external module,
require
-able by your application at build time
or - An external Javascript file, downloaded by the client at run-time with a
<script>
tag, and available to your application through a global variable.
We recommend the first method. Building plug-ins into your application build has the the following advantages:
- When your application and its plug-ins are Browserified together, they share commonly
require
'd modules. - A single downloadable file requires only one
<script>
tag. (By long-standing convention, web pages read script files sequentially by default, which impacts the page's load time. Even when marked as asynchronous, browsers typically can only read up to four files at a time.)
There are two basic types of plug-ins:
-
simple API plug-ins
- These are plain objects
- May have a
preinstall
method, called before grid initialization - May have an
install
method, called after grid initialization - Must have one of the above; may have both.
-
object API plug-ins
- These are functions which are treated as object constructors.
- May also have a
preinstall
method. - (If it happens to have an
install
method, it is ignored for installation purposes.)
Each plug-in is installed as follows:
-
Before grid initialization (before the grid's daughter objects have been instantiated)
- Call
preinstall
method - First arg:
Hypergrid.prototype
(see examples) - Additional args: Custom args per plug-in requirements
- A reference to the shared plug-in, if it has a name, is held in the
Hypergrid.plugins
dictionary.
- Call
-
After grid initialization (after all the grid's daughter objects have been instantiated)
- Call
install
method (simple API plug-in) - Call constructor with
new
keyword (object API plug-in). - First arg:
myGrid
(a grid instance; see examples) - Additional args: Custom args per plug-in requirements
- A reference to the instance plug-in, if it has a name, is held in the
myGrid.plugins
dictionary.
- Call
A flexible plug-in can be written to decide at run-time whether to install shared or instance by implementing both preinstall
and install
methods. The decision logic might check an installation option to make the determination. See below for an example.
Simply pass a plugins
option to the Hypergrid constructor with a list of plug-ins:
var plugins = [...]; // list of plug-ins and/or plugin specs
var myGrid = new Hypergrid({ plugins: plugins });
That's it; you're done! Hypergrid calls its installPlugins
method which does all the heavy lifting. This method is called twice, once before grid initialization and again after grid initialization.
For finer control, you could call installPlugins
yourself before and/or after Hypergrid instantiation. The following accomplishes exactly the same thing as the implicit installation described above.
var plugins = [...]; // list of plug-ins and/or plugin specs
Hypergrid.prototype.installPlugins(plugins);
var myGrid = new Hypergrid(...);
myGrid.installPlugins(plugins);
Plug-in installers have a required first argument that is either the grid instance (constructor or install
method) or the grid prototype (preinstall
method).
Sometimes a plug-in installer will have additional installation arguments. Depending on the plug-in, these may be requirements, or may be options. installPlugins
passes any additional arguments included in the pluginSpec
.
Typical design patterns for additional arugments include:
- A single additional argument called
options
, a hash of options, some of which may actually be requiured and some of which are true "options." - Required additional argument(s) followed by a single additional
options
hash.
See below for an example.
The simple API or the new object resulting from the object API instantiation is saved when and only when the API has a name. APIs are named as follows (in priority order):
- As named in the
pluginSpec
- The simple API object has a
name
property - The instantiated object API object has a
name
or$$CLASS_NAME
property - Unnamed if none of the above
If named:
- A reference to each shared plug-in is saved in
Hypergrid.plugins
- A reference to each instance plug-in is saved in
myGrid.plugins
If the plug-in is unnamed, no reference is saved.
The pluginSpec
mentioned above is actually just a jsdoc typedef name. It takes any of the following forms:
-
undefined
(or any other falsy value) - This is a no-op (fails silently). - A reference to a plug-in API - Installs the plug-in as explained above.
- An array:
- Optional: Plug-in name (a string).
- Reference to plug-in API.
- Optional: Additional installation argument(s).
A plug-in installation spec consists of either a single plug-in spec, or a list of 0 or more such plug-in specs.
The term "args" in the comments below means "additional installation arguments."
var plugins = [
undefined, // no-op
[], // also a no-op
require('plugin1'), // plug-in reference sans name override and args
[require('plugin2')], // plug-in reference sans name override and args
['myPlugin3', require('plugin3')], // plug-in reference with name override
[require('plugin4'), 42], // plug-in reference with single installation argument
['myPlugin5', require('plugin5'), 42, 'hello'], // plug-in reference with name override and multiple args
[require('plugin6'), { shared: true }], // plug-in with an `options` hash as its only arg
];
Notes:
-
plugin1
andplugin2
may or may not have implicit names. -
plugin3
may or may not have an implicit name but it doesn't matter because its name is overridden (with "myPlugin3"). -
plugin4
andplugin5
demonstrate additional installation arguments. -
plugin6
demonstrates the more typical case of anoptions
hash as the only additional installation argument. Semantically, the intent in this example is to tell the plug-in to install itself as a shared plug-in. Note however that plug-ins are not guaranteed to have anoptions
argument; and when they do they may or may not respect ashared
option. It is up to the application developer to understand each plug-in's specifications.
Plug-ins using require()
must be browserified in order to be read in by your page using <script>
tags. There are a number of caveats to be aware of with this paradigm. Attempts to require()
ing anything that Hypergrid also requires (including the Hypergrid
object or its prototype or any of Hypergrid's internal "classes") will create a copy inside the plug-in's build. Besides the size issues, these objects are distinct copies, which is often problematic.
Consider require()
ing your plug-ins (instead of including their build files with <script>
tags); and building your app with Browserify.
Another somewhat less elegant solution, at least for referencing Hypergrid objects, is to reference the grid
parameter to get access to the Hypergrid
prototype, its constructor, and its properties (which include references to many of Hypergrid's internal "classes"):
function install(grid) {
var hypergridPrototype = Object.getPrototypeOf(grid);
var Hypergrid = hypergridPrototype.constructor;
var Behavior = Hypergrid.behaviors.Behavior;
...
}
Yet another solution is to include Hypergrid in your build so that you can freely require()
its objects. This of course has the downside that all of Hypergrid will end up in your build which is not ideal.
The following examples make use of a method called mixIn
which seems to be available on the grid object. Actually, it is defined in fin-hypergrid/src/Base.js, making it available on the prototypes (and hence the instances) of many Hypergrid "classes."
Typical simple API might look like this:
'use strict';
var myPlugin = {
member1: ...,
member2: ...,
member3: ...
};
Object.defineProperty(myPlugin, 'install', { value: installer }); // non-enumerable
function installer(grid) {
grid.mixIn(this);
}
module.exports = myPlugin;
The point of making the install
method non-enumerable by using Object.defineProperty
is so that it won't itself be mixed into the grid instance.
The above implementation can be made to install as a shared plug-in (to the prototype) simply by changing 'install'
to 'preinstall'
. In that case, installer
will be called with Hypergrid.prototype
instead of the grid instance. Note that the this
in installer
is always going to be myPlugin
(which you could specify instead).
'use strict';
var myPlugin = {
member1: ...,
member2: ...,
member3: ...
};
Object.defineProperties(myPlugin, { // non-enumerable methods
preinstall: { value: function(grid, shared) { if (shared) { grid.mixIn(this); } } },
install: { value: function(grid, shared) { if (!shared) { grid.mixIn(this); } } },
});
module.exports = myPlugin;
This expects one additional installation argument, shared
, which should be true
or false
. The following example supplies this additional argument, installing the myPlugin
plug-in as shared (because shared
is true
):
var shared = true;
var plugins = [ require('myplugin'), shared ];
var myGrid = new Hypergrid({ plugins: plugins });
Plug-in installers can be much richer, for example by mixing into various objects:
var myComplexPlugin = {
install: function(grid) {
grid.mixIn({
/* grid members go here */
};
grid.behavior.mixIn({
/* behavior members go here */
};
grid.behavior.dataModel.mixIn({
/* dataModel members go here */
};
}
};
function MyPlugin(grid) {
this.grid = grid;
}
MyPlugin.prototype = {
constructor: MyPlugin.prototype.constructor,
name: 'MyPlugin',
prop: ...,
minRowCount: function() { return this.grid.getRowCount(); }
};
When the above plug-in is installed, MyPlugin is instantiated, and the new object is saved in myGrid.plugins.myPlugin
. You can then call on the plug-in like this:
myGrid.plugins.myPlugin.minRowCount();
Notes:
- The first character of the name is always forced to lower case.
- In the above example, no mix-in was involved, although you could also mix-in code from the constructor if you wanted to:
function HyperPlugin(grid) {
this.grid = grid;
grid.mixIn(require('./mix-ins/grid')); // instance mix-in
Object.getPrototypeOf(grid.behavior).mixIn(require('./mix-ins/behavior')); // prototype mix-in
}