-
Notifications
You must be signed in to change notification settings - Fork 142
Plug in Architecture
Plug-ins (or "add-ons") are basically mix-ins; there is no real plug-in "architecture" at this time other than the installation methodology described here.
Plug-ins currently live in the add-ons
folder and are not part of Hypergrid "core," i.e., not part of the browserfied build on the CDN.
Plug-ins, like Hypergrid itself, are reduced by a build process (using Browserify) to a single .js
file and output to the build folder as both dev and min (minified; no source map) versions. From there they can be referenced in a <script>
tag's src
attribute.
However, also like Hypergrid, they are also npm modules which you, as the application developer, can "require" into your own browserified build. (None of them have been published yet, however, and we have not decided which ones we will publish and which we will simply leave as sample code. Until such time as they are published, you can use them as local npm modules.)
The npm module approach has several advantages, starting with the fact that your code will be reduced to a single file, requiring a single <script>
tag (as opposed to separate <script>
tags for all your files and plug-ins). (By long-standing convention, web pages read script files sequentially by default. This can obviously impact 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 - May have an
install
method - Must have one of the above; may have both.
-
object API plug-ins
- These are functions which are treated as object constructors.
- May 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: Same args as in first call
- 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.mixIn.call(grid.behavior, {
/* behavior members go here */
};
grid.mixIn.call(grid.dataModel, ({
/* dataModel members go here */
};
}
};
function HyperPlugin(grid) {
this.grid = grid;
}
Hyperfilter.prototype.name: 'HyperPlugin';
Hyperfilter.prototype.prop: ...;
Hyperfilter.prototype.minRowCount: function() { return this.grid.getRowCount(); };
When the above plug-in is installed, HyuperPlugin is instantiated, and the new object is saved in myGrid.plugins.hyperPlugin
. You can then call on the plug-in like this:
myGrid.plugins.hyperPlugi.minRowCount()`
Note by the way how the first character of the name is always forced to lower case.
Note that no mix-in was needed, 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
}