使用ViewContainerRef探索Angular DOM操做技术

每当我阅读中遇到,关于Angular中使用DOM的内容时,总会看到一个或几个这样的类:ElementRef,TemplateRef,ViewContainerRef等等。 不幸的是,虽然其中的一些被Angular文档或相关文章所讲述,可是我尚未找到完整的描述以及这些它们是如何工做的。 html

若是你来自angular.js世界,那么你知道操纵DOM是至关容易的。Angular注入DOM elementRef到构造函数中,你能够查询组件模板中的任何节点,添加或删除子节点,修改样式等。可是,这种方法有一个主要的缺点 - 它牢牢地绑定到浏览器平台。浏览器

新的Angular版本运行在不一样的平台上 - 浏览器,移动平台等。 所以,站在平台特定的API和框架接口之间须要抽象层次。Angular中,这些抽象成为如下引用类型的形式:ElementRef,TemplateRef,ViewRef,ComponentRef和ViewContainerRef。 在本文中,咱们将详细介绍每种引用类型,并展现如何使用它们来操做DOM。安全

@ViewChild

在咱们探索DOM抽象以前,让咱们了解如何在组件/指令类中访问这些抽象。 Angular提供了一种称为DOM查询的机制。 它以@ViewChild和@ViewChildren装饰器的形式出现。 它们的行为相同,只有前者返回一个引用,后者则返回多个引用做为QueryList对象。 在这篇文章的例子中,我将主要使用ViewChild装饰器。框架

一般,这些装饰器与模板引用变量配对使用。 模板引用变量只是对模板中的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装饰器的基本语法以下:this

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

在这个例子中,你能够看到我在html中指定了tref做为模板引用名,而且接收到与这个元素相关的ElementRef。 读取的第二个参数并不老是必需的,由于Angular能够经过DOM元素的类型来推断引用类型。 例如,若是它是一个简单的HTML元素(如span),那么angular将返回ElementRef。 若是它是一个模板元素,它将返回TemplateRef。不过一些引用,如ViewContainerRef不能被推断,而且必须在读参数中特别要求。 其余的,像ViewRef不能从DOM返回,必须手动构造。spa

ElementRef

这是最基本的抽象。 若是你观察它的类结构,你会发现它只保存了它所关联的本地元素。 对于访问本地DOM元素很是有用,咱们能够在这里看到:code

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

不过,Angular团队不鼓励这种用法。 这不只会带来安全风险,还会在应用程序和渲染层之间形成紧密耦合,这使得在多个平台上运行应用程序变得困难。 我相信这不是对nativeElement的访问,而是打破了抽象,而是像textContent同样使用特定的DOM API。 可是后面你会看到,在Angular中实现的DOM操做心智模型几乎不须要这样一个较低级别的访问。component

可使用ViewChild装饰器为任何DOM元素返回ElementRef。 可是,因为全部组件都驻留在自定义DOM元素中,而且全部指令都应用于DOM元素,所以组件和指令类能够经过DI机制获取与其主机元素关联的ElementRef实例:htm

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

所以,虽然组件能够经过DI访问其主机元素,但ViewChild装饰器一般用于在其视图(模板)中获取对DOM元素的引用。 反之亦然,指令没有视图,他们一般直接与他们所附的元素。

模板的概念应该是大多数Web开发人员熟悉的。 这是一组DOM元素,在整个应用程序的视图中被重用。 在HTML5标准引入了模板标签以前,大多数模板都被包含在script标签中。

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

这种方法固然有许多缺点,如语义和手动建立DOM模型的必要性。 使用模板标签浏览器解析HTML并建立DOM树,但不呈现它。 而后能够经过内容属性访问:

<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>

Angular支持这种方法,并实现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

这种抽象表示Angular视图。 在Angular世界中,View是应用程序UI的基本构建块。 它是创造和消灭的最小的元素分组。 Angular哲学鼓励开发人员将UI视为Views的组合,而不是将其视为独立的HTML标签。

Angular支持两种类型的视图:

  • 嵌入视图连接到模板
  • 连接到组件的主机视图

建立嵌入的视图

一个模板只是一个视图的蓝图。 一个视图可使用前面提到的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中,每一个组件都绑定到一个注入器的特定实例,因此咱们在建立组件时传递当前的注入器实例。 此外,不要忘记,动态实例化的组件必须添加到模块或主机组件的EntryComponents。

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

ViewContainerRef

表示能够附加一个或多个视图的容器。

首先要提到的是,任何DOM元素均可以用做视图容器。有趣的是,Angular不在元素内插入视图,而是在绑定到ViewContainer的元素以后附加它们。 这与路由器插座如何插入组件相似。

一般,标记应该建立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绑定到经过元素属性访问的特定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
}

咱们以前已经看到,如何从模板和组件手动建立两种类型的视图。 一旦咱们有了一个视图,咱们可使用插入方法将其插入到DOM中。 因此,下面是从模板中建立一个嵌入式视图并将其插入到由ng-container元素标记的特定位置的示例:

@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>

要从DOM中删除视图,咱们可使用detach方法。 全部其余方法都是自解释性的,可用于经过索引获取对视图的引用,将视图移至其余位置或从容器中移除全部视图。

Creating Views

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

class ViewContainerRef {
    element: ElementRef
    length: number

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

这些都是咱们上面手动完成的简单包装。 他们从模板或组件建立一个视图,并将其插入到指定位置。

ngTemplateOutlet and ngComponentOutlet

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,不一样之处在于它建立一个宿主视图(实例化一个组件),而不是嵌入视图。 你能够像这样使用它:

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

总结

如今,全部这些信息彷佛均可以被消化,但实际上这些信息是很是连贯的,而且经过视图来显示操纵DOM的清晰模型。 经过使用ViewChild查询和模板变量引用,您能够得到对Angular DOM抽象的引用。 围绕DOM元素的最简单的包装是ElementRef。 对于具备TemplateRef的模板,您能够建立嵌入式视图。 主机视图能够在使用ComponentFactoryResolver建立的componentRef上访问。 视图能够用ViewContainerRef来操做。 有两个使自动手动过程的指令:ngTemplateOutlet - 用于嵌入视图,ngComponentOutlet用于宿主视图(动态组件)。

相关文章
相关标签/搜索