英文原版:
Exploring Angular DOM manipulation techniques using ViewContainerRefhtml
_翻译:giscafer
说明:根据我的理解翻译,不彻底词词对应。_git
每当我读到关于使用Angular DOM的操做时,我老是会看到其中的一个或几个类: ElementRef
, TemplateRef
, ViewContainerRef
等。遗憾的是,尽管Angular文档或相关文章当中提到这三者的一些内容,但我尚未发现关于这三者如何协做的完整的理想模型和示例的描述。本文旨在描述这种模型。github
若是你学习过angular.js
的话,你就会知道在angular.js
中很容易去操做DOM。Angular注入DOM element
到 link
函数中,你能够查询组件模板内的任何节点,添加或删除子节点,修改样式等等。然而,这种方法有一个主要缺点——它被牢牢绑定到一个浏览器平台上(意思是脱离浏览器就不能玩了)。web
新的 Angular 版本运行在不一样的平台上——在浏览器上,在移动平台上,或者在 web worker 中。所以,须要在平台特定API 和框架接口之间进行抽象级别的抽象。从 Angular 来看,这些抽象的形式有如下的参考类型: ElementRef
, TemplateRef
, ViewRef
, ComponentRef
和 ViewContainerRef
。在本文中,咱们将详细介绍每一个引用类型,并展现如何使用它们来操做DOM。api
在探索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
这是最基本的抽象概念。若是您观察它的类结构,您将看到它只包含与之关联的原生元素(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),他们一般直接与他们所依附的元素一块儿工做。
对于大多数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
表示一个Angular 视图。在 Angular 框架中,视图(View)是应用程序UI的基本构件。它是构成和毁灭在一块儿的最小元素组合。Angular 鼓励开发人员将UI看做是视图的组成,而不是独立的html标记树。
Angular 支持两种视图:
Embedded Views which are linked to a Template (链接到模板的嵌入视图)
Host Views which are linked to a 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 中,每一个组件都被绑定到一个注入器(injector)的特定实例,所以咱们在建立组件时传递当前的注入器实例。另外,不要忘记必须将动态实例化的组件添加到模块或托管组件的 EntryComponents
中。
所以,咱们已经看到了如何建立嵌入式视图和宿主视图。一旦建立了视图,就可使用 ViewContainer
将其插入到DOM中。下一节将探讨其功能。
表示一个容器,其中能够附加一个或多个视图。
这里要提到的第一件事是,任何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={}
。
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
方法。全部其余方法都是自解释性的,可用于获取索引视图的引用,将视图移到另外一个位置,或者从容器中删除全部视图。
ViewContainer
还提供了自动建立视图的API:
class ViewContainerRef { element: ElementRef length: number createComponent(componentFactory...): ComponentRef<C> createEmbeddedView(templateRef...): EmbeddedViewRef<C> ... }
这些都是咱们在上面手工完成的简单方便的包装。它们从模板或组件建立视图,并将其插入指定的位置。
虽然知道底层机制是如何工做的老是很好,但一般都但愿有某种快捷方式。此快捷方式以两种指令形式出现: ngTemplateOutlet
和 ngComponentOutlet
。在撰写本文时,二者都是实验性的,ngComponentOutlet
将在版本4中可用(angular4+已能够随意使用)。但若是你已经读过上面全部的内容,就很容易理解它们的做用。
它将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 {}
您能够看到,咱们在组件类中不使用任何实例化代码的视图。很是方便。
该指令相似于 ngTemplateOutlet
,其不一样之处在于它建立了一个宿主视图(实例化一个组件),而不是一个嵌入式视图。你能够这样使用:
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>
如今,全部这些信息彷佛都很容易消化,但实际上它是至关连贯的,并在经过视图操做DOM的过程当中造成了一个清晰的理想模型。您能够经过使用 ViewChild
查询和模板变量引用来得到 Angular DOM 抽象的引用。围绕DOM元素的最简单的包装是 ElementRef
。对于模板,您有 TemplateRef
,它容许您建立一个嵌入式视图。 能够经过使用 ComponentFactoryResolver
建立的 componentRef
访问宿主视图。视图可使用 ViewContainerRef
进行操做。有两种指令使手动过程变为自动化:ngTemplateOutlet
——操做嵌入视图 和 ngComponentOutlet
—— 建立宿主视图(动态组件)。