使用 ViewContainerRef 探索Angular DOM操做

英文原版:
Exploring Angular DOM manipulation techniques using ViewContainerRefhtml

_翻译:giscafer
说明:根据我的理解翻译,不彻底词词对应。_git


每当我读到关于使用Angular DOM的操做时,我老是会看到其中的一个或几个类: ElementRef, TemplateRef, ViewContainerRef等。遗憾的是,尽管Angular文档或相关文章当中提到这三者的一些内容,但我尚未发现关于这三者如何协做的完整的理想模型和示例的描述。本文旨在描述这种模型。github

若是你学习过angular.js的话,你就会知道在angular.js中很容易去操做DOM。Angular注入DOM elementlink 函数中,你能够查询组件模板内的任何节点,添加或删除子节点,修改样式等等。然而,这种方法有一个主要缺点——它被牢牢绑定到一个浏览器平台上(意思是脱离浏览器就不能玩了)。web

新的 Angular 版本运行在不一样的平台上——在浏览器上,在移动平台上,或者在 web worker 中。所以,须要在平台特定API 和框架接口之间进行抽象级别的抽象。从 Angular 来看,这些抽象的形式有如下的参考类型: ElementRef, TemplateRef, ViewRef, ComponentRefViewContainerRef。在本文中,咱们将详细介绍每一个引用类型,并展现如何使用它们来操做DOM。api

@ViewChild

在探索DOM抽象以前,让咱们了解一下如何在组件/指令类( component/directive class)中访问这些抽象。Angular 提供了一个称为DOM查询的机制。它以 @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 decorator 的基本语法以下:框架

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

在这个示例中,您能够看到,我将 tref 指定为 html 中的模板引用名称,并接收与此元素关联的
ElementRef 。第二个参数 read 并不老是必需的,由于 Angular 能够经过DOM元素的类型推断引用类型。例如,若是它是一个简单的 html 元素,好比 span,Angular 返回 ElementRef。若是它是一个 template 模板,它将返回 TemplateRef 。一些引用,如 ViewContainerRef 不能被推断,而且必须在
read 参数中被声明。其余的,如 ViewRef 不能从 DOM 接收返回,必须手动构造。dom

好了,如今咱们知道了如何查询引用,让咱们开始探索它们。angular4

ElementRef

这是最基本的抽象概念。若是您观察它的类结构,您将看到它只包含与之关联的原生元素(native element)。它对于访问原生DOM元素很是有用,正如咱们在这里看到的:

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

然而,这种用法却被 Angular 团队 所劝阻。它不只会带来安全风险,并且还会在应用程序和呈现层之间产生紧密耦合,使得在多个平台上运行应用程序变得困难。我认为,它不是访问 nativeElement 来打破抽象,而是使用特定的DOM API,好比 textContent 。可是,稍后您将看到,在 Angular 上实现的DOM操做思想模型几乎不须要这样一个较低级别的访问。

ElementRef 能够经过使用 ViewChild decorator做为任何 DOM元素被返回 。可是因为全部组件都驻留在一个自定义DOM元素中,而且全部的指令都被应用于DOM元素,组件和指令类能够经过DI机制(依赖注入机制)得到与它们的宿主元素(host element)相关联的元素的实例:

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

所以,虽然组件能够经过DI访问它的宿主元素,但 ViewChild decorator 一般会在其视图(模板)(view (template))中得到对DOM元素的引用。指令的反作用——他们没有任何视图模板(views),他们一般直接与他们所依附的元素一块儿工做。

TemplateRef

对于大多数web开发人员来讲,模板的概念应该是熟悉的。模板是一组DOM元素,在应用程序的视图中能够重用。在HTML5标准引入模板标签template以前,大多数模板都是在一个带有一些 type 属性变化的脚本标记的浏览器中完成的:

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

这种方法固然有许多缺点,好比语义和手动去建立DOM模型的必要性。使用模板标签 template 浏览器解析 html 并建立 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>
<template id="tpl">
    <span>I am span in template</span>
</template>

Angular 拥抱HTML5的这种方法并实现 TemplateRef 类以变动好的操做使用模板。下面是如何使用它:

@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);
    }
}

框架从DOM中删除模板元素,并在其位置插入注释。这就是呈现时的样子:

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

经过它自己, TemplateRef 类是一个简单的类。它在 elementRef 属性中引用它的宿主元素,并有一个createEmbeddedView 方法。可是,这个方法很是有用,由于它容许咱们建立一个视图并返回一个引用做为 ViewRef

ViewRef

ViewRef 表示一个Angular 视图。在 Angular 框架中,视图(View)是应用程序UI的基本构件。它是构成和毁灭在一块儿的最小元素组合。Angular 鼓励开发人员将UI看做是视图的组成,而不是独立的html标记树。

Angular 支持两种视图:

  • Embedded Views which are linked to a Template (链接到模板的嵌入视图)

  • Host Views which are linked to a Component (链接到组件的宿主视图)

Creating embedded view (建立嵌入视图)

模板仅包含视图的蓝图。可使用前面提到的 createEmbeddedView 方法从模板中实例化一个视图:

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

Creating 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;
}

在 Angular 中,每一个组件都被绑定到一个注入器(injector)的特定实例,所以咱们在建立组件时传递当前的注入器实例。另外,不要忘记必须将动态实例化的组件添加到模块或托管组件的 EntryComponents
中。

所以,咱们已经看到了如何建立嵌入式视图和宿主视图。一旦建立了视图,就可使用 ViewContainer
将其插入到DOM中。下一节将探讨其功能。

ViewContainerRef

表示一个容器,其中能够附加一个或多个视图。

这里要提到的第一件事是,任何DOM元素均可以用做视图容器。有趣的是,Angular 在元素内部没有插入视图,而是在元素绑定到 ViewContainer 以后附加它们。这相似于 router-outlet 插入组件。

一般,一个好的候选对象能够标记一个 ViewContainer 应该被建立的位置,它是 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);
    }
}

正如其余DOM抽象同样, ViewContainer 被绑定到经过 element 属性访问的特定DOM元素。在这个例子中,它绑定到 ng-container 元素做为注释,所以输出是 template bindings={}

Manipulating views (操做视图)

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方法将它 insert 到DOM中。所以,这里有一个示例,从模板建立一个嵌入式视图,并将其插入由 ng - container 元素标记的特定位置 :

@Component({
    selector: 'sample',
    template: `
        <span>I am first span</span>
        <ng-container #vc></ng-container>
        <span>I am last span</span>
        <template #tpl>
            <span>I am span in template</span>
        </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>

为了从DOM中删除一个视图,咱们可使用 detach方法。全部其余方法都是自解释性的,可用于获取索引视图的引用,将视图移到另外一个位置,或者从容器中删除全部视图。

Creating Views (建立视图)

ViewContainer 还提供了自动建立视图的API:

class ViewContainerRef {
    element: ElementRef
    length: number

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

这些都是咱们在上面手工完成的简单方便的包装。它们从模板或组件建立视图,并将其插入指定的位置。

ngTemplateOutlet 和 ngComponentOutlet

虽然知道底层机制是如何工做的老是很好,但一般都但愿有某种快捷方式。此快捷方式以两种指令形式出现: ngTemplateOutletngComponentOutlet 。在撰写本文时,二者都是实验性的,ngComponentOutlet 将在版本4中可用(angular4+已能够随意使用)。但若是你已经读过上面全部的内容,就很容易理解它们的做用。

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>
        <template #tpl>
            <span>I am span in template</span>
        </template>
    `
})
export class SampleComponent {}

您能够看到,咱们在组件类中不使用任何实例化代码的视图。很是方便。

ngComponentOutlet

该指令相似于 ngTemplateOutlet,其不一样之处在于它建立了一个宿主视图(实例化一个组件),而不是一个嵌入式视图。你能够这样使用:

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

总结

如今,全部这些信息彷佛都很容易消化,但实际上它是至关连贯的,并在经过视图操做DOM的过程当中造成了一个清晰的理想模型。您能够经过使用 ViewChild 查询和模板变量引用来得到 Angular DOM 抽象的引用。围绕DOM元素的最简单的包装是 ElementRef 。对于模板,您有 TemplateRef,它容许您建立一个嵌入式视图。 能够经过使用 ComponentFactoryResolver建立的 componentRef 访问宿主视图。视图可使用 ViewContainerRef 进行操做。有两种指令使手动过程变为自动化:ngTemplateOutlet  ——操做嵌入视图 和 ngComponentOutlet —— 建立宿主视图(动态组件)。

原文:https://github.com/giscafer/g...

相关文章
相关标签/搜索