© Matthew Duffield 2018
Matthew DuffieldPractical App Development with Aurelia https://doi.org/10.1007/978-1-4842-3402-0_20

20. Creating Plug-ins

Matthew Duffield
(1)
West Linn, Oregon, USA
 
In the last chapter, we learned how to create features that we could in turn use in our applications. In this chapter, we are going to learn how we can take our features a step further and create plug-ins so that we can install them via the CLI or Node Package Manager (NPM). We will look at creating a Node Package of your source code that will best structure your plug-in.

Going from a Feature to a Plug-in

Just as with features, it is important to structure your plug-ins in a way that is consistent with most of the official Aurelia plug-ins. Let’s take a look at the file structure a majority of Aurelia plug-ins use:
build/
dist/
doc/
src/
test/
You may want to create this base structure and put placeholder files in it so that you can have it as a starting repository. This way you use the structure for any new plug-in you wish to work on. One of the easiest ways to ensure that your plug-in is working is to use an existing Aurelia CLI project. In this project, create a bundle that only contains your plug-in and then copy this file to another project and configure the project to use it. Let’s use the input-mask as an example and see if we can get this working.

Existing Aurelia CLI Project

In this project, we will configure the CLI to create a bundle for the input-mask custom attribute. In the aurelia.json file under the aurelia_project folder, make sure the following is the first entry for the bundles array :
{
  "name": "index.js",
  "source": [
    "**/resources/index.js"
  ]
},
If this is not the first entry, the target file will be empty when you try to create the bundle. We are basically telling the CLI to take everything from our resources folder and bundle it up as a separate item. Here is the output from generating the index.js bundle:
define('resources/index',['exports'], function (exports) {
  'use strict';
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.configure = configure;
  function configure(config) {
    config.globalResources(['./attributes/input-mask/input-mask']);
  }
});
//# sourceMappingURL=index.js.map
We need to do a little work in order to use this generated file. The main reason for this is the fact that we have chosen to leave our source files in the exact same location as when we were using them. We need them now to be flattened out, and this means that we can remove all of the relative paths except the actual files. The following is the same file with the updated changes. We removed the ‘resources/index’ entry in the define function as well as the ‘/attributes/input-mask’ from the globalResources function. We also removed the sourceMapping entry but that is more of a preference:
define(['exports'], function (exports) {
  'use strict';
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.configure = configure;
  function configure(config) {
    config.globalResources(['./input-mask']);
  }
});
We have completed creating our index.js file; let’s now create our input-mask.js bundle. Add the following entry right after the index.js bundle:
{
  "name": "input-mask.js",
  "source": [
    "[**/resources/attributes/input-mask/*.js]",
    "**/resources/attributes/input-mask/*.{css,html}"
  ]
},
This simply is creating a new input-mask.js bundle targeting the input-mask folder under the attributes folder. The following is the output from generating this file:
define('resources/attributes/input-mask/input-mask',['exports', 'aurelia-framework', 'aurelia-pal'], function (exports, _aureliaFramework, _aureliaPal) {
  'use strict';
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.InputMask = undefined;
  function _initDefineProp(target, property, descriptor, context) {
    if (!descriptor) return;
    Object.defineProperty(target, property, {
      enumerable: descriptor.enumerable,
      configurable: descriptor.configurable,
      writable: descriptor.writable,
      value: descriptor.initializer ? descriptor.initializer.call(context) : void 0
    });
  }
  function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
      throw new TypeError("Cannot call a class as a function");
    }
  }
  function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
    var desc = {};
    Object['ke' + 'ys'](descriptor).forEach(function (key) {
      desc[key] = descriptor[key];
    });
    desc.enumerable = !!desc.enumerable;
    desc.configurable = !!desc.configurable;
    if ('value' in desc || desc.initializer) {
      desc.writable = true;
    }
    desc = decorators.slice().reverse().reduce(function (desc, decorator) {
      return decorator(target, property, desc) || desc;
    }, desc);
    if (context && desc.initializer !== void 0) {
      desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
      desc.initializer = undefined;
    }
    if (desc.initializer === void 0) {
      Object['define' + 'Property'](target, property, desc);
      desc = null;
    }
    return desc;
  }
  function _initializerWarningHelper(descriptor, context) {
    throw new Error('Decorating class property failed. Please ensure that transform-class-properties is enabled.');
  }
  var _dec, _class, _desc, _value, _class2, _descriptor, _class3, _temp;
  var InputMask = exports.InputMask = (_dec = (0, _aureliaFramework.customAttribute)('input-mask'), _dec(_class = (_class2 = (_temp = _class3 = function () {
    function InputMask(element) {
      _classCallCheck(this, InputMask);
      _initDefineProp(this, 'pattern', _descriptor, this);
      this.element = element;
      if (element instanceof HTMLInputElement) {
        this.element = element;
      } else {
        throw new Error('The input-mask attribute can only be applied on Input elements.');
      }
    }
    InputMask.prototype.attached = function attached() {
      this.element.addEventListener("keydown", this.keyDownHandler.bind(this));
    };
    InputMask.prototype.detached = function detached() {
      this.element.removeEventListener("keydown", this.keyDownHandler.bind(this));
    };
    InputMask.prototype.keyDownHandler = function keyDownHandler(e) {
      var value = e.target.value;
      var isInt = Number.isInteger(Number.parseInt(e.key));
      var key = e.key.toLowerCase();
      var valueLen = value.length;
      var patternLen = this.pattern.length;
      var char = this.pattern[valueLen];
      var options = {
        e: e,
        value: value,
        isInt: isInt,
        key: key,
        valueLen: valueLen,
        patternLen: patternLen,
        char: char
      };
      var result = true;
      if (this.isValidNonInputKey(key)) {} else if (valueLen === patternLen) {
        e.preventDefault();
        result = false;
      } else if (char === '#' && isInt) {} else if (this.processKey(options)) {} else {
        e.preventDefault();
        result = false;
      }
      return result;
    };
    InputMask.prototype.processKey = function processKey(options) {
      var key = options.key,
          char = options.char,
          isInt = options.isInt,
          valueLen = options.valueLen,
          e = options.e;
      if (key === char) {
        return true;
      } else if (char !== '#' && isInt) {
        var nextChar = this.pattern[valueLen + 1];
        if (nextChar === ' ') {
          e.target.value = e.target.value + char + ' ';
        } else {
          e.target.value = e.target.value + char;
        }
        return true;
      }
      return false;
    };
    InputMask.prototype.isValidNonInputKey = function isValidNonInputKey(key) {
      var keys = ["backspace", "arrowleft", "arrowright", "arrowup", "arrowdown", "home", "end", "tab"];
      return keys.includes(key);
    };
    return InputMask;
  }(), _class3.inject = [_aureliaPal.DOM.Element], _temp), (_descriptor = _applyDecoratedDescriptor(_class2.prototype, 'pattern', [_aureliaFramework.bindable], {
    enumerable: true,
    initializer: function initializer() {
      return '';
    }
  })), _class2)) || _class);
});
//# sourceMappingURL=input-mask.js.map
The only change necessary for this file is the very first line. Again, we want to remove the relative path, ‘resources/attributes/input-mask/input-mask’. We also will remove the very last line referencing the sourceMapping.
You now have your index.js and input-mask.js files created. These two files can go in the dist/amd/ folder. By default the Aurelia CLI generates modules in the AMD format. This can also be changed if you go to the aurelia.json file and refer to the ‘transpiler’ section as shown in the following:
"transpiler": {
  "id": "babel",
  "displayName": "Babel",
  "fileExtension": ".js",
  "options": {
    "plugins": [
      "transform-es2015-modules-amd"
    ]
  },
  "source": "src/**/*.js"
},
When you create an Aurelia CLI project targeting Babel, you will have the ‘transform-es2015-modules-amd’ and ‘transform-es2015-modules-commonjs’ plug-ins available out of the box. You can simply change out which target you wish to use for creating the other formats. You can also put your tests and documentation in the corresponding folders of your structure so that they are part of the repository when you commit.
This was a very contrived and painful approach to creating the bundled file formats for a given plug-in. Luckily, there exists an aurelia-plugin repository whose sole task is to make it easier to create plug-ins for Aurelia. You can find the plug-in here: https://github.com/aurelia/skeleton-plugin
Also, the Aurelia team is actively working on making it easier to produce plug-ins directly from the CLI.

Publishing your Plug-in

Now that we have our plug-in coded and our bundles generated appropriately, we need to make it available for consumption by other developers. The most common approach to this is to have your project source controlled with something like GitHub and then use NPM to publish it. NPM makes extensive use of your package.json file, so it is important that it is accurate. Take some time to ensure that you have all key properties filled out.
Execute the following command to publish your project:
npm publish ./
If this is the first time you have published to NPM on your development machine, you may also need to provide credentials as follows:
npm set init.author.name "Your name"
npm set init.author.email [email protected]
npm set init.author.url http://yourblob.com
npm adduser
These steps will get you set up for publishing to NPM.
After you have published and have made modifications to your code base and committed those changes to GitHub, it is important that you increment the version number in the package.json file prior to publishing the update.

Using Your Plug-in

You are finally ready to use your plug-in just like all the rest of the plug-ins available. Let’s say that you created your plug-in with the name ‘input-mask’; then you could execute the following command:
au install input-mask
or
npm install –save input-mask
Next, you will need to configure your main.js file to include the installed plug-in as follows:
import {PLATFORM} from 'aurelia-pal';
export function configure(aurelia) {
  aurelia.use
    .standardConfiguration()
    .developmentLogging()
    .plugin(PLATFORM.moduleName('input-mask'));
  aurelia.start().then(a => a.setRoot());
}
You only need to use the input-mask plug-in in your code like the following:
<template>
  <form>
    <div class="form-group">
      <label for="phone">
        Phone Number
      </label>
      <input id="phone"
        type="text"
        value.bind="currentRecord.phone"
        input-mask="pattern: (###) ###-####;"
        placeholder="(555) 555-1234">
    </div>
  </form>  
</template>
That’s it! You now have your custom attribute available as a plug-in for use by any developer.

Summary

Plug-ins are very powerful in that they let you bundle up functionalities and deliver them as NPM packages. This is one area where it is still a little complex, but the Aurelia team working hard on making this a seamless activity via the Aurelia CLI. Take your time and play with plug-ins. It is okay if you don’t get them working on the first attempt. You will find it rewarding once you have everything working and you can start sharing your plug-ins with the rest of the community.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.5.86