每当我阅读中遇到,关于Angular中使用DOM的内容时,总会看到一个或几个这样的类:ElementRef,TemplateRef,ViewContainerRef等等。 不幸的是,虽然其中的一些被Angular文档或相关文章所讲述,可是我尚未找到完整的描述以及这些它们是如何工做的。 html
若是你来自angular.js世界,那么你知道操纵DOM是至关容易的。Angular注入DOM elementRef到构造函数中,你能够查询组件模板中的任何节点,添加或删除子节点,修改样式等。可是,这种方法有一个主要的缺点 - 它牢牢地绑定到浏览器平台。浏览器
新的Angular版本运行在不一样的平台上 - 浏览器,移动平台等。 所以,站在平台特定的API和框架接口之间须要抽象层次。Angular中,这些抽象成为如下引用类型的形式:ElementRef,TemplateRef,ViewRef,ComponentRef和ViewContainerRef。 在本文中,咱们将详细介绍每种引用类型,并展现如何使用它们来操做DOM。安全
在咱们探索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
这是最基本的抽象。 若是你观察它的类结构,你会发现它只保存了它所关联的本地元素。 对于访问本地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的形式返回一个引用。
这种抽象表示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中。 下一节将探讨其功能。
表示能够附加一个或多个视图的容器。
首先要提到的是,任何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 = {}。
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方法。 全部其余方法都是自解释性的,可用于经过索引获取对视图的引用,将视图移至其余位置或从容器中移除全部视图。
ViewContainer还提供API来自动建立视图:
class ViewContainerRef { element: ElementRef length: number createComponent(componentFactory...): ComponentRef<C> createEmbeddedView(templateRef...): EmbeddedViewRef<C> ... }
这些都是咱们上面手动完成的简单包装。 他们从模板或组件建立一个视图,并将其插入到指定位置。
这个将一个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 {}
正如你所看到的,咱们不使用任何视图实例化组件类中的代码。 很是便利。
该指令相似于ngTemplateOutlet,不一样之处在于它建立一个宿主视图(实例化一个组件),而不是嵌入视图。 你能够像这样使用它:
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>
如今,全部这些信息彷佛均可以被消化,但实际上这些信息是很是连贯的,而且经过视图来显示操纵DOM的清晰模型。 经过使用ViewChild查询和模板变量引用,您能够得到对Angular DOM抽象的引用。 围绕DOM元素的最简单的包装是ElementRef。 对于具备TemplateRef的模板,您能够建立嵌入式视图。 主机视图能够在使用ComponentFactoryResolver建立的componentRef上访问。 视图能够用ViewContainerRef来操做。 有两个使自动手动过程的指令:ngTemplateOutlet - 用于嵌入视图,ngComponentOutlet用于宿主视图(动态组件)。