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.
elements.jspackage. 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.emitteris 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 componentthis.elementRef.nativeElement.dispatchEvent(new CustomEvent('event-from-web-comp', {detail: {...}}));inside my angular web component to notify the parent page.