Using Angular custom web component throws error: The selector "app-root" did not match any elements

8.4k views Asked by At

I have a normal Angular 10 application with lazy loaded modules and routing. However, I have a special requirement I need to fulfill.

On most pages I want to initialize the full application with routing etc. by embedding the <app-root> element in the index.html – which is the AppComponent. On some pages on the other hand not the full app should be initialized, rather only one specific <header-search> component that I've registered using @angular/elements (web components). This also means that no routing should take place nor should any other component except the <header-search> be initialized (if it's not embedding by the <header-search> component itself) in this case.

Side note just for you to understand the background of the use case: In the project I'm building not all parts are decoupled with Angular. Some pages are rendered backend-side using Twig/PHP. But I need the search functionality in the header that was built with Angular to be available on these pages too. This means I won't have the full application available at the same time, only the HeaderSearchComponent in this case. On other pages, however, the full application will be initialized including the HeaderSearchComponet, so there's no need for a separate embed using web components – in this case the <app-root> element is enough.

My thoughts where do register the HeaderSearchComponent as <header-search> custom web component using angular elements like:

@NgModule({
  imports: [BrowserModule, FormsModule, SharedModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  entryComponents: [HeaderSearchComponent]
})
export class AppModule implements DoBootstrap {
  constructor(injector: Injector) {
    const webComponent = createCustomElement(HeaderSearchComponent, {
      injector
    });
    customElements.define("header-search", webComponent);
  }

  public ngDoBootstrap(): void {}
}

With that I should be able to render the HeaderSearchComponent using <header-search> or the full app using <app-root>. However, once I only embed <header-search> without having <app-root> available at the same time Angular throws an error:

Error: The selector "app-root" did not match any elements

You can try this out yourself in the following minimal Stackblitz example without complex application logic or routing, by replacing <app-root></app-root> with <header-search></header-search> in the index.html file.

https://stackblitz.com/edit/angular-ivy-a4ulcc?file=src/index.html

As mentioned, what I need is a working component <header-search> without the <app-root> element being present. But also, it should be possible to have the <app-root> element for the full application without the <header-search> component being present. So it's either header-search or app-root, both should work.

How to have a custom element component (in this case <header-search>) as an entry point and still the possibility to initialize a full Angular application with the <app-root> element?

4

There are 4 answers

0
dude On BEST ANSWER

Angular web components already work with angular elements. The problem that I've had so far is that I couldn't have only a web component without an <app-root> element at the same time. However, removing the boostrap property of the app module entirely and instead adding

entryComponents: [AppComponent],

will make the error disappear. However, this will not initialize <app-root> anymore, since we've removed it from the boostrap part. Now we have to conditionally manually bootstrap the app with:

public ngDoBootstrap(appRef: ApplicationRef): void {
  if (document.querySelector('app-root')) {
    appRef.bootstrap(AppComponent);
  }
}

This will do the trick.

6
Owen Kelvin On

The problem

The problem involves the angular complaining about missing element. The below solution will hence check if an element if it exists and if non is found, create the element before loading the app

Solution

In our main.ts file, we will check if the element app-root exists and if non is found, we create one and append it to the desired place. For this case we are appending after header-search

We will also declare APP_ELEMENT on the window variable to track whether the element initially existed or not

window["APP_ELEMENT"] = { root: true, header: true };
let headerSearchTag = document.querySelector("header-search");
const appRootTag = document.querySelector("app-root");
if (!headerSearchTag) {
  document
    .querySelector("body")
    .prepend(document.createElement("header-search"));
  headerSearchTag = document.querySelector("header-search");
  window["APP_ELEMENT"] = { ...window["APP_ELEMENT"], header: false };
}
if (!appRootTag) {
  headerSearchTag.parentNode.insertBefore(
    document.createElement("app-root"),
    headerSearchTag.nextSibling
  );
  window["APP_ELEMENT"] = { ...window["APP_ELEMENT"], root: false };
}

app.component.ts

appElement = window["APP_ELEMENT"].root;

app.component.html

<ng-container *ngIf="appElement">
  <h1>BODY</h1>
  <p>
    APP CONTENTS
  </p>
</ng-container>

We can also apply the same technique for the header component header-search.component.ts

appElement = window["APP_ELEMENT"].header;

header-search.component.html

<ng-container *ngIf="appElement">
  <h1>Header</h1>
  <p>
    Some nice header
  </p>
</ng-container>

Finally we can add our HeaderComponent to the bootstrap array in app.module.ts

@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent, HeaderSearchComponent ],
  bootstrap: [ AppComponent, HeaderSearchComponent ]
})
export class AppModule { }

See this Demo on Stackblitz

1
nrausch On

For me, it seems like you forgot to add HeaderSearchComponent in the declarations part of your app.module.ts.

Probably try it like this:

@NgModule({
  declarations: [
    AppComponent,
    HeaderSearchComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    GraphQLModule,
    SharedModule,
    AppRoutingModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
  entryComponents: [HeaderSearchComponent],
})
export class AppModule {
  constructor(private injector: Injector) {
    const webComponent = createCustomElement(HeaderSearchComponent, {injector});
    customElements.define('header-search', webComponent);
  }
}

Also make sure your app.component.ts has the following annotation:

@Component({
  selector: 'app-root',
  ...
})
export class AppComponent { ... }
0
Marcin Milewicz On

first of all you have to remove your AppComponent from bootstrap array in NgModule declaration. You don't want to bootstrap your component in this way. What's more, you need to add appropriate libraries.

I've prepared for you solution.

https://angular-ivy-hsyuvz.stackblitz.io

https://stackblitz.com/edit/angular-ivy-hsyuvz

What I've done:

  • added @angular/elements to package.json
  • added @webcomponents/custom-elements to package.json
  • added @webcomponents/webcomponentsjs to package.json (for es5 compatibility
  • in polyfills.ts' added line import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'; `
  • as index.html is main file, I replace content by simple <header-search></header-search>
  • In app.module.ts I've removed AppComponent from bootstrap array and change way to WC creation

Now, it works as you expected:)

Moreover, I've written some time ago about this topic https://itnext.io/how-to-run-separate-angular-apps-in-one-spa-shell-5250e0fc6155 and prepared repository with more examples: https://github.com/marcinmilewicz/microfrontendly/tree/master/microfrontend-example/foo-app