Динамический Angular или манипулируй правильно

    image

    Любой создаваемый проект не обходится без динамического создания элементов. Рано или поздно вам понадобится либо создать tooltip для элемента, показать модальное окно, или вовсе сформировать некоторые блоки динамически подгружая их с сервера. При решении таких задач я зачастую определяю зрелость фреймворка, который использую: насколько просто я могу в нем создавать динамический контент, и какие возможности он мне для этого предлагает. В этой статье мы поговорим о динамическом создании контента в новом Angular и рассмотрим различные подходы, которые он нам предоставляет.


    Прежде чем перейти к созданию контента, нам необходимо рассмотреть ряд абстракций, которые есть в Angular — что они собой представляют и для чего используются. Так как ангуляр разработан как решение, которое может работать на различных платформах — в браузере, на мобильном устройстве и на сервере, — то прямая работа с DOM в нем не очень приветствуется, хотя и возможна. Например, следующий пример будет хорошо работать в браузере, но может перестать работать, если вы используете Web Worker или ваш код работает на мобильном устройстве.


    import { Component, AfterComponentInit, ViewChild } from '@angular/core';
    
    @Component({
       selector: 'some-component',
       template: '<input type="text" #input>'
    })
    export class SomeComponent implements AfterContentInit {
       @ViewChild('input') input;
    
       ngAfterContentInit() {
          this.input.nativeElement.focus();
       } 
    }

    Вместо прямой работы с DOM-элементом Angular предоставляет нам следующие абстракции — Renderer, TemplateRef, ElementRef и ViewContainerRef. Давайте рассмотрим их по порядку и посмотрим, как с помощью них мы сможем создавать динамический контент.


    Renderer


    Я буду говорить о Renderer2 (далее просто Renderer), так как первая версия уже помечена как deprecated. Renderer используется в основном для манипуляций над уже существующими элементами, например для изменения стилей элемента, атрибутов и параметров элемента. Наиболее часто его использование можно встретить при создании директив. Но он также позволяет создавать новые элементы и вставлять их в DOM, что подходит для нашей задачи.


    Давайте рассмотрим методы, которые нам предоставляет Renderer:


    • createElement(name: string, namespace?: string): any
      Позволяет создать элемент DOM и опционально указать для него пространство имен. Пространство имен используется, например, для вставки SVG-элементов. Элемент после создания не будет отображаться в DOM пока мы его туда не добавим.
      let inputElement = this.renderer.createElement('input');
    • appendChild(parent: any, newChild: any): void
      insertBefore(parent: any, newChild: any, refChild: any): void
      removeChild(parent: any, oldChild: any): void


      Используются для вставки/удаления созданных или существующих элементов в DOM.


      let inputElement = this.renderer.createElement('input');
      this.renderer.appendChild(parent, inputElement);

    • setAttribute(el: any, name: string, value: string, namespace?: string): void
      removeAttribute(el: any, name: string, namespace?: string): void
      setProperty(el: any, name: string, value: any): void


      Используются для изменения атрибутов или параметров DOM-элемента, например, для установки значения checkbox.


      this.renderer.setAttribute(inputElement, 'value', 'Hello from renderer');
      this.renderer.setProperty(inputElement, 'checked', true);

    • createText(value: string): any
      Создает текстовый DOM-элемент, который можно добавит как дочерний в нужный элемент.
      let buttonElement = this.renderer.createElement('button');
      const text = this.renderer.createText('Text');
      this.renderer.appendChild(buttonElement, text);
    • addClass(el: any, name: string): void
      removeClass(el: any, name: string): void

      Устанавливает или удаляет класс для DOM-элемента.
      this.renderer.addClass(buttonElement, 'btn-large');

    Это далеко не все, что предоставляет Renderer, но, даже используя указанные методы уже можно динамически создавать и изменять элементы DOM.


    Но прежде чем воспользоваться возможностями Renderer, нам необходимо рассмотреть еще один момент — как в ангуляр находить DOM элементы-контейнеры, в которые мы будем добавлять динамический контент. Для этого у нас есть два способа — воспользоваться Dependency Injector или использовать ряд декораторов — @ViewChild/@ViewChildren и>@ViewChildren/@ContentChildren. Давайте рассмотрим оба варианта и начнем с самого простого.


    Доступ к элементу через DI


    Данный способ довольно часто используется при создании собственных директив. Для того чтобы получить доступ к элементу (контейнеру) директивы, надо добавить в конструктор директивы приватную переменную с типом ElementRef. Давайте рассмотрим, как будет выглядеть добавление элементов с помощью сервиса Renderer в данном случае:


    input { Directive, Renderer2, ElementRef, Input} from '@angular/core';
    
    @Directive({
       selector: 'someDirective'
    })
    export class SomeDirective {
       constructor(
          private renderer: Renderer2,
          private elementRef: ElementRef
       ) {}
    
       @Input() set content(value: string) {
          let buttonElement = this.renderer.createElement('button');
          const text = this.renderer.createText('Text');
          this.renderer.appendChild(buttonElement, text);
          this.renderer.appendChild(this.elementRef.nativeElement, buttonElement);
       }
    }

    В данном примере мы создаем новую кнопку и вставляем ее в DOM. Пример, конечно, надуманный, но позволяет нам увидеть основные моменты для работы с DOM. Ссылка ElementRef указывает на элемент, на который была применена наша директива. Все довольно просто, но, к сожалению, данный метод удобен только для директив и не очень удобен, когда вы создаете компоненты с динамическим содержимым. Давайте теперь рассмотрим более универсальный метод.


    @ViewChildren/@ContentChildren


    Для поиска элементов в DOM ангуляр предоставляет ряд декораторов — @ViewChild/@ViewChildren и>@ViewChildren/@ContentChildren. Директива @ViewChild отличается от @ViewChildren тем, что первая всегда вернет вам только один элемент, в то время как вторая позволяет вам находить несколько элементов, возвращая вам объект типа QueryList.


    QueryList представляет из себя итерируемый интерфейс, а также позволяет подписываться на изменение элементов через механизм Observable. Декораторы @ViewChildren и @ContentChildren необходимо использовать в обработчике ngAfterViewInit жизненного цикла компонента, так как раньше QuryList просто будет не определен.


    Пара директив>@ViewChildren/@ContentChildren ведет себя аналогичным образом и отличается от связки @ViewChild/@ViewChildren только тем, что>@ViewChildren ищет элементы просто в DOM-дереве, в то время как @ViewChild ищет элементы в ShadowDom. В данной статье для простоты мы не будет рассматривать связку>@ViewChildren/@ContentChildren, а также ограничимся только @ViewChild-декоратором, так как не будем использовать несколько элементов. Для поиска элементов мы воспользуемся следующим синтаксисом @ViewChild:


    @ViewChild('[query params]', { read: [referenceType], descendants: boolean });

    где


    • query params – элемент который ищем. Может быть, как имя шаблона, html элемент или компонент/директива.
    • descendants – определяет искать элемент только среди прямых потомков или смотреть глубже.
    • read — указание типа возвращаемого элемента. Обычно указание данного параметра не является необходимым, так как ангуляр довольно сообразителен и, если вы ищете шаблон, он вернет вам TemplateRef, если вы ищете html элемент, ангуляр вернет вам ElementRef. Но в некоторых случая, например, когда вам надо получить ViewContainerRef, вам придётся указать тип возвращаемого элемента.

    При поиске элементов указанные декораторы возвращают переменную типа ElementRef — верхнеуровневую абстракцию, которая содержит в себе ссылку на «нативный» DOM-элемент:


    class ElementRef {
       constructor(nativeElement: any)
       nativeElement: any
    }

    Итак, давайте посмотрим, как нам найти элемент в компоненте и, используя Renderer, изменить его содержимое:


    @Component({
       selector: 'some-component',
       template: '<div #elem>Element text</div>'
    })
    export class SomeComponent implements AfterViewInit {
    
       @ViewChild('elem') _elem: ElementRef;
    
       constructor(private _renderer: Renderer2) {}
    
       ngAfterViewInit() {
          const buttonElement = this._renderer.createElement('button');
          const text = this._renderer.createText('Text');
    
          this._renderer.appendChild(buttonElement, text);
          this._renderer.appendChild(this._elem.nativeElement, buttonElement);
       }
    }

    Как и в примере выше, мы создаем кнопку с заданным текстом и добавляем ее в DOM. Только на этот раз мы вставляем кнопку в нужный нам контейнер внутри компонента. Данный подход слишком низкоуровневый и используется довольно редко, поэтому пойдем дальше и рассмотрим, что же еще предоставляет нам Angular.


    TemplateRef


    Идея использования шаблонов для вставки новых элементов не нова и давно используется JS-разработчиками. При использовании template тега из HTML5 браузер создаст DOM-дерево для содержимого тега, но не будет вставлять его в DOM. Вот пример использования тега template в классическом, «нативном» JS:


    <template id="some_template">
       <div>Template contrent text</div>
    
</template>

    
    
<div id="container"></div>
    
    

<script>

       let tpl = document.querySelector('#some_template');
   
       let container = document.querySelector('#container');
   
       insertAfter(container, tpl.content);

    </script>

    Ангуляр предоставляет свою нотацию описания шаблонов, а также позволяет манипулировать шаблоном и его содержимым. С этой абстракцией вы могли познакомиться, если создавали свои собственные структурные директивы наподобие ngIf и ngFor. Для доступа к шаблону мы воспользуемся типом TemplateRef — это ссылка на элемент ng-template в вашем компоненте или директиве. У вас есть два способа получить доступ к шаблону — используя тег ng-template и Dependency Injection или используя поиск элементов через Query-декораторы, о которых мы рассказывали выше. Давайте рассмотрим оба способа и начнем с самого простого:


    @Directive({
       selector: '[isAdmin]'
    })
    export class IsAdminDirective {
    
       @Input() set isAdmin(value: boolean) {
          if (value) {
             this.viewContainerRef.createEmbeddedView(this.templateRef);
          } else {
             this.viewContainerRef.clear();
          }
       }
    
       constructor(
          private templateRef: TemplateRef<any>,
          private viewContainerRef: ViewContainerRef
       ) {}
    
    }

    В примере выше мы использовали Dependency Injection, чтобы получить доступ к шаблону нашей директивы и динамически вставили ее в DOM, используя ViewContainerRef. О ViewContainerRef мы поговорим позднее, пока не обращайте на него внимания, а сейчас давайте рассмотрим, как мы можем динамически создавать DOM-элементы, используя декоратор @ViewChild:


    @Component({
       selector: 'some-component',
       template: `
          <ng-template #tpl1><span>Some template content 1</span></ng-template>
          <ng-template #tpl2><span>Some template content 2</span></ng-template>
    
          <div #container></div>
       `
    })
    export class SomeComponent {
       @Input() set isAdmin(value: boolean) {
          if (value) {
             this.view = this.viewContainerRef.createEmbeddedView(this._tpl);
          } else {
             this.view.destroy();
          }
       }
    
       @ViewChild('tpl1') _tpl: TemplateRef;
    
       private view: EmbeddedViewRef<Object>;
    
       constructor(private viewContainerRef: ViewContainerRef) {}
    }

    В данном примере с помощью декоратора @ViewChild мы находим нужный нам шаблон в виде переменной типа TemplateRef и вставляем его в DOM аналогичным способом, как и в примере с конструктором.


    Кстати, ангуляр удалит тег ng-template и его содержимое из DOM и вместо него разместит комментарий <!—ng-template bindings={}-->. Данный способ позволяет создавать простой динамический контент на основе готовых шаблонов. Но пойдем дальше и посмотрим, что еще нам доступно.


    ViewContainerRef


    Настало время поговорить о ViewContainerRef, который мы неоднократно видели в примерах выше. ViewContainerRef представляет собой ссылку на контейнер компонента или директивы и, кроме доступа к элементу, позволяет создавать два типа View — Host Views (View элементы, создаваемые на основе компонентов) и Embedded Views (View элементы, создаваемые на основе готовых шаблонов). Все создаваемые элементы имеют базовый тип View, который является основным строительным блоком для Angular приложений и представляет собой сгруппированные DOM-элементы, с которыми ангуляр работает как с единым целым и позволяет привязывать эту группу к Change Detection механизму. ViewContainerRef содержит в себе довольно много методов, давайте их рассмотрим:


    • createEmbeddedView(templateRef: TemplateRef, context?: C, index?: number): EmbeddedViewRef
      Этим методом мы пользовались в наших примерах. Он позволяет создавать новые View-элементы на основе готовых шаблонов и вставляет результат в DOM-контейнер. В качестве параметров можно также передать контекст, данные из которого можно использовать в шаблоне, и индекс, по которому можно разместить создаваемый элемент.


    • createComponent(componentFactory: ComponentFactory, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef): ComponentRef
      Создает View элемент на основе экземпляра компонента и вставляет его в DOM, возвращая нам указатель на созданный компонент. Для создания элемента необходимо сначала получить фабрику компонента и инжектор.


    • clear(): void
      Удаляет все View элементы в контейнере


    • insert(viewRef: ViewRef, index?: number): ViewRef
      Вставляет View-элемент, в заданную позицию контейнера


    • remove(index?: number): void
      Удаляет View-элемент по указанному индексу. Если индекс не задан, будет удален последний View-элемент.


    • destroy (index?: number): ViewRef
      Удаляет View-элемент из DOM

    Как работает createEmbeddedView мы видели уже в примерах выше, давайте теперь рассмотрим, как создавать View элементы, используя метод createComponent. Этот метод позволяет динамически создавать элементы на основе готовых компонентов. Но для начала нам нужно научиться находить фабрику нужного нам компонента и поможет нам в этом ComponentFactoryResolver. Я не буду тут описывать весь код создания компонента целиком, а сделаю ряд допущений.


    Во-первых, предположим, что у нас есть в проекте компонент Popover, который выглядит, например, так:


    @Component({
       selector: 'iw-popover',
       template: `
          <div class="popover popover-{{placement}}">
             <h3 class="popover-title">{{title}}</h3>
             <div class="popover-content">
                <ng-content></ng-content>
             </div>
          </div>
       `
    })
    export class Popover {
       @Input() placement: string;
       @Input() title: string;
    }

    и он добавлен в атрибут entryComponents нашего модуля. Последнее, кстати, важно, так как без добавления компонента в entryComponents ничего работать не будет, ангуляр просто не узнает о компоненте, потому что не встретит его в шаблонах.


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


    <ng-template #popoverContent>
       Popover content
    </ng-template>
    
    <button [popover]="popoverContent" title="Popover title" placement="right">
       Show popover
    </button>

    То есть наша директива получает на вход три параметра – ссылку на шаблон типа TemplateRef, значение заголовка и позицию, где надо показать popover.


    Исходя из указанных предположений, код нашей директивы, показывающей popover, будет выглядеть следующим образом:


    @HostListener('mouseover')
    show() {
       if (this._componentRef) {
          this._componentRef.destroy();
       }
    
       this._contentViewRef = this.popover.createEmbeddedView();
       const componentFactory = this._cfResolver.resolveComponentFactory(Popover);
       this._componentRef = this._vcRef.createComponent(
          componentFactory,
          this._injector,
          0,
          [this._contentViewRef.rootNodes]
       );
    
       this._componentRef.instance.placement = this.placement;
       this._componentRef.instance.title = this.title;
       this._contentViewRef.detectChanges();
    }
    
    @HostListener('mouseleave')
    hide() {
       if (this._componentRef) {
          this._componentRef.destroy();
          this._componentRef = null;
       }
    }
    
    constructor(
       private _injector: Injector,
       private _cfResolver: ComponentFactoryResolver
    ) {}

    Как вы видите, здесь есть два обработчика событий мышки на компоненте – один показывающий компонент и один удаляющий его из DOM. В коде, показывающем компонент, мы сначала создаем View на основе переданного нам шаблона, находим фабрику нашего компонента Popover и затем создаем его, передавая на вход фабрику компонента, инжектор, позицию и вложенный контент, который будет вставлен на место ng-content в компоненте Popover. Также, поскольку наш компонент динамический, то надо передать в него необходимые параметры и сказать Change Detector-механизму, что данные изменились.


    Все довольно несложно и, разобравшись в этом один раз, вы легко будете создавать компоненты. Казалось бы, мы научились создавать динамический контент на основе шаблонов и компонентов. И вроде все у нас есть для решения наших задач, но есть пара моментов, на которые я хотел бы обратить внимание.


    Во-первых, интересный факт в том, что ангуляр не вставляет View-элемент внутрь указанного контейнера, а добавляет его сразу после контейнера. Поэтому для вставки элементов в DOM удобно использовать ng-container элемент, который избавит нас от лишнего элемента в DOM. Лично для меня это было удивительным откровением, когда я начал отлаживать DOM-разметку и потратил кучу времени, чтобы понять, где ошибся, что мой элемент не вставляется внутрь.


    Во-вторых, динамически добавляемые компоненты не поддерживают Input- и Output-декораторы и это самая печаль. Для нас это выливается в то, что ngOnChanges метод компонента не будет вызываться, когда мы присваиваем новое значение Input переменным компонента. Из этой ситуации есть два выхода — использовать setter-метод для переменной компонента или контролировать перерисовку компонента вручную из компонента родителя. Можно еще использовать ngDoCheck и в нем самим сравнивать атрибуты.


    Но давайте усложним задачу и предположим, что мы хотим динамически создавать компонент, который находится в модуле, который лежит в отдельном файле на сервере. Прям «заяц в утке, утка в шоке». Но это еще не все, также мы хотим создавать наш компонент из сервиса, а не из существующего компонента. Итак, давайте разбираться по порядку как такое можно сделать.


    Первое, что нам нужно, это достать фабрику нужного компонента из модуля и создать экземпляр компонента. Для загрузки js файла с сервера я буду использовать SystemJS загрузчик. Также пусть нужный модуль экспортируется в переменной module из нашего JS файла. Ниже представлен код, который решает первую часть нашей задачи:


    (<any>window).System.import('module.js')
       .then((module: any) => module.module)
       .then((exportedModule: any) => this.compiler.compileModuleAndAllComponentsAsync(exportedModule))
       .then((moduleWithFactories: ModuleWithComponentFactories<any>) => {
          const factory = moduleWithFactories.componentFactories.find((component) => {
             return component.componentType.name === componentName
          });
    
          this.componentRef = this.content.createComponent(this.componentFactory, 0, this.injector);
       })

    В данном коде мы использовали JIT-компилятор ангуляра для того, чтобы скомпилировать загруженный модуль, и у данного подхода есть одна особенность. Так, если вы используете AOT-сборку вашего проекта, то компилятор будет не доступен во время выполнения приложения и указанный код работать не будет. Для решения данной проблемы можно создать отдельный модуль с сервисом, куда нужно будет вручную добавить компилятор, например, следующим образом:


    import {
       NgModule,
       ModuleWithProviders,
       Compiler,
       COMPILER_OPTIONS,
       CompilerOptions,
       Optional
    } from '@angular/core';
    import { JitCompilerFactory } from '@angular/compiler';
    
    export function createJitCompiler(options?: CompilerOptions[]) {
       options = options || [];
       return new JitCompilerFactory([{ useDebug: false, useJit: true }]).createCompiler(options);
    }
    
    @NgModule({
       ...
    })
    export class DynamicComponentModule {
       static forRoot(metadata: NgModule): ModuleWithProviders {
          return {
             ngModule: DynamicComponentModule,
             providers: [
                {
                   provide: Compiler, useFactory: createJitCompiler, deps: [Optional(), COMPILER_OPTIONS] 
                }
             ]
          }
       }
    }

    Теперь у нас есть компонент и нам осталось понять, как вставить его в DOM из сервиса. Ведь тут у нас нет ViewContainerRef, который мы использовали ранее. Для вставки компонента в DOM нам надо сделать две вещи — найти корневой элемент нашего приложения и вставить в него созданный View-компонент. И тут воспользуемся ссылкой на наше angular-приложение, которая доступна средствами DI через переменную с типом ApplicationRef. Для этого в конструктор нашего класса необходимо добавить ApplicationRef:


    constructor(
       private applicationRef: ApplicationRef,
       private injector: Injector
    ) {}

    Итак, для вставки нашего компонента, надо найти место для вставки View, а также не забыть скопировать в наш компонент нужные параметры как мы делали это раньше. Давайте напишем ряд вспомогательных функций для решения этих задач:


    private getRootViewContainer(): ComponentRef<any> {
       const rootComponents = this.applicationRef['_rootComponents'];
       if (rootComponents.length) {
          return rootComponents[0];
       }
    
       throw new Error('View Container not found!');
    }
    
    getComponentRootNode(componentRef: ComponentRef<any>): HTMLElement {
       return (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
    }
    
    projectComponentInputs(component: ComponentRef<any>, options: any): ComponentRef<any> {
       if (options) {
          const props = Object.getOwnPropertyNames(options);
          for (const prop of props) {
             component.instance[prop] = options[prop];
          }
       }
    
       return component;
    }

    Теперь соберем все вместе и получим следующий код, добавляющий наш компонент в DOM:


    let location: Element = this.getComponentRootNode(this.getRootViewContainer());
    
    let componentFactory = this.componentFactoryResolver.resolveComponentFactory(SomeComponent);
    let componentRef = componentFactory.create(this.injector);
    let appRef: any = this.applicationRef;
    let componentRootNode = this.getComponentRootNode(componentRef);
    
    const injector = ReflectiveInjector.resolveAndCreate([{
       provide: 'dialog', useValue: componentRef
    }], this.injector);
    
    this.projectComponentInputs(componentRef, {
       options: {
          ...
       },
       injector: injector
    });
    
    appRef.attachView(componentRef.hostView);
    
    componentRef.onDestroy(() => {
       appRef.detachView(componentRef.hostView);
       componentRef = null;
    });
    
    location.appendChild(componentRootNode);

    В данном коде есть пара моментов, которые надо пояснить. Кроме создания нашего компонента и поиска места для вставки его в DOM, мы также создали собственный Injector и добавили наш View компонента к приложению angular, вызвав метод attachView, чтобы он о нас знал и запускал ChangeDetector. Кроме этого, мы повесили обработчик на уничтожение компонента, в котором удаляем компонент из приложения.


    Итак, мы сделали код, который позволяет создавать динамически компонент, который лежит во внешнем файле, а также вставлять его в DOM из нашего сервиса. Данный код я использую в нашем продукте для создание модальных диалогов.


    Кстати, необязательно тянуть извне весь модуль целиком. Данная техника применима и в ситуациях, когда вы хотите создавать компоненты, имея только файл с содержимым шаблона. Например, у вас на сервере может лежать содержимое шаблона, и вы хотите на основе его создать View. Для этого вам нужно будет сделать следующее:


    • Создать модуль и добавить в него все используемые в шаблоне компоненты.
    • Создать компонент и указать загруженный шаблон.
    • Сделать то же, что мы делали в коде выше — скомпилировать полученный модуль, получить из него ваш компонент и вставить в DOM

    Вот пример кода, который решает данную задачу:


    private createComponentFromTemplateString(template: string) {
       @Component({
          selector: 'some-selector',
          template: template
       })
       class RuntimeComponent {}
    
       NgModule({
          imports: [imports],
          providers: [providers],
          declarations: [RuntimeComponent]
       })
       class RuntimeComponentModule {}
    
       this.compiler.compileModuleAndAllComponentsAsync(RuntimeComponentModule)
          .then((moduleWithFactories: ModuleWithComponentFactories<any>) => {
             ...
          })
    }

    Встроенные решения


    Ангуляр в последних версиях предоставляет две директивы, которые упрощают создание динамического контента — ngTemplateOutlet и ngComponentOutlet. Первая директива позволяет вам создавать DOM-элементы на основе готовых шаблонов, а вторая директива используется при создании полноценных компонентов. Давайте рассмотрим два примера как это происходит. Начнем с директивы ngTemplateOutlet:


    @Component({
       selector: 'some-component',
       template: `
          <ng-container *ngTemplateOutlet="greet"></ng-container>
          <ng-container *ngTemplateOutlet="eng; context: myContect"></ng-container>
          <ng-container *ngTemplateOutlet="svk; context: myContect"></ng-container>
          <hr>
          <ng-template #greet><span>Hello</span></ng-template>
          <ng-template #eng let-name><span>Hello {{name}}!</span></ng-template>
          <ng-template #svk let-person="localSk"><span>Ahoj {{person}}!</span></ng-template>
       `
    })
    class NgTemplateOutletExample {
       myContext = {$implicit: 'World', localSk: 'Svet'}
    }

    В данном примере будет вставлено три шаблона, в которые подставится значения, взятые из контекста. Ключевое слово $implicit определяет дефолтное значение переменной в случае отсутствия ее в контексте. По сути, данная директива под капотом просто делает вызов createEmbeddedView, используя переданный ей шаблон. Теперь рассмотрим директиву ngComponentOutlet.


    @Component({
       selector: 'some-component',
       template: `
          Hello World!
       `
    })
    class HelloWorldComponent {}
    
    @Component({
       selector: 'component-outlet-example',
       template: `
          <ng-container *ngComponentOutlet="HelloWorld"></ng-container>
       `
    })
    class ComponentOutletExample {
       HelloWorld = HelloWorldComponent
    }

    Данная директива под капотом работает аналогично рассмотренным нами вариантам создания компонентов и просто вызывает createComponent-метод, используя фабрику компонента.


    Итак, давайте подведем итоги. В новом ангуляре можно создавать динамический контент в различных вариантах: от простого создания DOM-элементов до формирования контента на основе готовых компонентов. Да кто-то скажет, что для этого надо написать довольно много кода, но тут в защиту могу сказать, что такой код пишется один раз и дальше просто используется. Как показывает мой опыт, другие фреймворки тоже не всегда дают простой способ создания, например, модальных окон, так что решение этой задачи в ангуляре для меня не было шокирующим, хотя и потребовало много времени.

    InfoWatch 65,48
    Компания
    Поделиться публикацией
    Похожие публикации
    Комментарии 17
      +3
      Ваша статья не содержит слов Service, Rx, EventEmittir, dispatch, emit, а это означает что читать её вредно, так как она может навредить не сформировавшимся личностям.
        0
        Допустил опечатку EventEmittir -> EventEmitter, но сути не меняет, просто уточнил.
        Хочу добавить, что для понятия angular, нужно научится понимать архитектуру приложения в виде слоев.
        Для примера в последнее время, очень часто о реакте пишут, что flux это тот же mvc, хотя flux это архитектурный слой view компонентов и ни о каком mvc речи там быть не может. Вот если взять связь между слоями-городами, то это будет либо железная дорога, либо самолеты. Но слой view, чем являются компоненты + логика которую они содержат, это слой-город. И это нужно понимать буквально, ведь то что поезда быстрее машин не означает что нам нужно отказаться от машин и добираться до соседнего квартала на самолете.
        Это первый шаг к постижения mvcs архитектуры, view у которой построена на компонентах (ещё может быть на слоях shape, но это другая история, я об этом сказал просто для справки).
        mvcs, это mvc + service. Да, это не прошлый век, а все ещё самая лучшая архитектура приложения и таковой она и будет до конца программирования в том виде, в котором мы его знаем сейчас. Все статьи, которые гласят что это не так, неправы, ведь они так же как и эта, ошибочны и все дело в том, что mvc пытаются засунуть не на тот слой.
          0
          Опять мысль упустил :) Я все это к чему, да к тому, что в service архитектуре вся логика приложения, а также логика компонентов выносится в сервисный слой, который в данной статье полностью отсутствует. А само создание компонента-popup или tooltip должно происходить в худшем случаи по вызову метода сервиса, а в лучшем по событию. Но ничего этого нет.
            0
            Статья построена по принципу «от простого к сложному» и если дочитать ее до конца, то можно увидеть пример где добавление компонента в DOM происходит из сервиса.
              0
              Да Вы правы, ознакомление с материалом я всегда начинаю с беглого просмотра и если что не читаю.
              И да Вы правы, у Вас есть упоминание о сервисе —
              Теперь у нас есть компонент и нам осталось понять, как вставить его в DOM из сервиса.

              После этой фразы я ещё раз хочу посоветовать не читать её неокрепшим умам. Если попробовать понять смысл этого предложения буквально, то мне представляется какая-то ужасная картина, приняв которую за истину можно сильно испортить психику. Возможно Вы и знаете как правильно писать приложения, но не умеете писать руководства. Сам такой, от переполняющих эмоций я не могу сформулировать мысль.
                0
                Теперь у нас есть компонент и нам осталось понять, как вставить его в DOM из сервиса.

                Здесь одно из двух — либо Вы хотите добавить компонент из сервиса, либо передать компонент при помощи сервиса. Оба варианта в корне неправильны, а подкрепление картинкой, которая гласит что с помощью angular простые вещи выглядят как недоразумение, это вообще ни в какие рамки не лезет.
          +1
          Любой создаваемый проект не обходится без динамического создания элементов. Рано или поздно вам понадобится либо создать tooltip для элемента, показать модальное окно, или вовсе сформировать некоторые блоки динамически подгружая их с сервера.

          Всегда считал это дичайшем костылем, когда часть интерфейса грузится с сервера.
            0
            А как делать плагинную систему по другому? В нашем проекте с сервера мы грузим плагины в виде ангуляровских модулей.
              0
              Забавно слышать слово плагин в контексте веб разработки, а именно разработки SPA приложений… Это вообще как?
                0
                Легко. Например у Вас есть enterprise приложение, которое расширяется плагинами. Пример такого — Jira от atlassian. У нас такая же ситуация — хочет клиент модуль отчетности — ставится сервис на бэк и плагин в GUI.
            0
            в первом примере нужно template вместо templateUrl
              0
              Спасибо, исправил.
              +1
              При решении таких задач я зачастую определяю зрелость фреймворка, который использую: насколько просто я могу в нем создавать динамический контент
              С учетом того, сколько кода нужно писать, я бы назвал ангуляр перезрелым.
                +1
                Я не хочу доказывать что лучше, а что хуже, но скажу так — я давно пишу на реакте + typescript и могу сказать не по наслышке, по объему кода angular не отличается от react. Те же компоненты, те же шаблоны, те же библиотеки.
                Все зависит от того, кто пишет код.
                  0
                  Тут уже были шаблонизируемые компоненты для angular 2, если сравнить объем кода: ~250 строк для Angular 2, и 30 строк для Angular Light.
                +1
                Что то подход оооочень смахивает на Jquery. Это ж не наш метод :-)
                  0
                  А кто-то сталкивался с ошибкой «createEmbeddedView is not a function» при попытке this.viewContainerRef.createEmbeddedView(this.tpl);?

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое