Plugin Development

Make sure to read the Getting Started Guide before you continue reading this section.

Project creation

  1. Create the project directory: mkdir jetjs-plugin-<name>
  2. Initiate the project with npm init and create a source directory: mkdir src
  3. Add a tsconfig.js to your project. I'm using the following one:
{
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules"
    ],
    "compilerOptions": {
      "allowSyntheticDefaultImports": true,
      "allowUnreachableCode": false,
      "allowUnusedLabels": false,
      "declaration": true,
      "esModuleInterop": true,
      "forceConsistentCasingInFileNames": true,
      "module": "ES6",
      "moduleResolution": "node",
      "noEmitOnError": true,
      "noFallthroughCasesInSwitch": true,
      "noImplicitAny": false,
      "noImplicitReturns": true,
      "pretty": true,
      "sourceMap": true,
      "strict": true,
      "strictNullChecks": true,
      "target": "es6",
      "outDir": "build/",
      "lib": [
        "dom",
        "es2016"
    ]
 }
}

It is important to update your main entrypoint inside your package.json file and add a typing field:

  "main": "build/index.js",
  "types": "build/index.d.ts",
  1. Add the needed dependencies to your project
$ npm i -D typescript rimraf

Typescript is needed to compile the Plugin to JavaScript and the package rimraf will be used to clear the build directory. You can use the following script inside your package.json to build the plugin:

  "scripts": {
    "build": "rimraf build dist && tsc -p tsconfig.json"
  },

The Jet.js Library should be added as a peer dependency.

  "peerDependencies": {
    "@jetjs/jetjs": "~0.0.1"
  },
  1. create an index.ts file and a <plugin-name>.ts file inside your src directory.

Plugin implementation

Your <plugin-name>.ts file should only include the plugin implementation and nothing else (e.g. jetjs registration - this will be done inside the index.tsfile.) It has one export with the factory function:

// <plugin-name>.ts file

/**
 * Factory function for <plugin-name>
 *
 * @param node The HTML Element on which the plugin was defined and should be executed on.
 */
export myPluginName = (node: HTMLElement) => {

};

If your Plugin needs to be configured with options, they can be defined as second argument. They will are parsed from the data-Attributes where the plugin is defined or while an Alias is defined. To define your Options, create a new File called Options.ts and export an interface wich contains every property you want to receive:

export interface Options {
  someProperty: string;
}

Note: Currently all properties are passed as strings, because they are defined as data Attributes inside your HTML Markup. If you need other data types like number, you have to convert it by yourself inside your factory function.

Extend your plugin, to receive the options:

// <plugin-name>.ts file
import { Options } from "./Options";

/**
 * Factory function for <plugin-name>
 *
 * @param node The HTML Element on which the plugin was defined and should be executed on.
 * @param opts Plugin options
 */
export myPluginName = (node: HTMLElement, opts: Options) => {
  // TODO: convert and validate your options
};

Void Plugin

To implement the Hello-World Example, we have to modify our Options Interface and need to implement the actual logic inside our factory function.

// Options.ts
export interface Options {
  /**
   * the message to show.
   */
  message?: string;
}
// <plugin-name>.ts file
import { Options } from "./Options";

/**
 * Factory function for <plugin-name>
 *
 * @param node The HTML Element on which the plugin was defined and should be executed on.
 */
export helloWorld = (node: HTMLElement, opts: Options) => {
  // convert and validate your options
  const msg = opts.message || "Hello World"; // use "hello World" as default

  // register event listener on the given node
  node.addEventListener("click", () => {
    alert(msg); // show the message on each click
  });
};

Observable Plugin

To reuse the event handling, we extract the logic to its own on Plugin. To create Plugins from type subscriber, operator and observable we need to add the @jetjs/streams package to the peer dependencies.

// Options.ts
export interface Options {
  /**
   * the event name to listen for.
   */
  event: string;
}
// on.ts file
import { Observable, SubscriptionObserver } from "@jetjs/streams";
import { Options } from "./Options";

export const on = (node: HTMLElement, options: Options) => {
  // validate options (can be undefined, because the object is constructed at runtime -> no type safety)
  if (!options.event) {
    throw new Error("Option [event] must be specified to execute the on Plugin");
  }

  // return a new observable
  return new Observable((subscriptionObserver: SubscriptionObserver<Event>) => {
    // producer - function called if the stream should be started (a subscriber is registered)

    const listener = (event: Event) => {
      // pass event to the stream
      subscriptionObserver.next(event);
    };

    // start listening for events
    node.addEventListener(options.event, listener);

    return () => {
      // cancel observable (remove event listener from node)
      node.removeEventListener(options.event, listener);
    };
  });
};

Subscriber Plugin

To show the alert, we need an alert Plugin from type subscriber. It can be implemented like the following:

// Options.ts
export interface Options {
  /**
   * the event name to listen for.
   */
  event: string;
}
// alert.ts
import { SubscriptionObserver } from "@jetjs/streams";
import { Options } from "./Options";

export const alert = (node: HTMLElement, options: Options): SubscriptionObserver<any> => {
  // return a new subscriber
  return {
    next: (value: any) => {
      let msg = options.message;

      // received a value as message?
      if (typeof value === "string") {
        msg = value;
      }

      if (msg) {
        window.alert(msg);
      } else {
        console.warn("Alert-Plugin: Message Option not defined and the received value is not from type string. => Nothing to show");
      }
    },
    error: () => { /* ... */ },
    complete: () => { /* ... */ },
    unsubscribe: () => { /* ... */ },
  };
};

Plugin registration

To register the Plugin, the Plugin Authors should provide a utility function inside the index.ts. The following example is for the previous shown on Plugin. But it is needed also for the alert plugin.

// index.ts
import { Jetjs } from "@jetjs/jetjs";
import { on } from "./on";

export const PLUGIN_NAME = "on";
export const PLUGIN_FACTORY = on;

export interface OnPluginRegistrationOpts {
}

/**
 * Helper method to register the on Plugin with a Jet.js instance
 */
export const register = (jetjs: Jetjs, opts?: OnPluginRegistrationOpts) => {
  // register plugin
  jetjs.registerPlugin(PLUGIN_NAME, PLUGIN_FACTORY);
};

Inside your main file for your Application, you have to import the Plugins and call the register functions.

import jetjs from "@jetjs/jetjs";
import { register as registerOnPlugin } from "@jetjs/jetjs-plugin-on";
import { register as registerAlertPlugin } from "@jetjs/jetjs-plugin-alert";

jetjs.init({
  threadPoolWorkerSrc: "/js/jetjs-worker.min.js"
});

// ...

registerOnPlugin(jetjs);
registerAlertPlugin(jetjs);

jetjs.searchAndRun(document.body);

After registration, you can use the new plugins:

<button data-plugins="on | alert" data-on-event="click" data-alert-message="my first plugins">click me</button>

Test it yourself:

Plugin Alias

An Alias is a Plugin that points to an existing Plugins and provides some default options. This makes it easier to use Plugins. The on Plugin can provide an onClick Alias to shorten the definition. To do so, we extend the register function inside our index.ts file from the plugin.

import { Jetjs } from "@jetjs/jetjs";
import { on } from "./on";

// ...

export interface OnPluginRegistrationOpts {
  /**
   * wether aliases should be registered or not
   *
   * false to not register any aliases. True to register all aliases.
   */
  alias: boolean;
}

/**
 * Helper method to register the on Plugin with a Jet.js instance
 */
export const register = (jetjs: Jetjs, opts?: OnPluginRegistrationOpts) => {
  // register plugin
  jetjs.registerPlugin(PLUGIN_NAME, PLUGIN_FACTORY);

  // return if aliases should be ignored
  if (opts !== undefined && opts.alias === false) { return; }

  jetjs.registerPluginAlias("onClick", "on", {
    event: "click"
  });
};

After registration, you can use the new alias:

<button data-plugins="onClick | alert" data-alert-message="my first plugins">click me</button>

Test it yourself:

Plugin Chain

To make the plugin definition even shorter, we can define an alias for a chain of Plugins. It doesn't matter if these are real plugins or Aliases.

// ...

registerOnPlugin(jetjs);
registerAlertPlugin(jetjs);

jetjs.registerPluginChain("helloWorldPluginChain", () => {
  return {
    plugin: "onClick",
    parameter: {},
    pipe: {
      plugin: "alert",
      parameter: {
        message: "Hello World - from plugin chain"
      }
    }
  };
});

jetjs.searchAndRun(document.body);

Usage:

<button data-plugins="helloWorldPluginChain">click me</button>

Test it yourself:

To create reusable plugin chains, it can use parameters.

jetjs.registerPluginChain("helloWorldPluginChain", (opts) => {
  return {
    plugin: "onClick",
    parameter: {},
    pipe: {
      plugin: "alert",
      parameter: {
        message: opts.message || "Hello World - from plugin chain"
      }
    }
  };
});

Usage:

<button data-plugins="helloWorldPluginChain" data-hello-world-plugin-chain-message="Configurable message">click me</button>

Test it yourself:

Dependencies

If possible don't use external dependencies to implement the plugin. If needed, make sure to define it as peerDependency. This ensures developers can update the versions on their own or to pin it to a fixed version, if needed.

Publishing

To publish the plugin(s) to npm use a name following the schema jetjs-plugin-<name> and add the jetjs-plugin keyword to your package.json.