How do you create Aurelia plugins?


#1

I’ve identified a need to build a custom element that probably needs to be turned into a plugin (so others can use it). I was wondering if there was a way to write my custom component, put it in a folder, and then publish it to npm somehow. Haven’t quite figured it out and couldn’t find any guides.

I do see that plugins don’t have to be overly complex (at least from what I see in my node_modules aurelia plugins), but setting it up as a project does seem to be.

Right now I’m trying to figure out the bindingEngine feature because my plugin may require (essentially Angular 1.x watchers) on a big array of objects (So I don’t think I can use bindable/observable). I need basically either (a) a javascript class that has javascript code that the user enters or modifies and it affects the plugin component custom element. OR (b) a big giant JSON object array that the user can customize completely.

I can see that “ag-grid” plugin has decided to use like 40 bindables… so maybe I should do that?

Then after I build a folder: /myplugin/, myplugin.js, myplugin.html . I want to maybe run some command like “au generate plugin” (Just maybe write up a package.json and a few other things and it’s ready? e.g. https://docs.npmjs.com/creating-node-js-modules )

Or am I supposed to be making a “feature” I dont know. Plugin vs feature thing is confusing nomenclature.


#2

The easiest, and simplest, to publish a custom element plugin is to export a plain class:

export class MyElementPlugin {
  
}

To define view for custom element:

export class MyElementPlugin {
  static $view = '<template>...</template>'
}

// or with dependencies:
export class MyElementPlugin {
  static $view = {
    template: '<template>...</template>',
    dependencies: () => [ClassBValueConverter, import('./module-a')]
  }
}

To declare bindables:

export class MyElementPlugin {
  static $resource = {
    bindables: [
      'items',
      { name: 'selectedItem', defaultBindingMode: 'twoWay' }
    ]
  }
}

Then in your readme, ask the user of your plugin to do:

import { MyElementPlugin } from 'your-plugin';

...
aurelia.use.globalResources(MyElementPlugin)

No that minification will mangle your class name, so defines its resource name explicitly:

export class MyElementPlugin {
  static $resource = {
    name: 'my-element',
    // if custom attribute:
    // type: 'attribute'
  }
}

#3

This is a great tutorial. But I have questions… I actually succeeded but I did something completely different.

Questions for your way:

  • Why static? Why $ dollar sign?
  • How come you didn’t use @bindable but used “resource = { bindables:” never seen that before (and why is it that I don’t see this way anywhere)?
  • why “globalResources” shouldn’t it be “feature()” or “plugin()” ?

What I did instead (and please tell me if I did something wrong):

I may have syntax errors below because I’m not copy pasting.

I made a folder, “MyElementPlugin” then I put in an “index.js”, with:

export function configure(aurelia) {
    aurelia.globalResources(
      PLATFORM.moduleName('/my-element')
    );
 }

my-element.html

<template>
  <div ref="customElement"></div>
</template>

my-element.js has a class with:

@bindable customElement;

Then use needs:

aurelia.use.feature(PLATFORM.moduleName('MyElementPlugin/index'))

#4
  • Why static? Why $ dollar sign?
  • How come you didn’t use @bindable but used “resource = { bindables:” never seen that before (and why is it that I don’t see this way anywhere)?

It’s announced here. https://aurelia.io/blog/2018/06/24/aurelia-release-notes-june-2018
Traditionally, there’s only one way to register a resource or add a plugin or add a feature: via module path (or module Id string) pointing to the resource/plugin/feature. This doesn’t play well with static code analysis, and part of the reason why it took huge amount of effort to do first webpack plugin. We can refer to this way of adding resource as “dynamic”, as in “not known at build time”:

export class App {
  configureRouter(config) {
    config.map([
      { route: 'home', moduleId: 'pages/home' },
      { route: 'dashboard', moduleId: 'pages/dashboard' }
    ])
  }
}

There is no way for tooling to know that 'pages/home' refers to the file at 'pages/home' without a plugin or special implementation of bundler like Aurelia-CLI.

Since not all applications need this dynamic capability, it’s desirable to refer to resources via their constructor directly, instead of the module id pointing to where they are, that feature was developed to better support this scenario. Doing it this way helps tooling “see” & “build” dependency graph correctly at build time, aka static, naturally. This enables easier tooling integration, as there is no need for special plugin to teach tooling when a dependency of a module needs to be pulled into the final bundle (like PLATFORM.moduleName with webpack).

While developing this feature, I saw that it enables tooling free capability, so it seemed it would be better to have a way to declare bindables or resource configurations easily without decorator, as decorator seemed to be in neverland because of ECMAScript class field proposal. Thus, $resource and $view was in, $ is a breaking chance avoidance move, a compromise made to avoid major version bump, without breaking existing app, as resource and view (or plain field names generally) were pointed out to be too risky.

The final result of this is aurelia-script as announced at https://aurelia.io/blog/2018/11/04/aurelia-2018-q3-report, and soon something like following (PR at https://github.com/aurelia/router/pull/624 , @davismj and I are working on it) :

export class App {
  configureRouter(config) {
    config.map([
      { route: 'home', viewModel: () => import('pages/home') },
      { route: 'dashboard', viewModel: () => import('pages/dashboard') },
    ]);
  }
}

Note that native dynamic import API (or import('pages/...')) is naturally understood by probably 100% of tooling, enabling code splitting by default, while still helps with static code analysis.

  • why “globalResources” shouldn’t it be “feature()” or “plugin()” ?

.feature(), .plugin() when used with a module path, will be resolved to underlying exported configure function, which will eventually call .globalResources() normally, so if there is no special setup needed, it can be just the direct registration.


#5

for:

<template>
  <div ref="customElement"></div>
</template>

you are using ref binding, which means its value will be populated by Aurelia. So doing

@bindable customElement

isn’t really needed (the @bindable), unless you want to expose the element with ref="customElement" to outside. Its value can be accessed in bind() and attached() lifecycles


#6

viewModel: () => import(‘pages/home’)

I’m guessing that means the end of PLATFORM.moduleName for webpack (Aside from backwards compatibility)?

configure function, which will eventually call .globalResources()

Doesn’t that mean that I can still use feature()? Feature takes one argument: PLATFORM.moduleName(‘item’)… For readability seems better than calling it “globalResources”? You’re just writing the code for efficiency, since feature() calls globalResources() anyway I assume.

ref="customElement" to outside

The reason I used ref is because I’m building a plugin that requires access to the element. Currently, only “my-element.js” uses the bindable customElement for now.

Basically I have another ES class imported in that needs a variable “myCustomElement” that directly links to the HTML element in the DOM.

my-element.js

@bindable customElement;

attached(){
   generateImageOnDiv();
}

generateImageOnDiv(){
   this.currentClass = new ImportedClass(this.customElement, this.settings);
}

And then later from a “parent element” I am accessing “this.currentClass” via bindable in the parent class. To basically, go back in, and change the “this.settings” or other internal fields.

app.js

export class App {
 @bindable InstanceOfCustom;
  
  constructor(){ } 
  
  InstanceOfCustomChanged(newvalue, oldvalue){
    console.log ("ok, the image is ready on the div... We're ready, we can start manipulating InstanceOfCustom which was just empty", newvalue);
   this.InstanceOfCustom = newvalue;
   this.InstanceOfCustom.settings.push("newsetting", 124);
 }

I wasn’t sure how else to expose this.settings to the parent classes aside from <custom-element settings.bind="passInArray">

I also thought about using events-aggregator, but currently I’m just using a bindable and "settingsChanged(newvalue, oldvalue);"

The reason for all this is because say the new plugin user wants to add more settings and customization. He needs access to the class instance that gets defined INSIDE my-element.js after some seconds.

I think Aurelia Ag-grid plugin is instead using events and binding directly to functions that you are forced to implement. EventInstance.bind=“EventFunctionInYourParentClass” as events or “hooks”.


#7

configure function, which will eventually call .globalResources()

Doesn’t that mean that I can still use feature()? Feature takes one argument: PLATFORM.moduleName(‘item’)… For readability seems better than calling it “globalResources”?

.feature() and .plugin() both take a config function, then it will be call with FrameworkConfiguration instance as its first parameter. Inside that config function, it’s normally called .globalResources() on that like this:

export function configure(config: FrameworkConfiguration) {
  config.globalResources(...pluginOrFeatureResources);
}

So in the end, it usually boils down to calling .globalResources() on FrameworkConfiguration instance to config thing. Using plugin / feature enable a delayed call of that, for when you want to do some extra setup.


#8

Is it still okay to use BindableItemChanged(nv, ov){}

To detect when some ES object has been loaded with data from the CHILD element (my-custom-element)?

Or is it better to use events? I am a bit concerned about events, because I may have to do it based on messageType or class, then parse out the id, and it just becomes a very confusing mess of events if they have tons of my “my-custom-elements” on the page.

I don’t want to use a singleton, because there could be multiples. I can’t imagine using DI, because DI would be like the eventaggregator anyway so why not just use events…

Or is there some other creative way to detect that the child element is ready to go and loaded? Perhaps have a different binding “IsLoaded”?

Basically I don’t want the user to use the “@bindable” instance of data, without it being loaded (and attached in the child element). It just seemed weird to use Changed() as the best way to detect this.

Maybe a class gets imported, @inject, then the user has to invoke/implement certain functions inside their “attached()” to get the plugin to work. And that would be part of the plugin install step. Whether loaded or not, the data is queued to be inserted into the child element.

I’m thinking of these things because I want to make plugins for certain things that are advanced UI components and things that require “loading” step or have graphical visual features that take some time to load.