Create a Element Library

Hello,
I am trying to create an Element Library ( npm package “my-element-library” ) composed of Aurelia Elements. I have tries searching how to create something like this and everything points me to create a plugin, so I started creating a plugin.

I have a model-view at path ./elements/video-player/video-player of the plugin project and this is my index.ts

  import process from 'process';
  window.process = process;
  import { FrameworkConfiguration } from 'aurelia-framework';
  import { PLATFORM } from 'aurelia-pal';

  export function configure(config: FrameworkConfiguration) {
      config.globalResources([
        PLATFORM.moduleName('./elements/video-player/video-player'),
      ]);
  }

  export * from "'./elements/video-player/video-player'";

I build the plugin and installed it in another aurelia project, I try to install the plugin by doing
aurelia.user.plugin(PLATFORM.moduleName('my-element-library'))

now I have two problems, if I do the above it causes an error while building

Can't figure out a normalized module name for ./elements/video-player/video-player, please call PLATFORM.moduleName() somewhere to help.

The second one is I tried to extend the VideoPlayer element inside the project to create new element ExtendedVideoPlayer and it still causes the same error as above.

Am I doing something wrong?

1 Like

I notice a few things:

on your last line, it should be

  export * from "./elements/video-player/video-player";

About the entry configure, you can have a look at another example here https://github.com/aurelia/ux/blob/master/packages/datepicker/src/index.ts . The code in your lib looks pretty much similar so i’m not sure why it’s throwing. Maybe if try elements/video-player/video-player instead (no ./ at the start)

I have removed the export statement on the bottom and removed the ./ from the beginning when registering globalResources, still the error is

`Can’t resolve ‘elements/video-player/video-player’ in ‘…/my-element-library/dist/native-modules’ even when said path resolves to a js file.

But I have noticed on big difference between the plugin you shared and mine, I am using sass instead of css, now when compiling all the sass files are converted to respective css files but in the template files the require statements are not changed from .sass to .css could this be the problem?
`
And also I am using Typescript instead of Javascript

1 Like

Just to test If something is wrong with my configuration, I created a new plugin called test-plugin from cli with Default TypeScript Aurelia Plugin configuration and tried to install that plugin without changing anything in plugin it still causes the same error with hello-world element

ERROR in ../test-plugin/dist/native-modules/index.js
Module not found: Error: Can't resolve 'elements/hello-world' in '.../test-plugin/dist/native-modules'

If I try to install @aurelia-ux/core, @aurelia-ux/components they work fine

I think I found the issue, I was trying to install the element-library as local package only then it causes this error, when i pushed the plugin to github and installed via github then everything started working.

But the sass issue still persists, plugin build converts scss to css but template still refers to scss files.

Edit: Regarding the sass I ended up writing a gulp task to replace .scss with .css in html files

1 Like

I was suspecting so as well. I think the PLATFORM.moduleName() and how it supports folder other than root is a bit difficult at the moment.

1 Like

Now I am facing a different kind of issue

This is the structure of the element at path src/elements/video-player/video-player.ts

@customElement("video-player")
@useView("./video-player.html")
export class VideoPlayer {
...
}

and it’s corresponding video-player.html is

<template>
  <require from="./video-player.css"></require>
  ...
</template>

I can register the plugin and use it if I register it globally, but if I want to extend the element in the main project by importing the element

like this

import { VideoPLayer } from 'my-template-library/dist/commonjs/elements/video-player/video-player';

export class ExtendedVideoPlayer extends VideoPlayer {}

it throws error

ERROR [app-router] 
Error: Unable to find module with ID: resources/elements/extended-video-player/video-player.html

it is searching for the html in the main project instead of plugin project path, basically @useView("./video-player.html") is pointing wrong html template

Now If I change the extended element to something like below

import { VideoPLayer } from 'my-template-library/dist/commonjs/elements/video-player/video-player';

@useView(PLATFORM.moduleName('my-template-library/dist/commonjs/elements/video-player/video-player.html'))
export class ExtendedVideoPlayer extends VideoPlayer {}

It throws an error

ERROR [app-router]
Error: Failed loading required CSS file: my-template-library/dist/commonjs/elements/video-player/video-player.css

even though my-template-library/dist/commonjs/elements/video-player/video-player.css exists

Now this looks like a problem with path resolution of aurelia plugins, and can be seen in this thread too https://discourse.aurelia.io/t/lernajs-with-an-aurelia-application/3353/39

Since I am using JSS to create style my elements partially, I moved my styling completely to JSS removing all scss/css files. For now I don’t think there is any other way with current implementation of Aurelia. But this is a serious limitation for sharing elements between projects.

1 Like

The path referencing the html shouldn’t be poking into the dist folder, generally. Can you do only

@useView(PLATFORM.moduleName('my-template-library/elements/video-player/video-player.html'))
1 Like

Now that worked, my bad for drawing the conclusions about limitation, but i still think sharing elements should be easier.

Thank you for your help

1 Like

I’m glad you got it resolved. And I’ll be more than happy to help with future hiccups. For the take on sharing stuff, it’s one of the gotcha when doing local plugin that is not easy to solve in the webpack plugin. Maybe for local sharing, you can use .feature api instead of .plugin, they are identity, except .feature may work better for local-but-not-inside-root-folder.

1 Like

Cool, I will give feature api a try

1 Like

@bigopon how does DI work with plugins? I have used DI in multiple Elements of plugin and extended one element in Main Project, according to my initial observation Injected class is instantiated twice always and the instance which was created first is overwritten with the new instance and all the data stored in the previous instance is lost.

I have been using Container.instance.getto get an instance of class in the constructor of Elements. I will look if there is some issue with my code and post the results along with the sample code.

1 Like

Most of the time, if you need a container, you can inject Container into the custom element itself. And then you can use that container to get the instances of the classes you want.

Or, you can inject the instance directly instead. It depends on what you want to do and how you organise stuff. Maybe a bit more details in your Q?

The easiest way perhaps is to grab the instance from the aurelia object passed into your plugins configure function. Take a look at the store plugin as an example https://github.com/aurelia/store/blob/master/src/aurelia-store.ts

1 Like

I believe i tried this too, registering an instance manually when plugin is being configured, but the problem still persisted then

Sorry for the late reply but I do think it’s something with my project structuring it’s a bit complex.
The whole Project is divided into two main parts an ProjectA and a ProjectB. The ProjectA and ProjectB share elements so all the shared elements are separated into a Plugin. The ProjectB doesn’t manipulate any parts of the elements it gets from Plugin but on the other hand ProjectA has to inject code and extend the elements it imports from plugin.

Now the elements in plugin are not independent of each other, they import other elements from plugin and share data between sibling elements using Service which is injected inside their constructor,
The service just maintains references to any object which is registered.

The code for reference service is

export class ReferenceService {
  referenceMap: Map<string, any>;
  constructor(){
    this.referenceMap = new Map()
  }

  register(name:string, item:any){
    this.referenceMap.set(name, item)
    return this.get(name)
  }

  get(name){
    return this.referenceMap.get(name)
  }
}

There is a main element in plugin which has multiple child elements from plugin lets call it ElementRoot and let call child elements as ChildElementA, ChildElementB, ChildElementC

All the elements ElementRoot, ChildElementA, ChildElementB, ChildElementC registers themselves in ReferenceService

constructor() {
    super()
    this.referenceService = Container.instance.get(ReferenceService)
}
attached() {
    this.referenceService.register(<element-id>,this)
}

In ProjectA the roo element ElementRoot is extended to create ExtendedElementRoot

@useView('my-element-library/elements/element-root/element-root.html')
export class ExtendedElementRoot extends ElementRoot {
    construtor() {
        super()
        'do stuff'
    }

    attached() {
        super.attached()
        'do stuff'
    }
}

When this is done The child elements get registered properly but when the ExtendedElementRoot tries to register a new instance of ReferenceService is created and only the reference of ExtendedElementRoot is stored everything else is lost.

1 Like
constructor() {
    super()
    this.referenceService = Container.instance.get(ReferenceService)
}

The above could be problematic around Container.instance.get, in case your plugin is used in multiple aurelia application at once.

Better do

@inject(Container)
export class ExtendedElementRoot extends ElementRoot {
  constructor(container: Container) {
    this.referenceService  = container.get(ReferenceService);
  }

And because you are using inheritance, so you will need to do:

@inject(Container)
export class ExtendedElementRoot extends ElementRoot {
  constructor(container: Container) {
    super(container);
    this.referenceService  = container.get(ReferenceService);
  }

For your main Q, using inheritance with custom element/custom attribute comes with a caveat: you need at least 1 bindable decorator on the subclass, otherwise, the metadata is going to fallback to the parent class metadata. Can you try

@inject(Container)
@customElement('...')
export class ExtendedElementRoot extends ElementRoot {
  constructor(container: Container) {
    this.referenceService  = container.get(ReferenceService);
  }

?

I think this might work, I will give it a try today or tomorrow. Thanks!

1 Like