Эквивалент $ compile в Angular 2

134

Я хочу вручную скомпилировать HTML-код, содержащий директивы. Что эквивалентно $compileв Angular 2?

Например, в Angular 1 я мог динамически скомпилировать фрагмент HTML и добавить его в DOM:

var e = angular.element('<div directive></div>');
element.append(e);
$compile(e)($scope);
pixelbits
источник
8
Большинство этих ответов (кроме 1 теперь устаревшего) НЕ эквивалентны компиляции angular 1 $. $ compile принимает строку HTML и компилирует содержащиеся в ней компоненты и выражения. Эти ответы просто динамически создают предварительно определенные компоненты (которые еще не созданы) и НЕ МОГУТ принимать строковый аргумент. Это НЕ одно и то же. Кто-нибудь знает настоящий ответ на этот вопрос?
danday74
1
npmjs.com/package/p3x-angular-compile
Патрик Ласло
В Angular 4 появился ComponentFactoryResolver, который эквивалентен $ compile в Angular 1.0.
Code-EZ
1
@ danday74 - Я согласен с тем, что ни один из этих ответов не дает возможности компилировать произвольные шаблоны HTML, вместо этого они просто выбирают из набора уже существующих компонентов. Я нашел здесь настоящий ответ, который работает, по крайней мере, в Angular 8: stackoverflow.com/questions/61137899/… . Посмотрите один ответ, который предоставляет рабочий StackBlitz, который компилирует произвольный HTML-шаблон, созданный во время выполнения.
EbenH

Ответы:

132

Угловой 2.3.0 (07.12.2016)

Чтобы получить все подробности, проверьте:

Чтобы увидеть это в действии:

Принципы:

1) Создать шаблон
2) Создать компонент
3) Создать модуль
4) Скомпилировать модуль
5) Создать (и кэшировать) ComponentFactory
6) использовать Target для создания его экземпляра

Краткий обзор, как создать компонент

createNewComponent (tmpl:string) {
  @Component({
      selector: 'dynamic-component',
      template: tmpl,
  })
  class CustomDynamicComponent  implements IHaveDynamicData {
      @Input()  public entity: any;
  };
  // a component for this particular template
  return CustomDynamicComponent;
}

Способ внедрения компонента в NgModule

createComponentModule (componentType: any) {
  @NgModule({
    imports: [
      PartsModule, // there are 'text-editor', 'string-editor'...
    ],
    declarations: [
      componentType
    ],
  })
  class RuntimeComponentModule
  {
  }
  // a module for just this Type
  return RuntimeComponentModule;
}

Фрагмент кода, как создать ComponentFactory (и кэшировать)

public createComponentFactory(template: string)
    : Promise<ComponentFactory<IHaveDynamicData>> {    
    let factory = this._cacheOfFactories[template];

    if (factory) {
        console.log("Module and Type are returned from cache")

        return new Promise((resolve) => {
            resolve(factory);
        });
    }

    // unknown template ... let's create a Type for it
    let type   = this.createNewComponent(template);
    let module = this.createComponentModule(type);

    return new Promise((resolve) => {
        this.compiler
            .compileModuleAndAllComponentsAsync(module)
            .then((moduleWithFactories) =>
            {
                factory = _.find(moduleWithFactories.componentFactories
                                , { componentType: type });

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });
}

Фрагмент кода, как использовать результат выше

  // here we get Factory (just compiled or from cache)
  this.typeBuilder
      .createComponentFactory(template)
      .then((factory: ComponentFactory<IHaveDynamicData>) =>
    {
        // Target will instantiate and inject component (we'll keep reference to it)
        this.componentRef = this
            .dynamicComponentTarget
            .createComponent(factory);

        // let's inject @Inputs to component instance
        let component = this.componentRef.instance;

        component.entity = this.entity;
        //...
    });

Полное описание со всеми подробностями читайте здесь , или посмотрите рабочий пример

,

,

УСТАРЕЛО - связано с Angular 2.0 RC5 (только RC5)

чтобы увидеть предыдущие решения для предыдущих версий RC, пожалуйста, поищите в истории этого поста

Радим Келер
источник
2
Большое спасибо, я искал рабочий пример ComponentFactoryи ViewContainerRefзамену устаревшего DynamicComponentLoader.
Андре Локер
1
@maxou Это краткая ссылка в index.html, просто добавьте эту ссылку, и все будет работать
Radim Köhler
62
Неужели это так сложно? Раньше я мог просто делать что-то вроде этого: $compile($element.contents())($scope.$new());а теперь это сотни строк кода, дополненные созданием NgModule ... Это то, что заставляет меня держаться подальше от NG2 и перейти к чему-то лучшему.
Karvapallo
2
В чем преимущество использования, JitCompilerесли ваш пример может работать с Compilerот @angular/core? plnkr.co/edit/UxgkiT?p=preview
yurzui
4
Боже мой, сколько строк кода мне нужно написать, чтобы скомпилировать небольшой элемент. Я не очень хорошо понял
Mr_Perfect
35

Примечание: как @BennyBottema упоминает в комментарии, DynamicComponentLoader теперь устарел, следовательно, и этот ответ.


Angular2 не имеет эквивалента $ compile . Вы можете использовать DynamicComoponentLoaderи взламывать классы ES6 для динамической компиляции кода (см. Этот лист ):

import {Component, DynamicComponentLoader, ElementRef, OnInit} from 'angular2/core'

function compileToComponent(template, directives) {
  @Component({ 
    selector: 'fake', 
    template , directives
  })
  class FakeComponent {};
  return FakeComponent;
}

@Component({
  selector: 'hello',
  template: '<h1>Hello, Angular!</h1>'
})
class Hello {}

@Component({
  selector: 'my-app',
  template: '<div #container></div>',
})
export class App implements OnInit {
  constructor(
    private loader: DynamicComponentLoader, 
    private elementRef: ElementRef,
  ) {}

  ngOnInit() {} {
    const someDynamicHtml = `<hello></hello><h2>${Date.now()}</h2>`;

    this.loader.loadIntoLocation(
      compileToComponent(someDynamicHtml, [Hello])
      this.elementRef,
      'container'
    );
  }
}

Но он будет работать только до тех пор, пока html-парсер не окажется внутри ядра angular2.

alexpods
источник
Шикарный трюк! но в случае, если у моего динамического компонента есть некоторые входные данные, можно ли также связать динамические данные?
Евгений Глухоторенко 06
2
отвечая на мой собственный вопрос: это возможно, передав данные в функцию компиляции. вот plunk plnkr.co/edit/dK6l7jiWt535jOw1Htct?p=preview
Евгений Глухоторенко 06
Это решение работает только с beta-0. От бета-версии 1 до 15 пример кода возвращает ошибку. Ошибка: в элементе [object Object] нет директивы компонента
Николас Форни
13
Поскольку rc1 DynamicComponentLoader устарел
Бенни Боттема,
1
@BennyBottema DynamicComponentLoaderустарела, как мы делаем то же самое в Angular 2? Скажем, у меня есть модальное диалоговое окно, и я хочу динамически загружать новый компонент по мере его содержимого
Люк Т О'Брайен
16

Угловая версия, которую я использовал - Angular 4.2.0

В Angular 4 есть ComponentFactoryResolver для загрузки компонентов во время выполнения. Это своего рода реализация $ compile в Angular 1.0, которая удовлетворяет ваши потребности.

В этом примере ниже я загружаю компонент ImageWidget динамически в DashboardTileComponent.

Чтобы загрузить компонент, вам понадобится директива, которую вы можете применить к ng-template, которая поможет разместить динамический компонент.

WidgetHostDirective

 import { Directive, ViewContainerRef } from '@angular/core';

    @Directive({
      selector: '[widget-host]',
    })
    export class DashboardTileWidgetHostDirective {
      constructor(public viewContainerRef: ViewContainerRef) { 


      }
    }

эта директива внедряет ViewContainerRef, чтобы получить доступ к контейнеру представления элемента, в котором будет размещаться динамически добавляемый компонент.

DashboardTileComponent (компонент-заполнитель для визуализации динамического компонента)

Этот компонент принимает входные данные, поступающие от родительских компонентов, или вы можете загрузить их из своей службы в зависимости от вашей реализации. Этот компонент выполняет основную роль в разрешении компонентов во время выполнения. В этом методе вы также можете увидеть метод с именем renderComponent (), который в конечном итоге загружает имя компонента из службы и разрешает его с помощью ComponentFactoryResolver и, наконец, устанавливает данные для динамического компонента.

import { Component, Input, OnInit, AfterViewInit, ViewChild, ComponentFactoryResolver, OnDestroy } from '@angular/core';
import { DashboardTileWidgetHostDirective } from './DashbardWidgetHost.Directive';
import { TileModel } from './Tile.Model';
import { WidgetComponentService } from "./WidgetComponent.Service";


@Component({
    selector: 'dashboard-tile',
    templateUrl: 'app/tile/DashboardTile.Template.html'
})

export class DashboardTileComponent implements OnInit {
    @Input() tile: any;
    @ViewChild(DashboardTileWidgetHostDirective) widgetHost: DashboardTileWidgetHostDirective;
    constructor(private _componentFactoryResolver: ComponentFactoryResolver,private widgetComponentService:WidgetComponentService) {

    }

    ngOnInit() {

    }
    ngAfterViewInit() {
        this.renderComponents();
    }
    renderComponents() {
        let component=this.widgetComponentService.getComponent(this.tile.componentName);
        let componentFactory = this._componentFactoryResolver.resolveComponentFactory(component);
        let viewContainerRef = this.widgetHost.viewContainerRef;
        let componentRef = viewContainerRef.createComponent(componentFactory);
        (<TileModel>componentRef.instance).data = this.tile;

    }
}

DashboardTileComponent.html

 <div class="col-md-2 col-lg-2 col-sm-2 col-default-margin col-default">        
                        <ng-template widget-host></ng-template>

          </div>

WidgetComponentService

Это сервисная фабрика для регистрации всех компонентов, которые вы хотите разрешить динамически.

import { Injectable }           from '@angular/core';
import { ImageTextWidgetComponent } from "../templates/ImageTextWidget.Component";
@Injectable()
export class WidgetComponentService {
  getComponent(componentName:string) {
          if(componentName==="ImageTextWidgetComponent"){
              return ImageTextWidgetComponent
          }
  }
}

ImageTextWidgetComponent (компонент, который мы загружаем во время выполнения)

import { Component, OnInit, Input } from '@angular/core';


@Component({
    selector: 'dashboard-imagetextwidget',
    templateUrl: 'app/templates/ImageTextWidget.html'
})

export class ImageTextWidgetComponent implements OnInit {
     @Input() data: any;
    constructor() { }

    ngOnInit() { }
}

Добавить Наконец, добавьте этот ImageTextWidgetComponent в свой модуль приложения как entryComponent

@NgModule({
    imports: [BrowserModule],
    providers: [WidgetComponentService],
    declarations: [
        MainApplicationComponent,
        DashboardHostComponent,
        DashboardGroupComponent,
        DashboardTileComponent,
        DashboardTileWidgetHostDirective,
        ImageTextWidgetComponent
        ],
    exports: [],
    entryComponents: [ImageTextWidgetComponent],
    bootstrap: [MainApplicationComponent]
})
export class DashboardModule {
    constructor() {

    }
}

TileModel

 export interface TileModel {
      data: any;
    }

Оригинальная ссылка из моего блога

Официальная документация

Скачать образец исходного кода

Код-EZ
источник
1
Вы забыли упомянуть о entryComponents. Без него ваше решение не сработает
yurzui
ComponentFactoryResolver находился в angular2. И я думаю, что это не эквивалент $ compile
yurzui
@yurzui. Но это удовлетворяет потребность в компиляции $?
Code-EZ
@yurzui Я использовал такую ​​же реализацию с использованием $ compile. Когда мы удаляем компоненты записи из модуля, выдается ошибка: ImageTextWidgetComponent не загружен. Но приложение все еще работает
Code-EZ
1
@BecarioSenior, если вы не привязаны ни к какому классу модели, он будет динамическим по умолчанию. В этом примере тип данных свойства - любой, что означает, что вы можете передавать любые данные в динамический компонент в качестве входных. Это сделает ваш код более читабельным.
Code-EZ
9

этот пакет npm упростил мне задачу: https://www.npmjs.com/package/ngx-dynamic-template

использование:

<ng-template dynamic-template
             [template]="'some value:{{param1}}, and some component <lazy-component></lazy-component>'"
             [context]="{param1:'value1'}"
             [extraModules]="[someDynamicModule]"></ng-template>
aelbatal
источник
3

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

html шаблон:

<div>
  <div id="container"></div>
  <button (click)="viewMeteo()">Meteo</button>
  <button (click)="viewStats()">Stats</button>
</div>

Компонент загрузчика

import { Component, DynamicComponentLoader, ElementRef, Injector } from '@angular/core';
import { WidgetMeteoComponent } from './widget-meteo';
import { WidgetStatComponent } from './widget-stat';

@Component({
  moduleId: module.id,
  selector: 'widget-loader',
  templateUrl: 'widget-loader.html',
})
export class WidgetLoaderComponent  {

  constructor( elementRef: ElementRef,
               public dcl:DynamicComponentLoader,
               public injector: Injector) { }

  viewMeteo() {
    this.dcl.loadAsRoot(WidgetMeteoComponent, '#container', this.injector);
  }

  viewStats() {
    this.dcl.loadAsRoot(WidgetStatComponent, '#container', this.injector);
  }

}
fabio_biondi
источник
1
DynamicComponentLoader больше не существует: '(После того, как это устарело, появился ComponentResolver. А теперь есть ComponentFactoryResolver ( blog.rangle.io/dynamically-creating-components-with-angular-2 )
11
3

Угловой TypeScript / ES6 (Angular 2+)

Работает сразу с AOT + JIT вместе.

Я создал, как его использовать здесь: https://github.com/patrikx3/angular-compile

npm install p3x-angular-compile

Компонент: должен иметь контекст и некоторые данные html ...

Html:

<div [p3x-compile]="data" [p3x-compile-context]="ctx">loading ...</div>
Патрик Ласло
источник
1
Не совсем понятно, что означает название «Angular TypeScript». Решение бесполезно для ES5 и ES6? Было бы полезно привести пример программного использования этого пакета, прямого аналога $compile(...)($scope). Даже в repo readme ничего нет.
Estus Flask
2

Вы можете увидеть компонент, который позволяет компилировать простые динамические компоненты Angular https://www.npmjs.com/package/@codehint-ng/html-compiler

Василий Мажекин
источник
он работал с этой библиотекой! :) спасибо за рекомендацию.
Хусем Члегоу,
0

Я знаю, что это старая проблема, но я потратил несколько недель, пытаясь понять, как заставить ее работать с включенным AOT. Я смог скомпилировать объект, но не смог выполнить существующие компоненты. Что ж, я наконец решил изменить тактичность, так как я хотел не столько компилировать код, сколько выполнить собственный шаблон. Моя мысль заключалась в том, чтобы добавить html, который может сделать каждый, и зацикливать существующие фабрики. При этом я могу искать элемент / атрибут / и т. Д. имена и выполнить компонент на этом HTMLElement. Я смог заставить его работать и решил, что должен поделиться этим, чтобы сэкономить кому-то огромное количество времени, которое я потратил на это.

@Component({
    selector: "compile",
    template: "",
    inputs: ["html"]
})
export class CompileHtmlComponent implements OnDestroy {
    constructor(
        private content: ViewContainerRef,
        private injector: Injector,
        private ngModRef: NgModuleRef<any>
    ) { }

    ngOnDestroy() {
        this.DestroyComponents();
    }

    private _ComponentRefCollection: any[] = null;
    private _Html: string;

    get Html(): string {
        return this._Html;
    }
    @Input("html") set Html(val: string) {
        // recompile when the html value is set
        this._Html = (val || "") + "";
        this.TemplateHTMLCompile(this._Html);
    }

    private DestroyComponents() { // we need to remove the components we compiled
        if (this._ComponentRefCollection) {
            this._ComponentRefCollection.forEach((c) => {
                c.destroy();
            });
        }
        this._ComponentRefCollection = new Array();
    }

    private TemplateHTMLCompile(html) {
        this.DestroyComponents();
        this.content.element.nativeElement.innerHTML = html;
        var ref = this.content.element.nativeElement;
        var factories = (this.ngModRef.componentFactoryResolver as any)._factories;
        // here we loop though the factories, find the element based on the selector
        factories.forEach((comp: ComponentFactory<unknown>) => {
            var list = ref.querySelectorAll(comp.selector);
            list.forEach((item) => {
                var parent = item.parentNode;
                var next = item.nextSibling;
                var ngContentNodes: any[][] = new Array(); // this is for the viewchild/viewchildren of this object

                comp.ngContentSelectors.forEach((sel) => {
                    var ngContentList: any[] = new Array();

                    if (sel == "*") // all children;
                    {
                        item.childNodes.forEach((c) => {
                            ngContentList.push(c);
                        });
                    }
                    else {
                        var selList = item.querySelectorAll(sel);

                        selList.forEach((l) => {
                            ngContentList.push(l);
                        });
                    }

                    ngContentNodes.push(ngContentList);
                });
                // here is where we compile the factory based on the node we have
                let component = comp.create(this.injector, ngContentNodes, item, this.ngModRef);

                this._ComponentRefCollection.push(component); // save for our destroy call
                // we need to move the newly compiled element, as it was appended to this components html
                if (next) parent.insertBefore(component.location.nativeElement, next);
                else parent.appendChild(component.location.nativeElement);

                component.hostView.detectChanges(); // tell the component to detectchanges
            });
        });
    }
}
Винс
источник
-5

Если вы хотите ввести html-код, используйте директиву

<div [innerHtml]="htmlVar"></div>

Если вы хотите загрузить весь компонент в какое-то место, используйте DynamicComponentLoader:

https://angular.io/docs/ts/latest/api/core/DynamicComponentLoader-class.html

Владо Тесанович
источник
2
Я хочу вставить фрагмент HTML в виде строки и передать его компилятору компонентов, а затем добавить этот компонент в мою DOM. Можете ли вы привести пример того, как может работать любое из ваших решений?
pixelbits
3
использование innerHtml не компилирует какие-либо компоненты внутри htmlVar
Хуанин,