1

I have a full Angular 16 application, containing routing, multiple components and services and I need to convert it into a single Webcomponent, that can be loaded via the script tag in an external UI application/shell.

So far, all the tutorials and resources I have come across demonstrate transforming just a single component Angular app into a Webcomponent using Angular Elements, such as this example -

import { Injector, NgModule } from '@angular/core';
import  { createCustomElement } from '@angular/elements';
import {HttpClientModule} from "@angular/common/http";
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing. Module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home. Component';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent, HomeComponent]
})
export class AppModule {

  constructor(private injector: Injector) {
    const el = createCustomElement(HomeComponent, {injector: this. Injector});
    customElements.define('element-home', el);
  }

  ngDoBootstrap() {}
 }

Here, I have a single component in the Angular app, called HomeComponent, from which I can create a Webcomponent titled <element-home>. This can be loaded, as an example, into an HTML page this way -

<body>
  <element-home></element-home>
  <script src="...." /> // bundled Angular javascript files
</body>

But, how do I transform a complete Angular app (with routing) into a Webcomponent? My angular component structure looks like this -

- AppComponent
  <router-outlet> 
  |
   - HomeComponent // for /home route
   - DetailComponent // for /detail route
   - DetailService

Additionally, any tutorials/resources/guides on this topic would be appreciated!

2
  • It is quite easy. But the question is: what version of Angular do you use? Commented Feb 15, 2024 at 18:34
  • @NgDaddy I use Angular 16 Commented Feb 15, 2024 at 19:31

2 Answers 2

1

So if you use Angular 16 you can utilize standalone API (see the stackblitz link at the bottom):

(WARNING: the app should not contain any lazy loaded parts)

When you build that app in the repo you will get three js files:

  1. runtime.js
  2. polyfills.js
  3. main.js

You have to concat them into one file keeping the order above. This way you will get one js file with your new webcomponent.

import { Component, OnInit, ViewEncapsulation, inject } from '@angular/core';
import { provideRouter, Router, RouterOutlet } from '@angular/router';
import { createCustomElement } from '@angular/elements';
import { createApplication } from '@angular/platform-browser';
import 'zone.js';

@Component({
  selector: 'home-component',
  standalone: true,
  template: `
    <h2 class="my-class">Hello from {{ name }}!</h2>
  `,
  styles: `
    .my-class {
      color: green;
    }
  `,
  encapsulation: ViewEncapsulation.ShadowDom,
})
export class HomeComponent {
  name = 'HomeComponent';
}

@Component({
  selector: 'app-component',
  standalone: true,
  template: `
    <h1 class="my-class">Hello from {{ name }}!</h1>
    <router-outlet></router-outlet>
  `,
  styles: `
    .my-class {
      color: red;
    }
  `,
  encapsulation: ViewEncapsulation.ShadowDom,
  imports: [RouterOutlet],
})
export class AppComponent implements OnInit {
  name = 'AppComponent';

  private router = inject(Router);

  ngOnInit() {
    this.router.initialNavigation();
  }
}

async function webComponentApp() {
  const app = await createApplication({
    providers: [
      provideRouter([
        { path: '', component: HomeComponent, pathMatch: 'full' },
      ]),
    ],
  });
  const element = createCustomElement(AppComponent, {
    injector: app.injector,
  });
  customElements.define('my-app-webcomponent', element);
}

(async function () {
  await webComponentApp();
})();

Stackblitz example

Sign up to request clarification or add additional context in comments.

14 Comments

Thank You for the example, but I'm having a hard time replicating your example for my project which uses @NgModules, not standalone components. It looks like your main bootstrapping code is in the webComponentApp() function, which lives in main.ts which also contains all other components. How would this translate to the older style? Where should I place the webComponentApp() function? If I put it in app.component.ts, it throws an error - core.mjs:28282 Uncaught Error: NG0400: A platform with a different configuration has been created. Please destroy it first.
It looks like this is because main.ts already seems to be bootstrapping the Angular app through AppModule - // platformBrowserDynamic().bootstrapModule(AppModule) // .catch(err => console. Error(err));... not sure how to adjust this based on your changes
I migrated my app to using standalone components only (no NgModules), and now it works on port 4200 (similar to your stackblitz example), but if you try to replicate it in a new index.html file and add the bundled 3 generated JS files, it doesn't show anything. Maybe you can give this a shot too..
Hello! So this example won't work with a NgModules implementation? Thanks in advance)
Have you added the newly created webcomponent tag to the new index.html file?
Structuring does not matter. In the stackblitz example I keep all in one file just for a simplification sake. Just to make it easier for you to get it I've splitted it into files stackblitz.com/edit/stackblitz-starters-qrjdcs. See how main.ts looks now and try to do the same. Do not run bootstrapApplication, run just createApplication as it is in the main.ts. Let me know if it works.
|
1

Complementary the NgDaddy answer, some advertisements:

I think remember read in some where that the "scripts" should be inserted at the end of the "body" -after the tag of your app-.

  <body>
    <my-app>
      loadding...
    </my-app>

    <script src="...."></script>
    <script src="...."></script>
    <script src="...."></script>
  </body>

To use with modules you should write

export class AppModule {
  constructor(private injector:Injector) {}
  ngDoBootstrap() {
    customElements.define(
      'my-app',
      createCustomElement(AppComponent, {injector: this.injector})
    )
  }
 }

You can use "concat" to join the three bundles.For this.

  1. Add the concat

    npm i concat
    
  2. Create a file concat.js in the way

    var concat = require('concat');
    concat(['./dist/runtime.js',
            './dist/polyfills.js',
            './dist/main.js'],'./dist/my-app.js');
    
  3. And in package.json, before the build write

    //add this line
    "build-element": "ng build --prod=true --localize --output-hashing=none &&
                       node ./concat.js",
    
    //before
    "build":.....
    

When you use routing, it's possible you need use HashLocationStrategy

{ provide: LocationStrategy, useClass: HashLocationStrategy }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.