angular: 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

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 30
  • Comments: 65 (20 by maintainers)

Commits related to this issue

Most upvoted comments

Had a chance to look closer at this today. As suspected, it’s a bundling issue. The root cause is that webpack (which drives the CLI) uses a runtime: webpackJsonp global, and you’re overwriting that each time you load another bundle (which also defines webpackJsonp) - See https://github.com/webpack/webpack/issues/3791#issuecomment-270821692. The CLI doesn’t expose this option (things like this are why we are not supporting this use case yet). You could (though I don’t recommend it) manually rename that global webpackJsonp in each bundle to something unique.

You’re also duplicating all the polyfills, which is likely to cause all kinds of unexpected results, as they’re shimming native APIs and overwriting them at various times. Further, bundling a copy of all the angular packages into each bundle seems suboptimal.

For the moment, if you want to do this sort of use case, your likely best option is going to be to use something like rollup to build UMD bundles, that rely on angular’s UMD bundles and exclude the angular source from each element’s package. It’s going to take some manual work.

Alternately, don’t use the CLI to build the individual elements into binaries, treat them as libraries and bring them into the build properly, so you only have one webpack runtime.

Again, we’re working to enable this use case for v7, so I’m going to close this (as its not actually an issue with Angular core)

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

This weekend, I’ve created a builder that makes the CLI to do what Rob suggested above: https://www.npmjs.com/package/ngx-build-plus

Now we are at Angular 7. Something new here?

simple enough if (!customElements.get('element')) customElements.define('element', cardElement);

ngx-builds-plus is the right way to do this for the moment. Once ivy goes out we’ll bring out some new stuff for Elements 😃

Again, we’re working to enable this use case for v7

Not sure how to reconcile these two statements. We’re long past v7, so why do we have to resort to using a separate plugin?

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

@robwormald any ideas?

ngx-builds-plus is the right way to do this for the moment. Once ivy goes out we’ll bring out some new stuff for Elements 😃

On Thu, Apr 18, 2019 at 6:06 AM TCastil notifications@github.com wrote:

For anyone wondering, this issue is closed and the explanation is here: #23732 (comment) https://github.com/angular/angular/issues/23732#issuecomment-388670907

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/angular/angular/issues/23732#issuecomment-484498955, or mute the thread https://github.com/notifications/unsubscribe-auth/AAECTEUQVDJ6Y7MYC3PATL3PRBW35ANCNFSM4E6RAOHA .

@sri1980 i had same error, because module bootstraped twice and customElements.define called twice too. You can not define same element twice. I don’t remember exactly, but one of them was the solution, if not all 😃

  1. use zone fix package (https://www.npmjs.com/package/elements-zone-strategy)
  2. don’t use same name for angular selector and for defined webcomponent name
  3. simple input if-else to definition code, like if customElements.get('comp') then do nothing, else define('comp').

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.

is this fixed in Angular 8?

@gkalpak and @BioPhoton following both of your suggestions, I commented out the polyfills.ts from the script that concats the built files, like so:

const fs = require('fs-extra');
const concat = require('concat');

(async function build(){

  const files = [
    './dist/scripts.js',
    './dist/runtime.js',
    // './dist/polyfills.js',
    './dist/main.js'
  ];

  await fs.ensureDir('elements');

  await concat(files, 'elements/example-angular-elements-component-button.js');

})();

I did this with both of the example components.

I am still getting the same error. The first component’s module is logging to console when the second component is introduced and then breaks.

I will copy and paste my browser’s log and include screen shots below.

Angular is running in the development mode. Call enableProdMode() to enable the production mode.
example-angular-elements-component-box.js:3 ngDoBootstrap example-angular-elements-component-box
example-angular-elements-component-box.js:3 ngDoBootstrap example-angular-elements-component-box
example-angular-elements-component-box.js:1 Uncaught 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.define (http://localhost:4200/assets/elements/example-angular-elements-component-button.js:1:12582)
    at new e (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:3:121311)
    at http://localhost:4200/assets/elements/example-angular-elements-component-box.js:3:74118
    at no (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:3:74335)
    at http://localhost:4200/assets/elements/example-angular-elements-component-box.js:3:84190
    at new e (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:3:84200)
    at Object.Eo [as createNgModuleRef] (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:3:83838)
    at t.create (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:3:115342)
    at http://localhost:4200/assets/elements/example-angular-elements-component-box.js:3:36458
ze.define @ example-angular-elements-component-box.js:1
ze.define @ example-angular-elements-component-button.js:1
e @ example-angular-elements-component-box.js:3
(anonymous) @ example-angular-elements-component-box.js:3
no @ example-angular-elements-component-box.js:3
(anonymous) @ example-angular-elements-component-box.js:3
e @ example-angular-elements-component-box.js:3
Eo @ example-angular-elements-component-box.js:3
t.create @ example-angular-elements-component-box.js:3
(anonymous) @ example-angular-elements-component-box.js:3
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke @ zone.js:388
onInvoke @ example-angular-elements-component-box.js:3
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke @ zone.js:387
push../node_modules/zone.js/dist/zone.js.Zone.run @ zone.js:138
e.run @ example-angular-elements-component-box.js:3
e.bootstrapModuleFactory @ example-angular-elements-component-box.js:3
zUnb @ example-angular-elements-component-box.js:3
p @ example-angular-elements-component-button.js:2
3 @ example-angular-elements-component-box.js:3
p @ example-angular-elements-component-button.js:2
n @ example-angular-elements-component-button.js:2
e @ example-angular-elements-component-button.js:2
(anonymous) @ example-angular-elements-component-button.js:2
(anonymous) @ example-angular-elements-component-button.js:2
example-angular-elements-component-box.js:1 Uncaught TypeError: Illegal constructor
    at t.e.(:4200/anonymous function) (http://localhost:4200/assets/elements/example-angular-elements-component-box.js:1:8552)
    at t [as constructor] (example-angular-elements-component-box.js:3)
    at new t (example-angular-elements-component-box.js:3)
    at HTMLDocument.n.createElement (example-angular-elements-component-box.js:1)
    at HTMLDocument.n.createElement (example-angular-elements-component-button.js:1)
    at app.component.ts:24
    at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:388)
    at Object.onInvoke (core.js:4071)
    at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:387)
    at Zone.push../node_modules/zone.js/dist/zone.js.Zone.run (zone.js:138)
e.(anonymous function) @ example-angular-elements-component-box.js:1
t @ example-angular-elements-component-box.js:3
t @ example-angular-elements-component-box.js:3
n.createElement @ example-angular-elements-component-box.js:1
n.createElement @ example-angular-elements-component-button.js:1
(anonymous) @ app.component.ts:24
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke @ zone.js:388
onInvoke @ core.js:4071
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke @ zone.js:387
push../node_modules/zone.js/dist/zone.js.Zone.run @ zone.js:138
(anonymous) @ zone.js:872
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask @ zone.js:421
onInvokeTask @ core.js:4062
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask @ zone.js:420
push../node_modules/zone.js/dist/zone.js.Zone.runTask @ zone.js:188
drainMicroTaskQueue @ zone.js:595
push../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask @ zone.js:500
invokeTask @ zone.js:1540
globalZoneAwareCallback @ zone.js:1566
  1. I can add multiple box WebComponents without any issue. See how the box’s module logged to the console once, upon introduction into the application. screenshot from 2018-05-09 19-25-26

  2. I then try and load the button WebComponent, breaking the application. You can now see that the box’s module logged to the console, again. screenshot from 2018-05-09 19-25-55

  3. I then try and add another box WebComponent, it is not added and I also get a new error logged in my console (the total log you see in this screenshot was pasted above.) screenshot from 2018-05-09 19-26-06

It’s possible. However, if you compile them separately, it’s not officially supported and you may end up with big bundles. The time after Ivy provides a remedy …

Thanks! Hopefully Ivy will be released soon. We are building micro front end demo by using Angular elements. Being able to support multiple Angular elements from independent projects on the same page is a key thing to us.

@BioPhoton - standalone, built individually and independently from one another.

So, for example, if I work with multiple teams then I could download the WebComponent that they built with their own Angular Elements (they’re own dependencies, versions, build pipeline, etc.) I am interested in consuming their built artifact.

By appending scripts to my document’s head, I should also be able to lazily load these WebComponents. I’m loading two in my example application, the first one works but once the second one is introduced the app breaks - while they are separate files and separate builds, they’re stepping on each other once introduced and consumed.

I have similar problems. zone runs twice…

It’s possible. However, if you compile them separately, it’s not officially supported and you may end up with big bundles. The time after Ivy provides a remedy …

simple enough if (!customElements.get('element')) customElements.define('element', cardElement);

it works!cool~

I have only one angular-element that i want to use inside a regular Angular 7.0.0 app. I’m getting the same error. “ERROR DOMException: Failed to execute ‘define’ on ‘CustomElementRegistry’: this name has already been used with this registry”

If I load the webcomponent js with defer (or load it in body below the angular app js files) its working at first, but after refresh or navigating to a non-angular page and then returning to the angular page, the attributes of the webcomponent dont work, only the tag without its properties.

I’m also using the same webcomponent in an Angular 6 app and AngularJS app and there its working without this bug.

It’s not zone.js related, I had separate zone.js file and now i’m using git://github.com/JiaLiPassion/zone.js#duplicate-patch-dist and getting the same error. and i’m using elements-zone-strategy in the webcomponent.

removing node_modules didnt change anything.

@HashanMWanniarachchi I got multiple angular elements on same page working with ngx-build-plus and without ‘noop’ on ngZone. Here is the sample repo multiple-angular-elements Thanks to @manfredsteyer

@robwormald Could you give more details about the bundling pls?

  • What should be in the bundle and what not?
  • Where is the problem with the cli?

Thanks for your time!

Taking a closer look at the demo, it seems to be trying to use the elements as standalone Angular “mini-apps” (for example each includes BrowserModule and does its own bootstrapping). This is likely what is causing the issue. AFAIK, this is not supported just yet (tagging @robwormald, @andrewseguin to confirm).

What is supported is including the elements from within an Angular app, which loads the custom element module factories and bootstraps them (sharing the main app’s injector among the custom element modules). This is what we currently do in angular.io btw (and here is the loader we use for reference).

Ill try more tonight. I also will publish my setup on npm and have a cli “story” (docs for setup) nearly done.