Solvedangular Using multiple Angular Elements, ngDoBootstrap runs twice, breaking with CustomElementRegistry

I'm submitting a bug report.

Edit: screenshots of this issue are posted below, here

Using multiple Web Components built from Angular Elements breaks the consuming application.

I have the following three repositories:

  1. example-angular-elments-app - the consuming application
  2. example-angular-elments-component-box - outputs the box WebComponent
  3. example-angular-elments-component-button - outputs the button WebComponent

Both components (2 and 3) build the files found in the consuming app's src/assets/elements folder.

You should receive the same error in your console (or something similar, depending on which component you load dynamically first) after following the reproduction steps further below.

example-angular-elements-component-box.js:4 ngDoBootstrap example-angular-elements-component-box
example-angular-elements-component-box.js:4 ngDoBootstrap example-angular-elements-component-box
example-angular-elements-component-box.js:4 ERROR DOMException: Failed to execute 'define' on 'CustomElementRegistry': this name has already been used with this registry
    at ze.define (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:1:12582)
    at ze.value (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:3:33689)
    at ze.define (http://localhost:4200/assets/elements/example-angular-elements-component-button.js:1:12582)
    at ze.value (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:3:33689)
    at e.ngDoBootstrap (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:4:121355)
    at e._moduleDoBootstrap (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:4:37688)
    at http://localhost:4200/assets/elements/example-angular-elements-component-box.js:4:36842
    at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (http://localhost:4200/polyfills.js:2704:26)
    at Object.onInvoke (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:4:30158)
    at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (http://localhost:4200/polyfills.js:2703:32)

After including the second Angular Element WebComponent, you'll see a line like example-angular-elements-component-box.js:4 ngDoBootstrap example-angular-elements-component-box logged twice, illustrating that component bootstrapping twice, likely causing the CustomElementRegistry error.

Expected behavior

The consuming application does not break when consuming multiple WebComponents built from Angular Elements.
The ngDoBoostrap method is not called twice, or at least checks for component registration.

Minimal reproduction of the problem with instructions

  1. Clone the example-angular-elments-app Angular 6 project.
  2. Run npm i
  3. Run npm start
  4. Open http://localhost:4200 and open the developer console.
  5. Click on one of the buttons to load a component.
  6. Click on the other button to load the other component.
  7. View the console and see how the component that was loaded first runs its' ngDoBootstrap method again, before breaking the page.

Environment

[xxx example-angular-elments-app]$ npm run ng -- -v

> example-angular-elments-app@0.0.0 ng /home/xxx/WebstormProjects/example-angular-elments-app
> ng "-v"


     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 6.0.0
Node: 8.11.1
OS: linux x64
Angular: 6.0.0
... animations, cdk, cli, common, compiler, compiler-cli, core
... forms, http, language-service, material, platform-browser
... platform-browser-dynamic, router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.6.0
@angular-devkit/build-angular     0.6.0
@angular-devkit/build-optimizer   0.6.0
@angular-devkit/core              0.6.0
@angular-devkit/schematics        0.6.0
@ngtools/webpack                  6.0.0
@schematics/angular               0.6.0
@schematics/update                0.6.0
rxjs                              6.1.0
typescript                        2.7.2
webpack                           4.6.0

Browser:
- [x] Chromium Web Browser (desktop) Version 65.0.3325.181 (Developer Build) Fedora Project (64-bit)
 
For Tooling issues:
- Node version: 5.6.0
- NPM version: 5.6.0
- Platform: Linux
41 Answers

✔️Accepted Answer

I created a detailed step by step descriptions to setup multiple projects and elements.

Unfortunately, this will just help to reproduce the bug, not it's not a fix, but easily adaptable if we find a solution,

Here is the all in one repo:
ng-elements-poc

@gkalpak as you can see in the attached description the polyfills are not included:

Angular Elements Build Setup

The dev-kit has a package available for building web components with angular.
You can use the @angular/elements package for this.
Here you can follow a step by step setup for angular elements running standalone or in another angular app.

Setup a new project

  1. Create a new project. Run ng new ng-elements-poc in the console.

  2. Switch into you new directory: cd ng-elements-poc

  3. You should now be able to test it by running ng serve --open in the console.

Setup WebComponents

  1. Run ng add @angular/elements in the console.
    The cli will install some packages to your package.json:
// package.json

{
[...] 
  dependencies: {
    [...]
    "@angular/elements": "^6.0.0",
    "document-register-element": "^1.7.2"
  }
[...]
}

And add a script to your projects scripts config in angular.json:

// angular.json

{
[...]
  "projects": {
    "ng-elements-poc": {
    [...]
      "scripts": [
        {
          "input": "node_modules/document-register-element/build/document-register-element.js"
        }
      ],
      [...]
    },
    [...]
  },
  [...]
}
  1. Test the if everything is still working: ng serve

Setup application for standalone web component

  1. Generate a new project in which we can test an elements setup.
    Run ng generate application my-first-element in the console.

  2. Copy the script in your angular.json (mentioned in step Setup WebComponents:1.) from project ng-elements-poc to my-first-element scripts:

// angular.json

{
[...]
  "projects": {
    [...]
    "my-first-element": {
    [...]
      "scripts": [
        {
          "input": "node_modules/document-register-element/build/document-register-element.js"
        }
      ],
      [...]
    },
    [...]
  },
  [...]
}
  1. Test it: ng serve --project my-first-element

  2. Setup a script in your package.json to start the my-first-element application:

// package.json

{
  [...] 
  scripts: {
  [...]
   "first-element:start": "ng serve --project my-first-element",
  },
  [...]
}
  1. Test it by running npm run first-element:start.

  2. Now lets create a build task that we can use later on to generate the bundled web component file.
    Setup a script in your package.json to build the application.
    Note that we set the flag output-hashing to none to have the bundles always with the same file names.

// package.json

{
  [...] 
  scripts: {
  [...]
   "first-element:build": "ng build --prod --project my-first-element --output-hashing=none"
  },
  [...]
}
  1. Run the command and check the file names in the dist folder.

  2. You can also test the bundles directly. Therefore lets another package:

Install the http-server globally:
npm install http-server -g

Now we can run http-server .\dist\my-first-element\ in our root folder.
As stated in the console we can now access the serve file under 127.0.0.1:8080.

Starting up http-server, serving .\dist\my-first-element\
Available on:
  http://192.168.43.58:8080
  http://127.0.0.1:8080

Create component and bootstrapping

We have setup a new project to test standalone web components. Now lets create one.

  1. Create a component called first-element and set viewEncapsulation to Native:
    ng generate component first-element --project my-first-element --spec=false --viewEncapsulation=Native

  2. Remove AppComponent from you project.

  • delete app.component.ts, app.component.html, app.component.css, app.component.spec.ts
  • remove all references in app.module.ts
  1. In app.module.ts remove the empty settings and add FirstElementComponent to the entryComponents
// projects/my-first-element/src/app/app.module.ts

import { FirstElementComponent } from './first-element/first-element.component';

@NgModule({
  declarations: [FirstElementComponent],
  imports: [BrowserModule],
  // providers: [],
  // bootstrap: [],
  entryComponents: [FirstElementComponent]
})
  1. Implement the bootstrapping logic for your component.
// projects/my-first-element/src/app/app.module.ts

import {Injector, [...]} from '@angular/core';
import {createCustomElement, NgElementConfig} from '@angular/elements';

@NgModule({
[...] 
})
export class AppModule {
  constructor(private injector: Injector) {

  }

  ngDoBootstrap() {
    const config: NgElementConfig = {injector: this.injector};
    const ngElement = createCustomElement(FirstElementComponent, config);

    customElements.define('app-first-element', ngElement);
  }

}
  1. In your index.html replace <app-root></app-root> with <app-first-element></app-first-element>:
<!-- projects/my-first-element/src/index.html --> 

[...]
<body>
  <!-- vvv REMOVE vvv
  <app-root></app-root>
  vvv ADD vvv -->
  <app-first-element></app-first-element>
</body>
</html>
  1. Test your web component.
    Run npm run first-element:start

  2. You can also setup a new script in package.json to bundle the files to use your web component in another place.
    Let's introduce the bundle-standalone script.

// package.json

{
  [...]
  "first-element:bundle-standalone": "cat dist/my-first-element/{runtime,polyfills,scripts,main}.js > dist/my-first-element/my-first-element-standalone.js",
}
  1. Run npm run first-element:bundle-standalone in the console to test it.

Test web component in another angular app

  1. Setup new script in package.json to bundle the files for another angular project
// package.json

{
  [...]
  "first-element:bundle-ng": "cat dist/my-first-element/{runtime,main}.js > dist/my-first-element/my-first-element-ng.js",
}
  1. Run npm run first-element:bundle-ng in the console to test it.

  2. Copy dist/my-first-element/my-first-element-ng.js into
    src/assets/ng-elements to serve this file as an asset of your root project.

  3. In your root application ng-elements-poc open app.module.ts

Add the following to your ngModule decorator:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})

And insert following code to AppModule constructor

export class AppModule {

  constructor() {
    const scriptTag = document
          .createElement(`script`);
        scriptTag.setAttribute('src', 'assets/elements/my-first-element-ng.js');
        scriptTag.setAttribute('type', 'text/javascript');

    document.body.appendChild(scriptTag);
  }

}
  1. Add the html tag into your app.component.html
<!-- src/app/app.component.html -->

[...]
<app-first-element></app-first-element>

Using multiple element bundles in one app

Test test if we can use multiple elements we can test a multiple elements in the same bundle and b multiple elements in different bundles.

Let's start with b multiple elements in a different bundle.

  1. Create a new project name my-other-element. Do this by following the steps from Setup application for standalone web component and Create component and bootstrapping

  2. Create npm scripts for copying the files over into src/assets/elements

// package.json

{
  [...]
  "first-element:copy-bundle": "cat dist/my-first-element/my-first-element-ng.js > src/assets/ng-elements/my-first-element-ng.js",
  "other-element:copy-bundle": "cat dist/my-other-element/my-other-element-ng.js > src/assets/ng-elements/my-other-element-ng.js",
  "copy-bundles": "npm run first-element:copy-bundle && npm run other-element:copy-bundle"
}
  1. In your root application ng-elements-poc open app.module.ts

    Refactor the creation of the script into a separate function:

    export class AppModule {
    
      constructor() {
        const bundles = ['my-first-element-ng', 'my-other-element-ng'];
        
        bundles
         .forEach(name => document.body.appendChild(this.getScriptTag(name)));
       
      }
      
      getScriptTag(fileName: string): HTMLElement {
         const scriptTag = document
           .createElement(`script`);
     
         scriptTag.setAttribute('src', `assets/ng-elements/${fileName}.js`);
         scriptTag.setAttribute('type', 'text/javascript');
     
         return scriptTag;
      }
       
    }
  2. Add the html tag into your app.component.html

<!-- src/app/app.component.html -->

[...]
<app-other-element></app-other-element>
  1. Test it. Run following commands:
npm run first-element:build
npm run first-element:bundle-ng
npm run other-element:build
npm run other-element:bundle-ng
npm run copy-bundles

Other Answers:

Now we are at Angular 7. Something new here?

@robwormald any ideas?

@robwormald why this issue is closed? I didn't find any solutions how to solve this problem.

This is very important to fix, I believe, especially considering the fact that once Angular Element WebComponents are built and published for everyone else to use (Wordpress, Polymer, etc.) they all look to be breaking one another in what appears to be friendly fire.

More Issues: