Зачастую, когда я наталкиваюсь на информацию о работе с DOM в Angular, в них присутствуют упоминания об одном или нескольких из этих классов: ElementRef, TemplateRef, ViewContainerRef. Именно упоминания, ведь общее  представление достаточно сложно сложить, даже тщательно изучив документацию Angular. Поэтому я решил подробно сформулировать, как это работает и для чего нужно.

Когда-то давно, как может показаться сейчас, была такая вещь как AngularJs. Нам он сейчас нужен лишь для воспоминаний о том, что различные манипуляции с DOM давались разработчикам довольно легко. Фреймворк внедрил элемент DOM в функцию link, и возможно было запрашивать любой узел в шаблоне компонента, добавлять или удалять дочерние узлы и т.д. Но ложкой дегтя была тесная привязанность к платформе браузера.

Новый же Angular способен работать на разных платформах – в браузере, на мобилке или же внутри web worker. Для реализации такой возможности необходим определенный уровень абстракции между API конкретной платформы и интерфейсами фреймворка. Так и появились следующие ссылочные типы:

  • ElementRef;

  • TemplateRef;

  • ViewRef;

  • ComponentRef;

  • ViewContainerRef.

Давайте взглянем на каждый из них и разберемся как можно манипулировать DOM.

Получение доступа к абстракциям

Для начала нужно понять, как мы получаем доступ к этим абстракциям внутри компонента или директивы. Angular имеет механизм DOM-запросов. Этот механизм поставляется в виде декораторов @ViewChild, @ViewChildren.

Они, по сути, одинаковы, только первый возвращает одну ссылку, а второй несколько в виде объекта QueryList. В дальнейшем будем использовать ViewChild и уберем символ @, чтобы меньше на него отвлекаться.

Обычно эти декораторы работают в связке с ссылочными переменными шаблона. Эти переменные ни что иное как просто именованная ссылка на элемент DOM в шаблоне. Элемент DOM помечается ссылкой на шаблон, а потом его можно запрашивать внутри класса с помощью ViewChild.

Основной синтаксис ViewChild decorator следующий:

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

Основной пример использования декоратора:

@Component({
    selector: 'example',
    template: `
        <span #reference>Я простой span!</span>
    `
})

export class ExampleComponent implements AfterViewInit {
    @ViewChild("reference", {read: ElementRef}) reference: ElementRef;

    ngAfterViewInit(): void {
        // Вывод: `Я простой span!`
        console.log(this.reference.nativeElement.textContent);
    }
}

В примере можно увидеть, что reference указана в качестве ссылочного имени шаблона и получена как ElementRef в компоненте. Второй параметр не всегда необходим, так как Angular может определять ссылочный тип по типу элемента DOM. Например, если это простой html-элемент, то вернется ElementRef, если элемент шаблона, то TemplateRef. Некоторые ссылки как ViewContainerRef не могут быть выведены и должны запрашиваться в параметре read.

Итак, теперь мы знаем, как запрашивать ссылки, и можем приступить к их изучению.

Класс ElementRef

Это самая базовая абстракция. Если присмотреться к структуре этого класса, мы увидим, что тут есть только собственный элемент, с которым он связан. Это может быть полезно, когда необходимо получить доступ к собственному элементу DOM.

// Вывод `Я простой span!`
console.log(this.tref.nativeElement.textContent);

Однако это является примером плохой практики, так как создает угрозу безопасности, а помимо этого появляется тесная связь между приложением и слоями рендеринга, которая затруднит запуск приложения на нескольких платформах. Дело тут скорее не в nativeElement, а в определенном DOM API, таком как textContent.

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

ElementRef может быть возвращен для любого элемента DOM при помощи ViewChild, но, так как все компоненты размещены внутри пользовательского элемента DOM и все директивы применяются к элементам DOM, классы компонентов и директив могут получать ElementRef, связанный с их основным элементом через механизм DI(Dependency Injection):

@Component({
    selector: 'example',
    ...
})

export class ExampleComponent {
    constructor(private hostElement: ElementRef) {
        //Вывод < example >...</ example >
        console.log(this.hostElement.nativeElement.outerHTML);
    }
}

Хотя компонент может получить доступ к своему элементу host через DI, ViewChild намного чаще используется для получения ссылки на элемент DOM в их представлении (шаблоне). С директивами всё наоборот – у них нет представлений и они работают с элементом, к которому подключены.

Класс TemplateRef

Понятие template должно быть знакомо большинству веб-разработчиков. Это группа элементов, которые повторно используются в представлении по всему приложению.

Angular использует этот подход и реализует класс TemplateRef для работы с шаблонами. Вот базовый пример:

@Component({
    selector: 'example',
    template: `
        <template #template_1>
            <span>Я span внутри template</span>
        </template>
    `
})
export class ExampleComponent implements AfterViewInit {
    @ViewChild("template_1") template_1: TemplateRef<any>;

    ngAfterViewInit() {	
        let elementRef = this.template_1.elementRef;
        // Вывод `template bindings={}`
        console.log(elementRef.nativeElement.textContent);
    }
}

При рендеринге элемент шаблона удаляется из DOM и на его место помещается комментарий.

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

TemplateRef также является простым классом, помимо ссылки на host элемент в свойстве ElementRef, он еще имеет метод createEmbeddedView. Этот метод очень полезен, поскольку позволяет создавать представление и возвращает ссылку на него как ViewRef.

Класс ViewRef

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

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

  • Embedded Views (Встроенные представления), которые связаны с шаблоном

  • Host Views (Представления хоста), которые связаны с компонентом

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

Шаблон просто содержит схему представления. Оно может быть создано из шаблона с использованием createEmbeddedView.

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

Создание представления хоста (host view)

Представления хоста создаются при динамическом создании экземпляра компонента. Это может быть сделано с помощью ComponentFactoryResolver:

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

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

Подытожим по этой части. Мы рассмотрели, как можно создавать встроенные представления и представления хоста. Как только представление создано, его можно вставить в DOM с помощью ViewContainer.

Класс ViewContainerRef

Представляет контейнер, к которому можно прикрепить одно или несколько представлений.

Для начала стоит упомянуть, что любой элемент может использоваться как контейнер представления. Есть интересная особенность, которая заключается в том, что представления вставляются не внутри элемента, привязанного к ViewContainer, а добавляются после него.

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

@Component({
    selector: 'example',
    template: `
        <span>Я первый</span>
        <ng-container #vc></ng-container>
        <span>А я последний</span>
    `
})
export class ExampleComponent implements AfterViewInit {
    @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;

    ngAfterViewInit(): void {
        // Вывод `template bindings={}`
        console.log(this.vc.element.nativeElement.textContent);
    }
}

ViewContainer привязан к определенному элементу DOM, доступ к которому осуществляется через свойство element. В примере выше элемент будет отрендерен как комментарий, и поэтому привязка шаблона = {}

Манипулирование представлениями

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
}

Мы уже видели, как два типа представлений могут быть созданы вручную из шаблона и компонента. Как только у нас будет представление, мы сможем вставить его в DOM с помощью метода insert.

@Component({
    selector: 'example',
    template: `
        <span>Я первый</span>
        <ng-container #vc></ng-container>
        <span>А я последний</span>
        <template #template>
            <span>Я внутри шаблона</span>
        </template>
    `
})
export class ExampleComponent implements AfterViewInit {
    @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;
    @ViewChild("template ") tpl: TemplateRef<any>;

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

В результате html код будет выглядеть так:

<example>
    <span>Я первый</span>
    <!--template bindings={}-->
    <span>Я внутри шаблона</span>

    <span>А я последний</span>
    <!--template bindings={}-->
</example>

Для удаления представления из DOM мы можем использовать метод detach. Остальные методы класса ViewContainerRef довольно очевидны, они могут использоваться для получения ссылки на представление по индексу, перемещения представления в другое место и удаления всех представлений из контейнера.

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

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

class ViewContainerRef {
    element: ElementRef
    length: number

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

Это просто удобные оболочки к тому, что мы сделали вручную выше. Они создают представление из шаблона или компонента и вставляют его в указанное местоположение.

ngTemplateOutlet и ngComponentOutlet

Всегда полезно знать и понимать работу механизма изнутри, но здорово еще и иметь возможность упростить ручной труд разработчика. Директивы ngTemplateOutlet и ngComponentOutlet как раз-таки это и делают.

ngTemplateOutlet

Эта директива помечает элемент DOM как ViewContainer и вставляет в него встроенное представление, созданное шаблоном, без необходимости лезть в класс компонента и делать это там. Приведенный выше пример, где мы создали view и вставили его в элемент vc, можно переписать так:

@Component({
    selector: 'example',
    template: `
        <span>Я первый</span>
        <ng-container [ngTemplateOutlet]="template"></ng-container>
        <span>А я последний</span>
        <template #template >
            <span>Я внутри шаблона</span>
        </template>
    `
})
export class ExampleComponent {}

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

ngComponentOutlet

Эта директива аналогична ngTemplateOutlet с той разницей, что она создает представление хоста (создает экземпляр компонента), а не встроенное представление.

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

Подведение итогов

На первый взгляд этой информации многовато для усвоения, но на деле она последовательно описывает модель управления DOM с помощью представлений. Вы получаете ссылку на абстракции Angular DOM с помощью ViewChild вместе со ссылками на переменные шаблона. Простейшая оболочка для элемента – это ElementRef, для шаблона есть TemplateRef, а к представлениям хоста можно получить доступ на ComponentRef, созданного с помощью ComponentFactoryResolver. Представлениями можно манипулировать с помощью ViewContainerRef либо в коде компонента, используя встроенные методы такие как insert, detach, или же с помощью директив ngTemplateOutlet и ngComponentOutlet.

Комментарии (0)