Skip to content

Plug in Architecture

Jonathan Eiten edited this page May 4, 2017 · 9 revisions

Definition

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.

Where found

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.)

Types of plug-ins

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.)

Installation

Each plug-in is installed as follows:

  1. 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.
  2. 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.

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.

Implicit installation

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.

Explicit installation

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

Additional installation arguments

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.

API names

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):

  1. As named in the pluginSpec
  2. The simple API object has a name property
  3. The instantiated object API object has a name or $$CLASS_NAME property
  4. Unnamed if none of the above

Saving API references

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.

Plug-in specs

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).

Example plug-in installation spec

A plug-in installation spec consists of either a single plug-in spec, or a list of 0 or more such plug-in specs.

Example

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 and plugin2 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 and plugin5 demonstrate additional installation arguments.
  • plugin6 demonstrates the more typical case of an options 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 an options argument; and when they do they may or may not respect a shared option. It is up to the application developer to understand each plug-in's specifications.

Plug-in development

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 mixIn method

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."

Example A

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).

Example B: Deciding "shared" at run-time

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

Example C: Complex plug-in

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 */
        };
    }
};

Example D: Object API plug-in

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:

  1. The first character of the name is always forced to lower case.
  2. 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
}