[Перевод] Изучение использования ViewContainerRef в Angular для управления DOM

Node.js

Оригинальная ссылка:Exploring Angular DOM manipulation techniques using ViewContainerRef

Если вы хотите узнать больше о том, как Angular использует технологии Renderer и View Containers для управления DOM, вы можете посмотреть это видео на YouTube.my talk at NgVikings.

Каждый раз, когда я читаю статьи о том, как Angular манипулирует DOM, я всегда нахожу в этих статьях упоминаниеElementRef,TemplateRef,ViewContainerRefи другие классы. Хотя эти классы описаны в официальной документации Angular или связанных статьях, общая идея редко описывается, и есть несколько соответствующих примеров того, как эти классы работают вместе, и эта статья в основном описывает их.

Если вы пришли из мира angular.js, легко понять, как манипулировать DOM с помощью angular.js. angular.js будет вlinkВнедрить DOM в функциюelement, вы можете запрашивать любой узел в шаблоне компонента, добавлять или удалять узлы, изменять стили и т. д. Однако у этого подхода есть существенный недостаток:Тесно связан с браузерной платформой.

Новая версия Angular должна работать на разных платформах, таких как платформа браузера, мобильная платформа или платформа веб-воркеров, поэтому требуется уровень абстракции между API-интерфейсом для конкретной платформы и интерфейсом фреймворка. Этот уровень абстракции в Angular состоит из следующих ссылочных типов:ElementRef,TemplateRef,ViewRef,ComponentRefа такжеViewContainerRef. В этой статье подробно объясняется каждый ссылочный тип и то, как этот ссылочный тип манипулирует DOM.

@ViewChild

Прежде чем изучать абстрактные классы DOM, давайте узнаем, как получить их в компонентах/директивах. Angular предоставляет технологию под названием DOM Query, в основном производную от@ViewChildа также@ViewChildrenДекораторы. Основные функции у них одинаковые, разница только в@ViewChildвозвращает одну ссылку,@ViewChildrenвернулсяQueryListОбъект оборачивает несколько ссылок. В этом примере основнойViewChildпример и исключен из описания@.

Обычно эти два декоратора связаны со ссылочными переменными шаблона (template reference variable) вместе ссылочная переменная шаблона представляет собой просто именованную ссылку на элемент DOM в шаблоне, подобноhtmlэлементальidАтрибуты. Вы можете пометить элемент DOM ссылкой на шаблон и использовать его в классе компонента/директивы.ViewChildДекоратор запрашивает его, например:

@Component({
    selector: 'sample',
    template: `
        <span #tref>I am span</span>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("tref", {read: ElementRef}) tref: ElementRef;

    ngAfterViewInit(): void {
        // outputs `I am span`
        console.log(this.tref.nativeElement.textContent);
    }
}

ViewChildОсновной синтаксис декоратора:

@ViewChild([reference from template], {read: [reference type]});

Как вы можете видеть в приведенном выше примере, я поставилtrefв качестве ссылочного имени шаблона и поместитеElementRefсвязанные с этим элементом. второй параметрreadявляется необязательным, потому что Angular выводит ссылочный тип на основе типа элемента DOM. Например, если он (#tref) монтирует что-то вродеspanПростой HTML-элемент, который возвращает Angular.ElementRef; если он монтируетсяtemplateэлемент, возвращаемый AngularTemplateRef. Некоторые ссылочные типы, такие какViewContainerRefне может быть выведен Angular, поэтому он должен быть вreadЯвно объявлено в параметре. другие, такие какViewRefЕго нельзя смонтировать в DOM-элементе, поэтому его нужно кодировать вручную и создавать в конструкторе.

Теперь давайте рассмотрим, как получить эти цитаты.

ElementRef

Это самый простой абстрактный класс, если вы посмотрите на его структуру класса, он содержит только объект смонтированного элемента, который полезен для доступа к собственным элементам DOM, таким как:

// outputs `I am span`
console.log(this.tref.nativeElement.textContent);

Однако,Команда Angular не одобряет эту нотацию, не только потому, что этот подход подвергает риску безопасность, но и тесно связывает вашу программу со слоями рендеринга, что затрудняет запуск вашей программы на нескольких платформах. думаю проблема не в использованииnativeElementВместо этого это вызвано использованием определенного API DOM, такого какtextContent. Но, как вы увидите позже, Angular реализует общую идею управления DOM, так что низкоуровневые API, такие какtextContent.

использоватьViewChildУкрашенный элемент DOM вернетсяElementRef, но поскольку все компоненты монтируются в пользовательские элементы DOM, а все инструкции действуют на элементы DOM, и компоненты, и инструкции могут получать информацию об элементе хоста через DI (внедрение зависимостей).ElementRefобъект. Например:

@Component({
    selector: 'sample',
    ...
export class SampleComponent{
	  constructor(private hostElement: ElementRef) {
          //outputs <sample>...</sample>
   		  console.log(this.hostElement.nativeElement.outerHTML);
	  }
	...

Таким образом, компонент может получить доступ к своему хост-элементу через DI (внедрение зависимостей), ноViewChildДекораторы часто используются для получения элементов DOM в представлениях шаблонов. Однако с инструкцией все обстоит наоборот: поскольку у инструкции нет шаблона представления, она в основном используется для получения смонтированного инструкцией хост-элемента.

TemplateRef

Концепция шаблона знакома большинству разработчиков и представляет собой набор элементов DOM в представлении для нескольких приложений. Представлено в HTML5templateвкладку, браузер передаетscriptНастройки во вкладкеtypeатрибуты для импорта шаблонов, такие как:

<script id="tpl" type="text/template">
  <span>I am span in template</span>
</script>

Этот подход не только семантически ошибочен, но и требует ручного создания модели DOM.templateтеги, которые браузеры могут анализироватьhtmlи создатьDOMдерево, но не будет отображать его, доступ к дереву DOM можно получить черезcontentДоступ к атрибутам, например:

<script>
    let tpl = document.querySelector('#tpl');
    let container = document.querySelector('.insert-after-me');
    insertAfter(container, tpl.content);
</script>
<div class="insert-after-me"></div>
<ng-template id="tpl">
    <span>I am span in template</span>
</ng-template>

Угловой принимаетtemplateТаким образом, этикетка реализуетTemplateRefабстрактные классы приходят иtemplateпометьте вместе, чтобы увидеть, как он используется:

@Component({
    selector: 'sample',
    template: `
        <ng-template #tpl>
            <span>I am span in template</span>
        </ng-template>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("tpl") tpl: TemplateRef<any>;

    ngAfterViewInit() {
        let elementRef = this.tpl.elementRef;
        // outputs `template bindings={}`
        console.log(elementRef.nativeElement.textContent);
    }
}

Фреймворк Angular удален из DOMtemplateэлемент и вставляем на его место комментарий, вот как это выглядит после рендеринга:

<sample>
    <!--template bindings={}-->
</sample>

TemplateRefэто абстрактный класс с простой структурой, егоelementRefАтрибут — это ссылка на его основной элемент, аcreateEmbeddedViewметод. ОднакоcreateEmbeddedViewметод полезен, потому что он создает представление и возвращает ссылку на это представлениеViewRef.

ViewRef

Эта абстракция представляет собой представление Angular (View), в мире Angular представление (View) представляет собой комбинацию кучи элементов, созданных и уничтоженных вместе, является краеугольным камнем построения пользовательского интерфейса программы. Angular побуждает разработчиков думать о пользовательском интерфейсе как о наборе представлений, а не просто о дереве html-тегов.

Angular поддерживает два типа представлений:

  • Встроенный вид, авторTemplateпоставка
  • Просмотр хоста, поComponentпоставка

Создание встроенного представления

Шаблоны — это просто чертежи для представлений, доступ к которым можно получить через ранее упомянутыйcreateEmbeddedViewметод для создания представления, например:

ngAfterViewInit() {
    let view = this.tpl.createEmbeddedView(null);
}

Создать представление хоста

Представление хоста создается при динамическом создании экземпляра компонента, динамический компонент может бытьComponentFactoryResolverСоздайте:

constructor(private injector: Injector,
            private r: ComponentFactoryResolver) {
    let factory = this.r.resolveComponentFactory(ColorComponent);
    let componentRef = factory.create(injector);
    let view = componentRef.hostView;
}

В Angular каждый компонент привязан к экземпляру инжектора, поэтому создайтеColorComponentИнжектор текущего компонента (т.е. SampleComponent) передается, когда компонент. Кроме того, не забывайте, что при динамическом создании компонентов вам необходимо создать компонент в модуле (модуле) или хост-компонент.EntryComponentsсвойство для добавления к созданному компоненту.

Теперь, когда мы увидели, как создаются встроенное представление и основное представление, после создания представления его можно использовать.ViewContainerВставьте в дерево DOM. Эта функция в основном исследуется ниже.

ViewContainerRef

Контейнер представления — это контейнер, который монтирует одно или несколько представлений.

Первое, что нужно сказать, это то, что любой элемент DOM может быть контейнером представления, как бы интересно это ни было, для привязкиViewContainerDOM-элемент, Angular не вставляет представление внутрь элемента, а добавляет представление после элемента, что похоже наrouter-outletКак вставлять компоненты.

Обычно лучше поставитьViewContainerсвязаны вng-containerэлемент, потому чтоng-containerЭлементы отображаются как комментарии, поэтому в DOM не добавляются дополнительные элементы HTML. В следующем примере показано, как создать в шаблоне компонентаViewContainer:

@Component({
    selector: 'sample',
    template: `
        <span>I am first span</span>
        <ng-container #vc></ng-container>
        <span>I am last span</span>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;

    ngAfterViewInit(): void {
        // outputs `template bindings={}`
        console.log(this.vc.element.nativeElement.textContent);
    }
}

Как и другие абстрактные классы,ViewContainerпройти черезelementАтрибуты привязаны к элементам DOM. Например, в приведенном выше примере привязка будет отображаться как комментарий.ng-containerэлементы, поэтому вывод также будетtemplate bindings={}.

Просмотр действий

ViewContainerПредоставляются некоторые API представления действий:

class ViewContainerRef {
    ...
    clear() : void
    insert(viewRef: ViewRef, index?: number) : ViewRef
    get(index: number) : ViewRef
    indexOf(viewRef: ViewRef) : number
    detach(index?: number) : ViewRef
    move(viewRef: ViewRef, currentIndex: number) : ViewRef
}

Выше мы видели, как создавать два типа представлений с помощью шаблонов и компонентов, а именно встроенные представления и представления компонентов. Как только у вас есть представление, вы можете пройтиinsertметод вставляется в DOM. В следующем примере показано, как создать встроенное представление из шаблона иng-containerВставьте вид в отмеченное местоng-containerпозже, а не вставлять внутрь этого элемента DOM).

@Component({
    selector: 'sample',
    template: `
        <span>I am first span</span>
        <ng-container #vc></ng-container>
        <span>I am last span</span>
        <ng-template #tpl>
            <span>I am span in template</span>
        </ng-template>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;
    @ViewChild("tpl") tpl: TemplateRef<any>;

    ngAfterViewInit() {
        let view = this.tpl.createEmbeddedView(null);
        this.vc.insert(view);
    }
}

С приведенной выше реализацией окончательныйhtmlЭто выглядит как:

<sample>
    <span>I am first span</span>
    <!--template bindings={}-->
    <span>I am span in template</span>

    <span>I am last span</span>
    <!--template bindings={}-->
</sample>

в состоянии пройтиdetachМетод удаляет DOM из представления, и другие методы могут узнать его значение по имени метода, например, получить объект ссылки на представление по индексу, переместить позицию представления или удалить все представления из контейнера представления.

Создать представление

ViewContainerТакже предоставляется API для ручного создания представления:

class ViewContainerRef {
    element: ElementRef
    length: number

    createComponent(componentFactory...): ComponentRef<C>
    createEmbeddedView(templateRef...): EmbeddedViewRef<C>
    ...
}

Вышеупомянутые два метода являются хорошей инкапсуляцией, вы можете передать объект ссылки на шаблон или объект фабрики компонентов, чтобы создать представление и вставить представление в определенную позицию в контейнере представления.

нгтемплатаутлет и нгкомпонентаутлет

Хотя хорошо знать внутренности манипуляций Angular с DOM, было бы неплохо иметь какой-то ярлык. Да, Angular предоставляет два ярлыка:ngTemplateOutletа такжеngComponentOutlet. Обе директивы являются экспериментальными на момент написания статьи.ngComponentOutletТакже будет доступна в версии 4. Если вы читали вышеизложенное, легко увидеть, что делают эти две инструкции.

ngTemplateOutlet

Эта директива пометит элемент DOM какViewContainerи вставляет встроенное представление, созданное шаблоном, устраняя необходимость явного создания встроенного представления в классе компонента. Таким образом, в приведенном выше примере для создания встроенного представления и вставки#vcЗатем код для элемента DOM можно переписать:

@Component({
    selector: 'sample',
    template: `
        <span>I am first span</span>
        <ng-container [ngTemplateOutlet]="tpl"></ng-container>
        <span>I am last span</span>
        <ng-template #tpl>
            <span>I am span in template</span>
        </ng-template>
    `
})
export class SampleComponent {}

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

ngComponentOutlet

Эта команда связана сngTemplateOutletочень похоже, разница естьngComponentOutletСоздается основное представление, созданное путем создания экземпляра компонента, а не встроенное представление. Вы можете использовать его следующим образом:

<ng-container *ngComponentOutlet="ColorComponent"></ng-container>

Суммировать

Кажется, что нужно переварить много новых знаний, но на самом деле модель мышления Angular по манипулированию DOM через представления очень четкая и последовательная. вы можете использоватьViewChildЗапросите ссылочную переменную шаблона, чтобы получить абстрактный класс Angular DOM. Простейшая оболочка для элемента DOM:ElementRef; в то время как для шаблонов вы можете использоватьTemplateRefдля создания встроенных представлений; для компонентов вы можете использоватьComponentRefдля создания основного представления, и в то же время вы можете использоватьComponentFactoryResolverСоздайтеComponentRef. Два созданных представления (то есть встроенное представление и основное представление) будутViewContainerRefуправлять. Наконец, Angular предоставляет еще два ярлыка для автоматизации этого процесса:ngTemplateOutletДирективы используют шаблоны для создания встроенных представлений;ngComponentOutletИспользуйте динамические компоненты для создания основных представлений.