Using Angular custom web component throws error: The selector "app-root" did not match any elements
Using Angular custom web component throws error: The selector "app-root" did not match any elements
我有一个正常的 Angular 10 应用程序,带有延迟加载模块和路由。但是,我有一个特殊的要求需要满足。
在大多数页面上,我想通过在 index.html 中嵌入 <app-root>
元素(即 AppComponent
)来初始化带有路由等的完整应用程序。另一方面,在某些页面上,不应初始化完整的应用程序,而应仅初始化我使用@angular/elements(Web 组件)注册的一个特定 <header-search>
组件。这也意味着在这种情况下,不应发生任何路由,也不应初始化除 <header-search>
之外的任何其他组件(如果它不是由 <header-search>
组件本身嵌入)。
旁注只是为了让您了解用例的背景:在我正在构建的项目中,并非所有部分都与 Angular 解耦。某些页面使用 Twig/PHP 呈现 backend-side。但是我需要 header 中使用 Angular 构建的搜索功能也可以在这些页面上使用。这意味着我不会同时获得完整的应用程序,在这种情况下只有 HeaderSearchComponent
。然而,在其他页面上,整个应用程序将被初始化,包括 HeaderSearchComponet
,因此不需要使用 Web 组件单独嵌入 - 在这种情况下
<app-root>
个元素就够了
我的想法是使用 angular 元素将 HeaderSearchComponent
注册为 <header-search>
自定义 Web 组件,例如:
@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 {}
}
有了它,我应该能够使用 <header-search>
渲染 HeaderSearchComponent
或使用 <app-root>
渲染完整的应用程序。但是,一旦我只嵌入 <header-search>
而没有 <app-root>
同时可用 Angular 就会抛出错误:
Error: The selector "app-root" did not match any elements
您可以在以下没有复杂应用程序逻辑或路由的最小 Stackblitz 示例中自行尝试,方法是在 index.html
文件中将 <app-root></app-root>
替换为 <header-search></header-search>
。
https://stackblitz.com/edit/angular-ivy-a4ulcc?file=src/index.html
如前所述,我需要的是一个没有 <app-root>
元素的工作组件 <header-search>
。而且,应该可以在不存在 <header-search>
组件的情况下为完整应用程序提供 <app-root>
元素。所以它是 header-search 或 app-root,两者都应该有效。
如何将自定义元素组件(在本例中为 <header-search>
)作为入口点,并且仍然可以使用 [=11] 初始化完整的 Angular 应用程序=]元素?
问题
问题涉及 angular 抱怨缺少元素。因此,以下解决方案将检查元素是否存在,如果未找到,则在加载应用程序之前创建元素
解决方案
在我们的 main.ts 文件中,我们将检查元素 app-root
是否存在,如果不存在,我们创建一个并将其附加到想要的地方。对于这种情况,我们在 header-search
之后追加
我们还将在 window 变量上声明 APP_ELEMENT
以跟踪元素最初是否存在
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>
我们也可以对 header 组件应用相同的技术
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>
最后我们可以将 HeaderComponent
添加到 app.module.ts
中的 bootstrap 数组
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent, HeaderSearchComponent ],
bootstrap: [ AppComponent, HeaderSearchComponent ]
})
export class AppModule { }
对我来说,你好像忘了在 app.module.ts
的 declarations
部分添加 HeaderSearchComponent
。
大概这样试试:
@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);
}
}
还要确保您的 app.component.ts
具有以下注释:
@Component({
selector: 'app-root',
...
})
export class AppComponent { ... }
Angular 网络组件已与 angular 元素一起使用。到目前为止,我遇到的问题是我不能同时拥有一个没有 <app-root>
元素的 Web 组件。但是,完全删除应用程序模块的 boostrap
属性 并添加
entryComponents: [AppComponent],
将使错误消失。但是,这将不再初始化 <app-root>
,因为我们已将其从 boostrap
部分中删除。现在我们必须有条件地手动 bootstrap 应用程序:
public ngDoBootstrap(appRef: ApplicationRef): void {
if (document.querySelector('app-root')) {
appRef.bootstrap(AppComponent);
}
}
这样就可以了。
首先,您必须从 NgModule 声明中的 bootstrap 数组中删除您的 AppComponent。您不希望以这种方式 bootstrap 您的组件。
更重要的是,您需要添加适当的库。
我已经为你准备好了解决方案。
https://angular-ivy-hsyuvz.stackblitz.io
https://stackblitz.com/edit/angular-ivy-hsyuvz
我做了什么:
- 将@angular/elements添加到package.json
- 将@webcomponents/custom-elements添加到package.json
- 添加@webcomponents/webcomponentsjs到package.json(为了兼容es5
- 在
polyfills.ts' added line
导入'@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'; `
- 因为index.html是主文件,我用简单的
<header-search></header-search>
替换内容
- 在 app.module.ts 中,我从 bootstrap 数组中删除了 AppComponent 并更改了创建 WC 的方式
现在,它如你所愿:)
此外,我前段时间写过关于这个话题的文章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
我有一个正常的 Angular 10 应用程序,带有延迟加载模块和路由。但是,我有一个特殊的要求需要满足。
在大多数页面上,我想通过在 index.html 中嵌入 <app-root>
元素(即 AppComponent
)来初始化带有路由等的完整应用程序。另一方面,在某些页面上,不应初始化完整的应用程序,而应仅初始化我使用@angular/elements(Web 组件)注册的一个特定 <header-search>
组件。这也意味着在这种情况下,不应发生任何路由,也不应初始化除 <header-search>
之外的任何其他组件(如果它不是由 <header-search>
组件本身嵌入)。
旁注只是为了让您了解用例的背景:在我正在构建的项目中,并非所有部分都与 Angular 解耦。某些页面使用 Twig/PHP 呈现 backend-side。但是我需要 header 中使用 Angular 构建的搜索功能也可以在这些页面上使用。这意味着我不会同时获得完整的应用程序,在这种情况下只有 HeaderSearchComponent
。然而,在其他页面上,整个应用程序将被初始化,包括 HeaderSearchComponet
,因此不需要使用 Web 组件单独嵌入 - 在这种情况下
<app-root>
个元素就够了
我的想法是使用 angular 元素将 HeaderSearchComponent
注册为 <header-search>
自定义 Web 组件,例如:
@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 {}
}
有了它,我应该能够使用 <header-search>
渲染 HeaderSearchComponent
或使用 <app-root>
渲染完整的应用程序。但是,一旦我只嵌入 <header-search>
而没有 <app-root>
同时可用 Angular 就会抛出错误:
Error: The selector "app-root" did not match any elements
您可以在以下没有复杂应用程序逻辑或路由的最小 Stackblitz 示例中自行尝试,方法是在 index.html
文件中将 <app-root></app-root>
替换为 <header-search></header-search>
。
https://stackblitz.com/edit/angular-ivy-a4ulcc?file=src/index.html
如前所述,我需要的是一个没有 <app-root>
元素的工作组件 <header-search>
。而且,应该可以在不存在 <header-search>
组件的情况下为完整应用程序提供 <app-root>
元素。所以它是 header-search 或 app-root,两者都应该有效。
如何将自定义元素组件(在本例中为 <header-search>
)作为入口点,并且仍然可以使用 [=11] 初始化完整的 Angular 应用程序=]元素?
问题
问题涉及 angular 抱怨缺少元素。因此,以下解决方案将检查元素是否存在,如果未找到,则在加载应用程序之前创建元素
解决方案
在我们的 main.ts 文件中,我们将检查元素 app-root
是否存在,如果不存在,我们创建一个并将其附加到想要的地方。对于这种情况,我们在 header-search
我们还将在 window 变量上声明 APP_ELEMENT
以跟踪元素最初是否存在
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>
我们也可以对 header 组件应用相同的技术 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>
最后我们可以将 HeaderComponent
添加到 app.module.ts
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent, HeaderSearchComponent ],
bootstrap: [ AppComponent, HeaderSearchComponent ]
})
export class AppModule { }
对我来说,你好像忘了在 app.module.ts
的 declarations
部分添加 HeaderSearchComponent
。
大概这样试试:
@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);
}
}
还要确保您的 app.component.ts
具有以下注释:
@Component({
selector: 'app-root',
...
})
export class AppComponent { ... }
Angular 网络组件已与 angular 元素一起使用。到目前为止,我遇到的问题是我不能同时拥有一个没有 <app-root>
元素的 Web 组件。但是,完全删除应用程序模块的 boostrap
属性 并添加
entryComponents: [AppComponent],
将使错误消失。但是,这将不再初始化 <app-root>
,因为我们已将其从 boostrap
部分中删除。现在我们必须有条件地手动 bootstrap 应用程序:
public ngDoBootstrap(appRef: ApplicationRef): void {
if (document.querySelector('app-root')) {
appRef.bootstrap(AppComponent);
}
}
这样就可以了。
首先,您必须从 NgModule 声明中的 bootstrap 数组中删除您的 AppComponent。您不希望以这种方式 bootstrap 您的组件。 更重要的是,您需要添加适当的库。
我已经为你准备好了解决方案。
https://angular-ivy-hsyuvz.stackblitz.io
https://stackblitz.com/edit/angular-ivy-hsyuvz
我做了什么:
- 将@angular/elements添加到package.json
- 将@webcomponents/custom-elements添加到package.json
- 添加@webcomponents/webcomponentsjs到package.json(为了兼容es5
- 在
polyfills.ts' added line
导入'@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'; ` - 因为index.html是主文件,我用简单的
<header-search></header-search>
替换内容
- 在 app.module.ts 中,我从 bootstrap 数组中删除了 AppComponent 并更改了创建 WC 的方式
现在,它如你所愿:)
此外,我前段时间写过关于这个话题的文章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