angulartypescriptcustom-element

Angular custom elements keeps giving NullInjectorError


I really think I am really close, but I can't seem to get it to work. Probably something stupid.

What I try to do:

A client has a WordPress website and I want to embed a piece of Angular inside the WordPress website. How I do is not really important, but I want to let you know the ultimate goal of this weeks adventure. I know the host supports NodeJS as I can run a complete Angular TS app in a subdomain.

I have followed many tutorials and I always end with the same: Some lame error that doesn't make much sense (like most errors).

To make sure it can what I want I created a very simple Angular application with a component called "my-embedded", as stand alone component. The parts, base dir is src/app:

my-embedded/my-embedded.component.ts:

import { Component } from '@angular/core';

@Component({
    selector: 'my-embedded-element',
    standalone: true,  
    template: `<h1>Hello from Angular Standalone Element!</h1>`,
})
export class MyEmbeddedComponent {}

I don't need the HTML and CSS for now, so I removed those.

app.component.ts:

import { Component } from '@angular/core';

@Component({
    selector: 'app-root',
    standalone: true,
    template: ''
})
export class AppComponent {
  
}

Should do nothing, so it's empty. HTML, SCSS, and test are removed.

Thanks to some other post on SO I changed my main.ts:

import 'zone.js'; // Required for Angular
import '@webcomponents/custom-elements'; // Polyfill for custom elements if necessary
import { bootstrapApplication } from '@angular/platform-browser';
import { MyEmbeddedComponent } from './app/my-embedded/my-embedded.component';
import { createCustomElement } from '@angular/elements';

// Bootstrap the standalone component as a custom element
bootstrapApplication(MyEmbeddedComponent).then((ref) => {
    const injector = ref.injector;
    const myElement = createCustomElement(MyEmbeddedComponent, { injector });
    customElements.define('my-embedded-element', myElement);
});

I build the whole project with

ng build --configuration=production --optimization=false

This makes the JS files larger than 1MB, so I had to change the budgets of the production in the angular.json:

"budgets": [
    {
        "type": "initial",
        "maximumWarning": "2mb",
        "maximumError": "2mb"
    }

This is the complete angular.json:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-elements-project": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss"
        }
      },
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:application",
          "options": {
            "outputPath": "dist/angular-elements-project",
            "index": "src/index.html",
            "browser": "src/main.ts",
            "polyfills": [
              "zone.js"
            ],
            "tsConfig": "tsconfig.app.json",
            "inlineStyleLanguage": "scss",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "2mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kb",
                  "maximumError": "4kb"
                }
              ],
              "outputHashing": "all"
            },
            "development": {
              "optimization": false,
              "extractLicenses": false,
              "sourceMap": true
            }
          },
          "defaultConfiguration": "production"
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "configurations": {
            "production": {
              "buildTarget": "angular-elements-project:build:production"
            },
            "development": {
              "buildTarget": "angular-elements-project:build:development"
            }
          },
          "defaultConfiguration": "development"
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "buildTarget": "angular-elements-project:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "polyfills": [
              "zone.js",
              "zone.js/testing"
            ],
            "tsConfig": "tsconfig.spec.json",
            "inlineStyleLanguage": "scss",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": []
          }
        }
      }
    }
  }
}

I could use the g build --configuration=production --output-hashing=none, but then it won't give me correct errors. I will use this one when it works.

This gives me a bunch of files. I copy all the JS files to a local webserver and add an index.html with the following HTML:

<!doctype html>
<html lang="en" data-critters-container>

<head>
    <meta charset="utf-8">
    <title>AngularElementsProject</title>
    <base href="/CrawlerWebsite/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
    <my-embedded-element></my-embedded-element>

    <script src="polyfills-TGKJVAXZ.js"></script>
    <script src="main-G5IVIT5U.js"></script>
</body>

</html>

When I open the index.html on the service I get this error:

polyfills-TGKJVAXZ.js:1828 NullInjectorError: R3InjectorError(Environment Injector)[_ComponentFactoryResolver$12 -> _ComponentFactoryResolver$12]: 
    NullInjectorError: No provider for _ComponentFactoryResolver$12!
        at NullInjector.get (main-G5IVIT5U.js:5058:21)
        at R3Injector.get (main-G5IVIT5U.js:5940:27)
        at R3Injector.get (main-G5IVIT5U.js:5940:27)
        at getComponentInputs (main-G5IVIT5U.js:50206:45)
        at createCustomElement (main-G5IVIT5U.js:50444:18)
        at main-G5IVIT5U.js:50518:21
        at _ZoneDelegate.invoke (polyfills-TGKJVAXZ.js:300:158)
        at _ZoneImpl.run (polyfills-TGKJVAXZ.js:94:35)
        at polyfills-TGKJVAXZ.js:2004:30
        at _ZoneDelegate.invokeTask (polyfills-TGKJVAXZ.js:326:171)

It does work and I do see the text from the custom element, but this console error keep bothering me.

Some websites/solutions say that I should change

bootstrapApplication(MyEmbeddedComponent).then((ref) => {
    const injector = ref.injector;
    const myElement = createCustomElement(MyEmbeddedComponent, { injector });
    customElements.define('my-embedded-element', myElement);
});

to

bootstrapApplication(AppComponent).then((ref) => {
    const injector = ref.injector;
    const myElement = createCustomElement(MyEmbeddedComponent, { injector });
    customElements.define('my-embedded-element', myElement);
});

But when I do that, the error says it can't find app-root.

Other solutions on Google, SO, ChatGPT don't do much and it keeps giving me the same error.


Solution

  • As Angular docs states:

    Avoid using the component's selector as the custom element tag name.

    So, in your component definition change the selector as follows: selector: 'app-embedded-element'. In your main.ts file you will use the tag name you originally assigned (my-embedded-element).