Использование пользовательского веб-компонента Angular вызывает ошибку: селектор корень приложения не соответствует ни одному элементу

У меня есть обычное приложение Angular 10 с лениво загруженными модулями и маршрутизацией. Однако у меня есть особое требование, которое мне нужно выполнить.

На большинстве страниц я хочу инициализировать полное приложение с маршрутизацией и т. Д. Путем встраивания элемента <app-root> в index.html, то есть AppComponent. С другой стороны, на некоторых страницах необходимо инициализировать не все приложение, а только один конкретный <header-search> компонент, который я зарегистрировал с помощью @ angular / elements (веб-компоненты). Это также означает, что в этом случае не должна выполняться ни маршрутизация, ни инициализировать какой-либо другой компонент, кроме <header-search> (если он не внедряется самим компонентом <header-search>).

Примечание для вас, чтобы вы понимали предысторию варианта использования: в проекте, который я создаю, не все части отделены от Angular. Некоторые страницы визуализируются на стороне сервера с использованием Twig / PHP. Но мне нужно, чтобы функция поиска в заголовке, созданная с помощью Angular, была доступна и на этих страницах. Это означает, что у меня не будет полного приложения, доступного одновременно, в данном случае только HeaderSearchComponent. Однако на других страницах полное приложение будет инициализировано, включая HeaderSearchComponet, поэтому нет необходимости в отдельном встраивании с использованием веб-компонентов - в этом случае достаточно элемента <app-root>.

Мои мысли, где зарегистрировать пользовательский веб-компонент HeaderSearchComponent как <header-search> с использованием угловых элементов, таких как:

@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 {}
}

После этого я смогу визуализировать HeaderSearchComponent с помощью <header-search> или полное приложение с помощью <app-root>. Однако, как только я встраиваю только <header-search>, не имея <app-root> одновременно, Angular выдает ошибку:

Ошибка: селектор app-root не соответствует ни одному элементу

Вы можете попробовать это самостоятельно в следующем минимальном примере Stackblitz без сложной логики приложения или маршрутизации, заменив <app-root></app-root> на <header-search></header-search> в файле index.html.

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

Как уже упоминалось, мне нужен рабочий компонент <header-search> без элемента <app-root>. Но также должна быть возможность иметь элемент <app-root> для всего приложения без присутствия компонента <header-search>. Так что либо поиск по заголовку, либо корень приложения, оба должны работать.

Как иметь компонент настраиваемого элемента (в данном случае <header-search>) в качестве точки входа и по-прежнему иметь возможность инициализировать полное приложение Angular с помощью элемента <app-root>?


person dude    schedule 30.09.2020    source источник


Ответы (4)


Веб-компоненты Angular уже работают с элементами angular. Проблема, с которой я столкнулся до сих пор, заключается в том, что я не мог одновременно иметь только веб-компонент без элемента <app-root>. Однако, полностью удалив свойство boostrap модуля приложения и добавив вместо этого

entryComponents: [AppComponent],

заставит ошибку исчезнуть. Однако это больше не будет инициализировать <app-root>, поскольку мы удалили его из части boostrap. Теперь мы должны условно вручную загрузить приложение с помощью:

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

Это поможет.

person dude    schedule 19.10.2020

Мне кажется, что вы забыли добавить HeaderSearchComponent в declarations часть вашего app.module.ts.

Наверное, попробуй вот так:

@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 { ... }
person nrausch    schedule 06.10.2020
comment
Я привел пример Stackblitz. Объявление не отсутствует, но оно объявлено в модуле, который импортируется в модуль приложения (SharedModule). Эта информация теперь включена в вопрос и в новый пример. Извините за беспокойство. - person dude; 18.10.2020

Эта проблема

Проблема заключается в том, что angular жалуется на недостающий элемент. Таким образом, приведенное ниже решение будет проверять, существует ли элемент, и если он не найден, создайте элемент перед загрузкой приложения.

Решение

В нашем файле main.ts мы проверим, существует ли элемент app-root, и если он не найден, мы создадим его и добавим в нужное место. В этом случае мы добавляем после header-search

Мы также объявим 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-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 { }

См. Эту демонстрацию на Stackblitz

person Owen Kelvin    schedule 04.10.2020
comment
Спасибо за ваш ответ. К сожалению, это не решает мою проблему. Причина в том, что если вы удалите элемент <app-root> из HTML в свою скрипку вы все равно получите ERROR Error: The selector "app-root" did not match any elements. Мне нужно решение для сценария, когда доступен компонент app-root или header-search, поэтому он должен работать даже без элемента app-root. - person dude; 04.10.2020
comment
Хотя это предотвращает ошибку, она по-прежнему загружается <app-root> без необходимости, потому что вы добавляете ее, даже если она не используется (то же самое для <header-search>). Решение также не использует элементы Angular для создания веб-компонента, а напрямую использует DOM API браузеров. Честно говоря, это похоже на взлом, а не на способ его решения с помощью Angular. Я совершенно уверен, что есть способ заставить его работать с самими элементами Angular и изменять какие-то конфигурации. - person dude; 20.10.2020
comment
Также будет выдана ошибка, если элемент <header-search> недоступен - как уже упоминалось, он должен работать либо с <app-root> , либо с <header-search>. Тем не менее, это пока первое решение, так что большое спасибо! - person dude; 20.10.2020
comment
Правда, этот метод по-прежнему загружает app-root, но загружает только содержимое app-root, только если оно изначально существовало с использованием *ngIf, т.е. содержимое app-root фактически никогда не загружается. Если вы планируете использовать AOT, может быть сложно достичь своей цели с помощью стандартного angular без «взлома». Мой другой подход заключался в том, чтобы просто динамически предоставлять массив начальной загрузки, но angular жаловался на то, что JIT не разрешен - person Owen Kelvin; 20.10.2020
comment
Я награждаю вас наградой, так как вы опубликовали единственное рабочее решение, кроме меня. Однако я продолжаю использовать свое собственное решение, поскольку оно более элегантно и может быть решено с помощью самого модуля приложения (никаких манипуляций с main.ts и манипуляций с оконным объектом не требуется). - person dude; 24.10.2020
comment
Верно, ваше решение - лучший подход к решению проблемы, я также узнал из вашего решения новый способ ручной загрузки приложения. - person Owen Kelvin; 24.10.2020

Прежде всего вам нужно удалить свой AppComponent из массива начальной загрузки в объявлении NgModule. Вы не хотите загружать свой компонент таким образом. Более того, вам необходимо добавить соответствующие библиотеки.

Я приготовил для вас решение.

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

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

Что я сделал:

  • добавлены элементы @ angular / в package.json
  • добавлены @ webcomponents / custom-elements в package.json
  • добавил @ webcomponents / webcomponentsjs в package.json (для совместимости с es5
  • в polyfills.ts' added line import '@ webcomponents / webcomponentsjs / custom-elements-es5-adapter.js'; `
  • поскольку index.html является основным файлом, я заменяю содержимое простым <header-search></header-search>
  • В app.module.ts я удалил AppComponent из массива начальной загрузки и изменил способ создания WC

Теперь все работает так, как вы ожидали :)

Более того, некоторое время назад я писал на эту тему https://itnext.io/how-to-run-separate-angular-apps-in-one-spa-shell-5250e0fc6155 и подготовил репозиторий с другими примерами: https://github.com/marcinmilewicz/microfrontendly/tree/master/microfrontend-example/foo-app

person Marcin Milewicz    schedule 22.10.2020