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

4

11

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?

Relativistic answered 30/9, 2020 at 22:0 Comment(0)
R
11

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.

Relativistic answered 19/10, 2020 at 21:32 Comment(0)
S
2

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 { ... }
Signpost answered 6/10, 2020 at 9:33 Comment(1)
I've provided a Stackblitz example now. The declaration isn't missing, but it's declared in a module that is imported in the app module (SharedModule). This information is now included in the question and in the new example. Sorry for the inconvenience.Relativistic
C
0

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

Conard answered 4/10, 2020 at 6:18 Comment(6)
Thanks for your reply. Unfortunately, it doesn't fix my problem. The reason is, that if you remove the <app-root> element from the HTML in your fiddle you will still get ERROR Error: The selector "app-root" did not match any elements. What I need is a solution for the scenario when either the app-root or the header-search component is available, so it should work even without app-root element.Relativistic
While this prevents the error, it's still bootstrapping <app-root> unnecessarily because you're adding it even if it's unused (same for <header-search>). The solution also doesn't use Angular elements to create a web component, but uses browsers DOM API directly. To be honest, this totally feels like a hack and not the Angular way to solve it. I'm quite confident there's a way to make it work with Angular elements itself and modifying some kind of configurations.Relativistic
Also an error will be thrown when the <header-search> element isn't available – as mentioned it should work with either <app-root> or <header-search>. However, it's the first solution so far, so thanks very much!Relativistic
True, this method still bootstraps the app-root but only loads the contents of the app-root only if it initially existed using *ngIf i.e, the contents of the app-root are actually never loaded. It may be tricky to achieve your goal using standard angular without a 'hack' if you plan on using 'AOT'. My other approach was to simply supply the bootstrap array dynamically but angular complained about JIT not allowedConard
I'm awarding you the bounty since you have posted the only working solution beside from myself. However, I continue with my own solution as it's more elegant and can be solved with the app module itself (no main.ts manipulation necessary and no window object manipulation needed).Relativistic
True, your Solution is a better approach to tackle the problem, I also have also learned from your solution a new way to manually bootstrap an appConard
W
0

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

Wartburg answered 22/10, 2020 at 14:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.