3

I have created an Angular Elements web component to be used as a micro-frontend in an existing Vue application. The project requires the custom web component be in Angular and the main application be in Vue. That is outside of my control.

The Angular web component loads correctly and initializes in the Vue app (I see working life cycle hooks, API calls, and the web component UI loads on the screen), but I am getting a zone.js error TypeError: emitter.pipe is not a function from the polyfill.js file. If I remove the polyfill.js file from the Vue app index.html I get an error from the Angular web component that says Error: NG0908: In this configuration Angular requires Zone.js.

The zone.js error is breaking reactivity in the Angular web component and causing the app to get stuck on the first page. It is not responding to updates in Observables, even though they are correctly logging updated values.

However, if I load the Angular component in an Angular app everything works just fine.

This is my Angular main.ts file:

/// <reference types="@angular/localize" />

import { ApplicationRef } from "@angular/core";
import { createCustomElement } from "@angular/elements";
import {
  bootstrapApplication,
  createApplication,
} from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
import { appConfig } from "./app/app.config";
import { MainComponent } from "./app/main/main.component";
import { environment } from "./environments/environment";

if (environment.buildEmbedded) {
  console.log("Building web component");
  (async () => {
    const app: ApplicationRef = await createApplication(appConfig);

    // Define Web Components
    const embeddedComponent = createCustomElement(MainComponent, {
      injector: app.injector,
    });
    customElements.define("my-custom-component", embeddedComponent);
  })();
} else {
  console.log("Building web app");
  bootstrapApplication(AppComponent, appConfig).catch((err) =>
    console.error(err),
  );
}

and here is my angular.json file with the build config:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "embedded-ui": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss"
        }
      },
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:application",
          "options": {
            "outputPath": "dist/embedded-ui",
            "index": "src/index.html",
            "browser": "src/main.ts",
            "polyfills": ["zone.js", "@angular/localize/init"],
            "tsConfig": "tsconfig.app.json",
            "inlineStyleLanguage": "scss",
            "assets": [
              {
                "glob": "**/*",
                "input": "public"
              }
            ],
            "styles": [
              "src/styles.scss",
              "node_modules/@ctrl/ngx-emoji-mart/picker.css"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "500kB",
                  "maximumError": "2MB"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kB",
                  "maximumError": "4kB"
                }
              ],
              "outputHashing": "all"
            },
            "development": {
              "optimization": false,
              "extractLicenses": false,
              "sourceMap": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.development.ts"
                }
              ]
            },
            "emulator": {
              "optimization": false,
              "extractLicenses": false,
              "sourceMap": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.emulator.ts"
                }
              ]
            },
            "embedded": {
              "optimization": false,
              "extractLicenses": false,
              "sourceMap": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.embedded.ts"
                }
              ]
            }
          },
          "defaultConfiguration": "production"
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "configurations": {
            "production": {
              "buildTarget": "embedded-ui:build:production"
            },
            "development": {
              "buildTarget": "embedded-ui:build:development"
            },
            "emulator": {
              "buildTarget": "embedded-ui:build:emulator"
            },
            "embedded": {
              "buildTarget": "embedded-ui:build:embedded"
            }
          },
          "defaultConfiguration": "development"
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n"
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "polyfills": [
              "zone.js",
              "zone.js/testing",
              "@angular/localize/init"
            ],
            "tsConfig": "tsconfig.spec.json",
            "inlineStyleLanguage": "scss",
            "assets": [
              {
                "glob": "**/*",
                "input": "public"
              }
            ],
            "styles": ["src/styles.scss"],
            "scripts": []
          }
        }
      }
    }
  }
}

and here is my Vue index.html showing where I import the built files from the Angular web component:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="http://127.0.0.1:8080/styles.css" />
    <link
      rel="preload"
      href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
      as="style"
    />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
    <script src="http://127.0.0.1:8080/polyfills.js" type="module"></script>
    <script src="http://127.0.0.1:8080/main.js" type="module"></script>
  </body>
</html>

and here is the stacktrace from the error

zone.js:125 Uncaught 
TypeError: emitter.pipe is not a function
    at elements.mjs:310:28
    at Array.map (<anonymous>)
    at ComponentNgElementStrategy.initializeOutputs (elements.mjs:308:61)
    at ComponentNgElementStrategy.initializeComponent (elements.mjs:290:14)
    at elements.mjs:212:22
    at _ZoneDelegate.invoke (zone.js:365:28)
    at Object.onInvoke (core.mjs:14882:33)
    at _ZoneDelegate.invoke (zone.js:364:34)
    at _ZoneImpl.run (zone.js:111:43)
    at _NgZone.run (core.mjs:14733:28)
(anonymous) @   elements.mjs:310
initializeOutputs   @   elements.mjs:308
initializeComponent @   elements.mjs:290
(anonymous) @   elements.mjs:212
invoke  @   zone.js:365
onInvoke    @   core.mjs:14882
invoke  @   zone.js:364
run @   zone.js:111
run @   core.mjs:14733
runInZone   @   elements.mjs:381
connect @   elements.mjs:203
connectedCallback   @   elements.mjs:471
invoke  @   zone.js:365
runGuarded  @   zone.js:121
(anonymous) @   zone.js:105
insert  @   chunk-2LTNOSJU.js?v=36498c81:9636
mountElement    @   chunk-2LTNOSJU.js?v=36498c81:6124
processElement  @   chunk-2LTNOSJU.js?v=36498c81:6040
patch   @   chunk-2LTNOSJU.js?v=36498c81:5908
patchBlockChildren  @   chunk-2LTNOSJU.js?v=36498c81:6307
processFragment @   chunk-2LTNOSJU.js?v=36498c81:6398
patch   @   chunk-2LTNOSJU.js?v=36498c81:5894
componentUpdateFn   @   chunk-2LTNOSJU.js?v=36498c81:6674
run @   chunk-2LTNOSJU.js?v=36498c81:435
instance.update @   chunk-2LTNOSJU.js?v=36498c81:6718
callWithErrorHandling   @   chunk-2LTNOSJU.js?v=36498c81:1663
flushJobs   @   chunk-2LTNOSJU.js?v=36498c81:1876
invoke  @   zone.js:365
run @   zone.js:111
(anonymous) @   zone.js:2498
invokeTask  @   zone.js:398
runTask @   zone.js:158
drainMicroTaskQueue @   zone.js:577
Promise.then (async)        
nativeScheduleMicroTask @   zone.js:553
scheduleMicroTask   @   zone.js:564
scheduleTask    @   zone.js:387
scheduleTask    @   zone.js:201
scheduleMicroTask   @   zone.js:221
scheduleResolveOrReject @   zone.js:2488
resolvePromise  @   zone.js:2422
(anonymous) @   zone.js:2330
(anonymous) @   zone.js:2346
Promise.then (async)        
(anonymous) @   zone.js:2740
ZoneAwarePromise    @   zone.js:2662
Ctor.then   @   zone.js:2739
queueFlush  @   chunk-2LTNOSJU.js?v=36498c81:1786
queueJob    @   chunk-2LTNOSJU.js?v=36498c81:1780
(anonymous) @   chunk-2LTNOSJU.js?v=36498c81:6712
resetScheduling @   chunk-2LTNOSJU.js?v=36498c81:516
triggerEffects  @   chunk-2LTNOSJU.js?v=36498c81:560
triggerRefValue @   chunk-2LTNOSJU.js?v=36498c81:1318
set value   @   chunk-2LTNOSJU.js?v=36498c81:1365
(anonymous) @   App.vue:15
invoke  @   zone.js:365
run @   zone.js:111
(anonymous) @   zone.js:2498
invokeTask  @   zone.js:398
runTask @   zone.js:158
drainMicroTaskQueue @   zone.js:577
Promise.then (async)        
nativeScheduleMicroTask @   zone.js:553
scheduleMicroTask   @   zone.js:564
scheduleTask    @   zone.js:387
scheduleTask    @   zone.js:201
scheduleMicroTask   @   zone.js:221
scheduleResolveOrReject @   zone.js:2488
resolvePromise  @   zone.js:2422
(anonymous) @   zone.js:2330
(anonymous) @   zone.js:2346
Promise.then (async)        
(anonymous) @   zone.js:2740
ZoneAwarePromise    @   zone.js:2662
Ctor.then   @   zone.js:2739
(anonymous) @   App.vue:12
Promise.then (async)        
setup   @   App.vue:11
callWithErrorHandling   @   chunk-2LTNOSJU.js?v=36498c81:1663
setupStatefulComponent  @   chunk-2LTNOSJU.js?v=36498c81:9080
setupComponent  @   chunk-2LTNOSJU.js?v=36498c81:9041
mountComponent  @   chunk-2LTNOSJU.js?v=36498c81:6484
processComponent    @   chunk-2LTNOSJU.js?v=36498c81:6450
patch   @   chunk-2LTNOSJU.js?v=36498c81:5920
render2 @   chunk-2LTNOSJU.js?v=36498c81:7246
mount   @   chunk-2LTNOSJU.js?v=36498c81:4511
app.mount   @   chunk-2LTNOSJU.js?v=36498c81:11129
(anonymous) @   main.ts:11
Show less

The error is coming from the Angular elements.mjs package in the Angular elements module.

    /** Sets up listeners for the component's outputs so that the events stream emits the events. */
    initializeOutputs(componentRef) {
        const eventEmitters = this.componentFactory.outputs.map(({ propName, templateName }) => {
            const emitter = componentRef.instance[propName];

            ***** THIS IS THE LINE THROWING THE ERROR *****
            return emitter.pipe(map((value) => ({ name: templateName, value })));
        });
        this.eventEmitters.next(eventEmitters);
    }

I also tried rolling the Angular application back to Angular 17 to see if it was an ng18 issue, but I got the same result.

5
  • "zone.js error TypeError: emitter.pipe is not a function from the polyfill.js file" - please, list the whole error including callstack. Zone itself doesn't use emitter.pipe, this suggests that you could misidentify the problem, and zone just exists in callstack due to the way it works. Please, provide a way to reproduce. There could be a problem with rxjs dependency, for instance Commented Jul 15, 2024 at 11:18
  • @joed4no try running the normal angular project using the dist folder by using a simple httpserver, and check if you aregetting the same errror, then the problem is in angular code. Commented Jul 15, 2024 at 11:22
  • @EstusFlask You are correct, the error is actually from the Angular elements.js package. I added the stacktrace and a the erroring snippet from the elements module. I am not sure how to provide a recreation repo at the moment since this project is confidential and I cannot share it publicly. Commented Jul 15, 2024 at 11:31
  • Then you need to debug what emitter is and why it doesn't have pipe method. This info will help. Please, also add package.json. The problem should be reproducible with empty project and hello world component Commented Jul 15, 2024 at 11:38
  • I had the same problem. I was in a hurry and needed a workaround so I ended up using this.elementRef.nativeElement.dispatchEvent(new CustomEvent('event-from-web-comp', {detail: {...}})); inside my angular web component to notify the parent page. Commented Nov 30, 2024 at 22:28

0

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.