141
votes

Update 5/24/2018: We are now +3 versions of Angular from my original post and still don't have a final workable solution. Lars Meijdam (@LarsMeijdam) has come up with an interesting approach which is certainly worth a look-see. (Due to proprietary issues, he had to temporarily remove the GitHub repository where he had originally posted his sample. However, you may message him directly if you would like a copy. Please see the comments below for more info.)

Recent architectural changes in Angular 6 do bring us closer to a solution. Additionally, Angular Elements (https://angular.io/guide/elements) provides some component functionality--though not quite what I originally described in this post.

If anyone from the amazing Angular team happens to come across this, please note that there seem to be many other people who are also very interested in this functionality. It might well be worth considering for the backlog.


I would like to implement a pluggable (plug-in) framework in an Angular 2, Angular 4, Angular 5, or Angular 6 application.

(My specific use case for developing this pluggable framework is that I need to develop a miniature content management system. For a number of reasons not necessarily elaborated here, Angular 2/4/5/6 is a near perfect fit for most of the needs of that system.)

By pluggable framework (or plug-in architecture), I specifically mean a system which allows third party developers to create or extend the functionality of a primary application through the use of pluggable components without having direct access to or knowledge of the primary application's source code or inner workings.

(That phrasing about "without having direct access to or knowledge of the application's source code or inner workings" is a core objective.)

Examples of pluggable frameworks include common content management systems like WordPress or Drupal.

The ideal situation (as with Drupal) would be to simple be able to place these pluggable components (or plug-ins) into a folder, have the application auto-detect or discover them, and have them just magically "work." Having this occur in some sort of hot-pluggable manner, meaning while the app was running, would be optimum.

I am currently trying to determine answers (with your help) to the following five questions.

  1. Practicality: Is a plugin framework for an Angular 2/4/5/6 application even practical? (Until now, I have not found any practical way to create a truly pluggable framework with Angular2/4/5/6.)
  2. Expected Challenges: What challenges might one encounter in implementing a plugin framework for an Angular 2/4/5/6 application?
  3. Implementation Strategies: What specific techniques or strategies could be employed for implementing a plugin framework for an Angular 2/4/5/6 application?
  4. Best Practices: What are the best practices for implementing a plugin system for an Angular 2/4/5/6 application?
  5. Alternative Technologies: If a plugin framework is not practical in an Angular 2/4/5/6 application, what relatively equivalent technologies (e.g. React) might be suitable for a modern highly reactive Web application?

In general, use of Angular 2/4/5/6 is very desirable because:

  • it is naturally extremely fast--blazingly so.
  • it consumes very little bandwidth (after the initial load)
  • it has a relatively small footprint (after AOT and tree shaking)--and that footprint continues to shrink
  • it is highly functional, and the Angular team and community are continuing rapid growth of its ecosystem
  • it plays well with many of the best and latest Web technologies such as TypeScript and Observables
  • Angular 5 now supports service workers (https://medium.com/@webmaxru/a-new-angular-service-worker-creating-automatic-progressive-web-apps-part-1-theory-37d7d7647cc7)
  • being backed by Google, it is likely to be supported and enhanced well into the future

I would very much like to use Angular 2/4/5/6 for my current project. If I am able to use Angular 2/4/5/6, I will also be using Angular-CLI and probably Angular Universal (for server-side rendering.)

Here are my thoughts, so far, regarding the questions above. Please review and provide your feedback and enlightenment.

  • Angular 2/4/5/6 apps consume packages--but this is not necessarily the same as allowing plugins within an application. A plugin in other systems (e.g. Drupal) can be essentially added by dropping the plugin folder into a common modules directory where it is automatically "picked up" by the system. In Angular 2/4/5/6, a package (as a plugin might be) is usually installed via npm, added to the package.json, and then manually imported into the app--as in app.module. This is much more complicated than the Drupal method of dropping a folder and having the system automatically detect the package. The more complicated it is to install a plugin, the less likely people will be to use them. It would be much better if there was a way for Angular 2/4/5/6 to automatically detect and install plugins. I am very interested to find a method which allows non-developers to install the Angular 2/4/5/6 application and install any chosen plugins without having to understand all of the application's architecture.

  • Generally, one of the benefits of providing a pluggable architecture, is that it is very easy for 3rd party developers to extend the functionality of the system. Obviously, these developers will not be familiar with all of the intricacies of the code for the application they are plugging into. Once the plugins are developed, other even less technical users may simply install the application and any selected plugins. However, Angular 2/4/5/6 is relatively complicated and has a very lengthy learning curve. To further complicate things, most production Angular 2/4/5/6 applications also utilize Angular-CLI, Angular Universal, and WebPack. Someone who is implementing a plugin would probably have to have at least some basic knowledge of how all of these fit together--along with a strong working knowledge of TypeScript and a reasonable familiarity with NodeJS. Are the knowledge requirements so extreme that no third party would ever want to develop a plugin?

  • Most plugins will likely have some server side component (e.g. for storing/retrieving plugin related data) as well as some client-side output. Angular 2/4/5 specifically (and strongly) discourages developers from injecting their own templates at runtime--as this poses a serious security risk. In order to handle many types of output that a plugin may accommodate (e.g. display of a graph), it appears that allowing users to create content which is injected into the response stream, in one form another, is probably necessary. I wonder how it might be possible to accommodate this need without figuratively shredding Angular 2/4/5/6's security mechanisms.

  • Most production Angular 2/4/5/6 applications are pre-compiled using Ahead of Time (AOT) compilation. (Probably all should be.) I am uncertain how plugins might be added to (or integrated with) pre-compiled applications. The best scenario would involve compiling the plugins separately from the main application. However, I am uncertain how to make this work. A fallback might be to re-compile the entire application with any included plugins but that complicates things a bit for an administrative user who simply wants to install the application (on his own server) along with any selected plugins.

  • In an Angular 2/4/5/6 application, especially a pre-compiled one, a single piece of errant or conflicting code can break the entire application. Angular 2/4/5/6 applications are not always the easiest to debug. Application of ill-behaved plugins could result in very unpleasant experiences. I am currently unaware of a mechanism to gracefully handle ill-behaved plugins.

12
In my oppinion, an angular 2 module is a plugin. @angular/router, @angular/forms, @angular/http, @angular/material, these are 'plugins' from angular, we can check out how they make 'plugins'.Timathon
@Timathon, unfortunately, they are not the same. Plugin systems allow an application to be extended without modification of the core application code. Use of @angular/router, @angular/forms, etc. require the user to modify the application. Those are really libraries as opposed to plugins. I am really more interested in allowing non-developer administrative users to select and use the plugins which are most interesting to them without having to be aware of the internal details of the application.Anthony Gatlin
Did you get anywhere with this? I'm interested in trying something similar. The way Angular 2 is build (around Modules) I thought a plugin type architecture would fit it really well but it doesn't seem to any examples etc.Joe
@Joe, I still do not have a good solution for this problem. I thought the same as you.Anthony Gatlin
I created a repository on github with a solution which might help. It uses Angular 6 libraries and 1 base applications which load up the UMD bundled libraries lazily; github.com/lmeijdam/angular-umd-dynamic-example If you have any suggestions, please feel free to add!Lars Meijdam

12 Answers

23
votes

Update

For Angular 11 I strongly recommend you to take a look at implementation with Webpack 5 Module Federation

🎉 https://github.com/alexzuza/angular-plugin-architecture-with-module-federation

Previos version

🛠️ Github demo angular-plugin-architecture

Maybe Ivy can change something but for the time being I use the solution that uses Angular CLI Custom Builder and meets the following requirements:

  • AOT
  • avoid duplicate code(packages like @angular/core{common,forms,router},rxjs,tslib)
  • use shared library in all plugins but DO NOT SHIP generated factories from that shared library in each plugin but rather reuse library code and factories
  • the same level of optimization that Angular CLI gives us
  • for importing the external modules we just need to know only one thing: their bundle file path
  • our code should recognize module and place plugin into the page
  • support server-side rendering
  • load module only when needed

The usage is simple as:

ng build --project plugins --prod --modulePath=./plugin1/plugin1.module#Plugin1Module 
         --pluginName=plugin1 --sharedLibs=shared --outputPath=./src/assets/plugins

More on this in my article:

17
votes

I created a repository on github with a solution which might help. It uses Angular 6 libraries and 1 base applications which load up the UMD bundled libraries lazily; https://github.com/lmeijdam/angular-umd-dynamic-example

If you have any suggestions, please feel free to add!

12
votes

I have just published a new chapter for my book "Developing with Angular" that addresses the topic of plugins in Angular 2+ and should be of a great interest to people that are trying to build external plugins.

Key points:

  • Plugins
  • Building components based on string names
  • Loading configuration from external sources
  • Dynamically changing application routes
  • External plugins
  • Creating plugin libraries
  • Loading plugins into the application
  • Dynamic routes with plugin content

The book is free to get, and has "pay what you want" model. Feel free to grab a copy and hope that helps.

7
votes

Example application with a working plugin system (thanks to Gijs for founding the github repo!) https://github.com/PacktPublishing/Mastering-Angular-2-Components/tree/master/angular-2-components-chapter-10 based on the eBook Mastering Angular 2 Components

  • plugin architecture to extend core app components
  • file plugin system (for simply adding plugin directories/files without editing any core config files or the need to recompile your application!)
  • load and dynamically use plugins
  • building a rudimentary plugin manager to activate/deactive plugins on-the-fly

Cheers, Niklas

5
votes

What you're looking for is lazy module loading. Here is an example of it: http://plnkr.co/edit/FDaiDvklexT68BTaNqvE?p=preview

import {Component} from '@angular/core';
import {Router} from '@angular/router';

@Component({
  selector: 'my-app',
  template: `
    <a [routerLink]="['/']">Home</a> | 
    <a [routerLink]="['/app/home']">App Home</a> |
    <a [routerLink]="['/app/lazy']">App Lazy</a>

    <hr>
    <button (click)="addRoutes()">Add Routes</button>

    <hr>
    <router-outlet></router-outlet>
  `
})
export class App {
  loaded: boolean = false;
  constructor(private router: Router) {}

  addRoutes() {
    let routerConfig = this.router.config;

    if (!this.loaded) {
      routerConfig[1].children.push({
        path: `lazy`,
        loadChildren: 'app/lazy.module#LazyModule'
      });

      this.router.resetConfig(routerConfig);
      this.loaded = true;
    }
  }
}

Best...Tom

2
votes

I made a hack for load and compile other modules in bootstrap time, but i haven't solve the problem of cyclic dependencies

 const moduleFile: any = require(`./${app}/${app}.module`),
                    module = moduleFile[Object.keys(moduleFile)[0]];

 route.children.push({
     path: app,
     loadChildren: (): Promise<any> => module
 });
 promises.push(this.compiler.compileModuleAndAllComponentsAsync(module));

then in AppModule add this:

{
        provide: APP_INITIALIZER,
        useFactory: AppsLoaderFactory,
        deps: [AppsLoader],
        multi: true
},
2
votes

I was looking for a plugin system in angular 2/4 too for developing a RAD environment for an enterprise application at work. After some research, I decided to implement a collection of database-stored (but could be in the filesystem) pseudo-Angular components.

The components stored in the database database are based on ng-dynamic and the main component implementation is similar to this:

declare var ctx: any;

@Component({
    selector: 'my-template',
    template: `
<div>
    <div *dynamicComponent="template; context: { ctx: ctx };"></div>
</div>
  `,
    providers: [EmitterService],

})

export class MyTemplateComponent implements OnMount, AfterViewInit, OnChanges {


    // name
    private _name: string;
    get name(): string {
        return this._name;
    }
    @Input()
    set name(name: string) {
        this._name = name;        
        this.initTemplate();
    }

    template: string;
    ctx: any = null;

    private initTemplate() {

        this.templateSvc.getTemplate(this.name).subscribe(res => {
            // Load external JS with ctx implementation
            let promise1 = injectScript(res.pathJs);
            // Load external CCS
            let promise2 = injectScript(res.pathCss);

            Promise.all([promise1, promise2]).then(() => {

                // assign external component code
                this.ctx = ctx; //

                // sets the template
                this.template = res.template;

                this.injectServices();

                if (this.ctx && this.ctx.onInit) {
                    this.ctx.onInit();
                }

            });

        });

    }

The external javascript code is similar to angular components:

var ctx = {

// injected    
_httpService: {},
_emitterService: null,

// properies
model: {
    "title": "hello world!",
},


// events
onInit() {
    console.log('onInit');
},

onDestroy() {
    console.log('onDestroy');
},

onChanges(changes) {
    console.log('changes', changes);
},

customFunction1() {
    console.log('customFunction1');
},

childTemplateName: string = 'other-component'; 

};

And the templates of the components are like angular templates:

<a (click)="customFunction1()">{{ ctx.model.title }}</a>
<input [(ngModel)]="ctx.model.title" type="text" />

And could be nested too:

<a (click)="customFunction1()">{{ ctx.model.title }}</a>
<my-template [name]="childTemplateName"></my-template>

Although it's not perfect, the developers of the custom components have a similar framework than in angular2/4.

2
votes

It can be done, "manually". Since webpack do not know anything about external(plug-ins) module, he cannot include them in bundle(s). So what I did, is to look at the code generated by webpack and I found this pies of code in main.bundle.js:

var map = {
"./dashboard/dashboard.module": ["../../../../../src/app/dashboard/dashboard.module.ts","dashboard.module"]}; 

Lets examine what that array contains:

  1. "./dashboard/dashboard.module" - this is routing URL of the module witch we want to lazy load for example :{ path: 'dashboard', loadChildren: './dashboard/dashboard.module#DashboardModule' }
  2. "../../../../../src/app/dashboard/dashboard.module.ts" - this is entry point(contructor) takes from
  3. "dashboard.module" - actual file name without chunk.js(for example: dashboard.module.chunk.js)

So in theory, if you add entry to the map property configure your routing and follow the pattern, you can have a plug-in system. Now the challenge is how to add or remove entries from that map property. Obviously it cannot be done from angular code, it should be done for external tool.

2
votes

I tried to implement a plugin architecture making use of ABP, Angular and ASP.NET Core: https://github.com/chanjunweimy/abp_plugin_with_ui

Basically, I developed angular plugins using different angular application, then I dynamically add them together.

More Information on how I achieve it:

I have 2 angular-cli application, 1 is the main angular cli application, and another is the plugin angular cli application. The problem we are facing in Angular-cli plugin architecture approach is how we integrate them.

Right now, what I did was, I run ng-build on both of the applications, and put them into a "wwwroot" folder, which then hosted in a ASP.NET core 2.0 server. A simpler repository that shows this idea is Angular Multiple App: https://github.com/chanjunweimy/angular-multiple-app

abp_plugin_with_ui is a repository which works on developing a plugin which contains both the backend and Angular cli. For the backend, I made use of the aspnetboilerplate framework, which the frontend is developed using multiple angular-cli application.

To have the main application integrated with the plugin application, we have to run "ng-build" on both of the application (note that we have to change to href of the plugin application as well), then we move the built contents of plugin angular cli application, to the main application "wwwroot" folder. After achieving all this, we can then run "dotnet run" to serve the ASP.NET Core 2.0 Web Application to host the static files generated by "ng build".

Hopefully it helps. Any comments is welcome! ^^

2
votes

A little off topic, but UI Component libraries could be of interest for some of the readers, who search for plugins:
https://medium.com/@nikolasleblanc/building-an-angular-4-component-library-with-the-angular-cli-and-ng-packagr-53b2ade0701e

NativeScript has build-in UI Plugins:
https://docs.nativescript.org/plugins/building-plugins
Those plugins needs an Angular Wrapper:
https://docs.nativescript.org/plugins/angular-plugin

2
votes

i'm currently in the same quest as you are, trying to make a Pluggable/Themable Version of Angular, and it's not a trivial problem.

I Actually Found Pretty Good Solutions, reading the book Developing with Angular by the Genius Denys Vuyika, he actually on the book explain a pretty good solution, he talks about External plugins on the page 356 of the book and Uses Rollup.js to achieve the solution, he then process to dynamically load external plugins that have been previously built outside of your application.

There is also two other libraries/projects that help you achieve this result ng-packagr and Nx extensions for Agnular (from Nrwl) we are tying to implement the latter, and i'd say it's not as smooth as we anticipated, angular was simple not built for that, so we have to work around some of the the core on how Angular, and the NX ppls are one of the best on it.

We are only at the beginning of our Open Source Project, we are using Django+Mongo+Angular, (We are calling WebDjangular and one of our possible approaches to this answer, is that Django will have to write some JSON configuration files and build the application every time a new plugin or theme is installed and activated.

What we already accomplished is, from the database we can use tags for the components like on the plugin, and the component will be printed on the screen! Again the project is in very early stages, we are basing our architecture a little bit on Wordpress, and we have a lot of more tests to do to achieve our dream :D

I Hope the Book can help you, and using Rollup.js i know you will be able to crack this non trivial problem.

0
votes

I found a good article from Paul Ionescu about how to build a plugin extensible application in angular.

https://itnext.io/how-to-build-a-plugin-extensible-application-architecture-in-angular5-736890278f3f

He also references an example application at github: https://github.com/ionepaul/angular-plugin-architecture