高级组件

Posted wpengch1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高级组件相关的知识,希望对你有一定的参考价值。

注:学习使用,禁止转载

通读本书,我们学会了怎么使用内建组件和怎么新建自己的组件,在这一章,我们会讲解一些在构建组件过程中使用到的高级特性。

在本章中,我们会学习下面的几个部分:

  • 组件样式(封装的)
  • 改变宿主(host)元素
  • 修改嵌入模板
  • 访问邻居指令
  • 使用生命周期回调
  • 脏检查

样式(style)

angular提供了一种指定组件样式的机制,CSS代表级联样式表,但是有时候我们并不希望级联,相反,我们希望给一个组件提供样式,但是这个样式不会污染我们的页面。

angular提供了两个属性来给我们的组件指定样式。

为了指定我们组件的样式,我们使用view的styles属性定义内联样式,或者使用styleUrls引用外部的样式。我们可以直接在组件的配置里面声明这些样式。

让我们写一个使用内联样式的组件。
code/advanced_components/app/ts/styling/styling.ts

@Component(
  selector: 'inline-style',
  styles: [`
  .highlight 
    border: 2px solid red;
    background-color: yellow;
    text-align: center;
    margin-bottom: 20px;
  
  `],
  template: `
  <h4 class="ui horizontal divider header">
    Inline style example
  </h4>

  <div class="highlight">
    This uses component <code>styles</code>
    property
  </div>
  `
)
class InlineStyle 

在这个例子中,我们定义了我们想要使用的.highlight样式类。通过在styles数组中声明。

在模板中,我们用

.来引用我们的类。

结果跟我们的预期是一样的。div会被渲染上红色的边框和黄色的背景。

另外一种方式就是使用styleUrls应用外部css类。
让我们编写一个使用外部css的例子,但是首先我们需要编写一个外部css文件。

external.css

code/advanced_components/app/ts/styling/external.css

.highlight 
  border: 2px dotted red;
  text-align: center;
  margin-bottom: 20px;

然后,我们编写引用它的代码:

code/advanced_components/app/ts/styling/styling.ts

@Component(
  selector: 'external-style',
  styleUrls: [externalCSSUrl],
  template: `
  <h4 class="ui horizontal divider header">
    External style example
  </h4>

  <div class="highlight">
    This uses component <code>styleUrls</code>
    property
  </div>
  `
)
class ExternalStyle 

当我们运行的时候,看起来是这样子的:

视图(样式)封装

一个有趣的地方就是两个组件都定义了一个highlight类,但是相互不污染。

这是因为,angular的样式默认会被组件上下文封装,如果我们审查页面并找到head标签,可以看到angular注入了一个style标签。

你可以注意到,css属性被_ngcontent-hve-2范围包含:

.highlight[\\_ngcontent-hve-2] 
 border: 2px solid red;
 background-color: yellow;
 text-align: center;
 margin-bottom: 20px;
 

如果我们审查我们的div,你会发现增加了一个_ng-content-hve-2范围。

对于外部样式,也是一样的:

angular允许我们使用encapsulation属性改变这个默认行为。这个属性是ViewEncapsulation枚举,定义了下面几个值:

  1. Emulated: 这个是默认选项,它会使用我们刚才讲的技术封装样式
  2. Native:使用这个选项,angular会使用Shadow DOM的方式
  3. None:使用这个选项,angular不会封装样式,让他们互相污染。

Shadow DOM封装

你可能想知道:使用Shadow DOM的意义是什么?通过使用Shadow DOM,我们使用的组件是一个唯一的DOM树,它对页面中的其他元素是隐藏的。这就让定义在这个组件中的元素对于其他元素来说是不可见的。

让我们创建一个组件,使用Shadow DOM-native的方式,看看它是怎么工作的。

code/advanced_components/app/ts/styling/styling.ts

@Component(
  selector: `native-encapsulation`,
  styles: [`
  .highlight 
    text-align: center;
    border: 2px solid black;
    border-radius: 3px;
    margin-botton: 20px;
  `],
  template: `
  <h4 class="ui horizontal divider header">
    Native encapsulation example
  </h4>

  <div class="highlight">
    This component uses <code>ViewEncapsulation.Native</code>
  </div>
  `,
  encapsulation: ViewEncapsulation.Native
)
class NativeEncapsulation 

在这个例子中,如果我们审查元素,我们会看到:

所有的东西都是封装在shadow-encapsulation里面,跟页面中的其他部分隔离。

No Encapsulation-不封装

最后,如果我们创建一个组件使用No Encapsulation,不会添加任何的封装:

code/advanced_components/app/ts/styling/styling.ts

@Component(
  selector: `no-encapsulation`,
  styles: [`
  .highlight 
    border: 2px dashed red;
    text-align: center;
    margin-bottom: 20px;
  
  `],
  template: `
  <h4 class="ui horizontal divider header">
    No encapsulation example
  </h4>

  <div class="highlight">
    This component uses <code>ViewEncapsulation.None</code>
  </div>
  `,
  encapsulation: ViewEncapsulation.None
)
class NoEncapsulation 

当我们审查元素的时候:

可以看到,没有任何封装。

创建一个弹框-引用和修改host元素

host元素是我们的指令或者组件绑定的元素,有时,我们的组件需要添加一些标记和行为给host元素。

这个例子中,我们创建一个Popup的指令,它将会添加一个行为到它的host元素上。当点击的时候显示一个消息。

组件和指令的区别
组件与指令是很像的,你可能听说过,“组件是带有view的指令”,其实严格意义上来说,它是不对的,组件有一些功能使得它添加view比较容易,但是指令也可以有view,实际上,组件是通过指令实现的。
指令的一个伟大例子就是ngIf。
但是,通过指令,我们可以附加一个行为到元素上,不需要模板。
你可以这样认为,组件就是指令,组件总是有view,但是指令可能没有
如果你在指令中渲染一个模板或者视图,你可以有更多的控制,对于模板怎么被渲染。接下来我们会讨论

Popup的结构

现在,让我们编写我们的第一个指令。我们希望当我们点击一个DOM的时候显示一个警告框。这个显示的信息来自于元素的message属性。
它看起来像这样:

<element popup message="Some message"></element>

为了让这个指令工作,我们需要做两件事情:

  • 从host中接收message属性
  • 当host被点击的时候的时候得到通知

让我们开始我们的指令:

code/advanced_components/app/ts/host/steps/host_01.ts


@Directive(
  selector: '[popup]'
)
class Popup 
  constructor() 
    console.log('Directive bound');
  

当我们使用Directive注解并设置selector为popup时,它说明这个指令可以附加到任何元素上。

让我们创建一个使用该元素的APP。

code/advanced_components/app/ts/host/steps/host_01.ts

@Component(
  selector: 'host-sample-app',
  directives: [Popup],
  template: `
  <div class="ui message" popup>
    <div class="header">
      Learning Directives
    </div>

    <p>
      This should use our Popup diretive
    </p>
  </div>
  `
)
export class HostSampleApp1 

当我们运行这个程序的时候,在console里面会打印出相应的信息,预示着我们的指令成功了。

使用ElementRef

如果你需要了解更多指令附加的元素的信息,你可以使用内建的ElementRef类。

这个类包含了给定angular元素的信息,包含本地DOM元素和本地DOM元素的属性。

为了访问host元素,我们可以在指令的构造函数中注入ElementRef,并且将其打印到console上。

code/advanced_components/app/ts/host/steps/host_02.ts

@Directive(
  selector: '[popup]'
)
class Popup 
  constructor(_elementRef: ElementRef) 
    console.log(_elementRef);
  

我们也可以添加第二个元素到页面上,我们可以看到两个不同的ElementRefs打印出来。

code/advanced_components/app/ts/host/steps/host_02.ts

@Component(
  selector: 'host-sample-app',
  directives: [Popup],
  template: `
  <div class="ui message" popup>
    <div class="header">
      Learning Directives
    </div>

    <p>
      This should use our Popup diretive
    </p>
  </div>

  <i class="alarm icon" popup></i>
  `
)
export class HostSampleApp2 

当我们运行应用程序的时候,我们会看到两个不同的ElementRefs:一个是div.ui.message,另外一个是i.alarm.icon,这就意味着指令被成功绑定到了两个元素上。

绑定到host

接下来,我们就是处理当host被点击的时候做一些事情。

之前,我们学到,在angular中,绑定事件的方式是使用(事件)的方式。

为了绑定事件到host,我们做的事情类似,但是使用host属性。host属性允许指令去修改它的host元素的属性和行为。
我们也希望host元素去定义我们将要展示的message。

为了做这个事情,我们使用前面介绍的(input)的属性给指令。

我们的指令看起来像下面这样:

code/advanced_components/app/ts/host/steps/host_03.ts

@Directive(
  selector: '[popup]',
  inputs: ['message'],
  host: 
    '(click)': 'displayMessage()'
  
)

我们会接收一个叫message的输入,并且,当host元素被点击的时候调用指令的displaymessage方法,

我们需要改进我们的代码

  • 增加一个新的message变量
  • 创建一个displayMessage方法

下面是我们做的:

code/advanced_components/app/ts/host/steps/host_03.ts

class Popup 
  message: String;

  constructor(_elementRef: ElementRef) 
    console.log(_elementRef);
  

  displayMessage(): void 
    alert(this.message);
  

最后,我们修改修改我们的模板,添加一个message属性。

code/advanced_components/app/ts/host/steps/host_03.ts

@Component(
  selector: 'host-sample-app',
  directives: [Popup],
  template: `
  <div class="ui message" popup
       message="Clicked the message">
    <div class="header">
      Learning Directives
    </div>

    <p>
      This should use our Popup diretive
    </p>
  </div>

  <i class="alarm icon" popup
     message="Clicked the alarm icon"></i>
  `
)
export class HostSampleApp3 

运行程序,并点击,得到下面:

使用exportAs增加一个button

现在,我们有一个新的需求:我们想要通过点击一个按钮手动触发警告框。我们怎么从host的外部去触发警告框呢?

为了完成这个目标,我们需要让指令可以从模板的任何地方访问。

正如我们前面讨论的,引用一个组件的方法是使用模板变量,指令也是一样。

为了给指令使用模板引用,我们使用exportAt属性。这使得host元素(或者host元素的子元素)去定义一个模板变量,并且使用#var=”exportName”的语法引用它。让我们为指令增加exportAs语法。

code/advanced_components/app/ts/host/steps/host_04.ts

@Directive(
  selector: '[popup]',
  inputs: ['message'],
  exportAs: 'popup',
  host: 
    '(click)': 'displayMessage()'
  
)
class Popup 
  message: String;

  constructor(_elementRef: ElementRef) 
    console.log(_elementRef);
  

  displayMessage(): void 
    alert(this.message);
  

现在,我们需要去改变到处模板变量的两个元素

code/advanced_components/app/ts/host/steps/host_04.ts

<div class="ui message" popup #p1="popup"
       message="Clicked the message">
    <div class="header">
      Learning Directives
    </div>

    <p>
      This should use our Popup diretive
    </p>
  </div>

  <i class="alarm icon" popup #p2="popup"
     message="Clicked the alarm icon"></i>

  <div style="margin-top: 20px;">
    <button (click)="p1.displayMessage()" class="ui button">
      Display popup for message element
    </button>

    <button (click)="p2.displayMessage()" class="ui button">
      Display popup for alarm icon
    </button>
  </div>

现在,我们可以使用模板变量var #p1和var p2
让我们增加两个按钮,分别取触发。

code/advanced_components/app/ts/host/steps/host_04.ts

<div style="margin-top: 20px;">
    <button (click)="p1.displayMessage()" class="ui button">
      Display popup for message element
    </button>

    <button (click)="p2.displayMessage()" class="ui button">
      Display popup for alarm icon
    </button>
  </div>

现在,重新load应用程序,分别点击按钮,会发现分别弹出两个框。

创建一个嵌入的消息panel

有时,当我们创建组件的时候,我们想要传递一些标签进去。这个技术叫:transclusion.

transclusion指的是一个普通模板嵌入到一个指令的模板中,或者是指令的模板嵌入到一个普通模板中。简而言之,transclusion就是一个指令包括或者是被包括到一段其他的代码中。

这使得我们制定一个标记,它可以扩展到一个更大的模板中去。让我们创建一个新的指令,它会渲染一个好看的信息:

我们的目标是编写下面的标签:

<div message header="My Message">
 This is the content of the message
 </div>

然后它将会呈现一个更加复杂的标记:

 <div class="ui message">
 <div class="header">
 My Message
 </div>

 <p>
 This is the content of the message
 </p>
 </div>

这里有两个挑战:

  1. 改变host元素,去增加ui和message的css类属性
  2. 我们需要增加特定的div内容到特定的地方

改变host元素的css样式

为了给host增加css类,我们需要使用像增加事件的方式,使用host属性。但是现在,我们使用属性名字和属性值的方式代替(事件)方式。

host:  'class': 'ui message' 

改变host元素,增加了这些css类给它

使用ng-content

接下来的挑战就是host元素的子元素去包含一个特定的view。为了完成这个,我们使用ng-content。

因为这个指令需要模板,让我们使用模板的方式编写这个代码:

code/advanced_components/app/ts/transclusion/transclusion.ts

@Component(
  selector: '[message]',
  inputs: ['header'],
  host: 
    'class': 'ui message'
  ,
  template: `
  <div>
    <div class="header">
       header 
    </div>
    <p>
      <ng-content></ng-content>
    </p>
  </div>
  `
)
class Message 
  header: string;

  ngOnInit(): void 
    console.log('header', this.header);
  

说明:

  1. 我们使用input属性表示我们希望接收一个message的属性
  2. 我们设置host元素的class属性,添加ui 和message。
  3. 我们使用ng-content,将特定的模板引用到我们的模板里面来

但我们打开浏览器,审查div元素的时候,发现它像预想的那样运行:

查询邻居指令–编写tab控件

当你创建一个完全封装的组件的时候,是很有意义的。

但是,随着组件的变化,会将它分解成相互合作的几个组件。

一个最好的例子就是就是一个tab panel里面包含很多的tab。选项卡面板或选项卡组,通常由很多个选项卡组成。
在这个例子中,我们有一个父组件tabset和很多子组件(tabs),不要让tabs和选项卡分离,但是把所有的逻辑放到一个组件里很麻烦。所以在这个例子中,我们会讲解怎么去分离他们,同时让他们协作。

让我们编写这些组件,下面的标签用法:

 <tabset>
 <tab title="Tab 1">Tab 1</tab>
 <tab title="Tab 2">Tab 2</tab>
 ...
 </tabset>

Tab Component

让我们开始编写Tab组件

code/advanced_components/app/ts/tabs/tabs.ts

@Component(
  selector: 'tab',
  inputs: ['title'],
  template: `
  <div class="ui bottom attached tab segment"
       [class.active]="active">

    <ng-content></ng-content>

  </div>
  `
)
class Tab 
  @Input('title') title: string;
  active: boolean = false;
  name: string;

需要注意的是,添加到title属性的@Input(‘title’)注解,这个注解是告诉angular,自动使用输入的title绑定到那属性title。

Tabset Component

让我们看看Tabset组件

code/advanced_components/app/ts/tabs/tabs.ts

@Component(
  selector: 'tabset',
  template: `
  <div class="ui top attached tabular menu">
    <a *ngFor="let tab of tabs"
       class="item"
       [class.active]="tab.active"
       (click)="setActive(tab)">

       tab.title 

    </a>
  </div>
  <ng-content></ng-content>
  `
)
class Tabset implements AfterContentInit 
  tabs: QueryList<Tab>;

  constructor(@Query(Tab) tabs:QueryList<Tab>) 
    this.tabs = tabs;
  

  ngAfterContentInit() 
    this.tabs.toArray()[0].active = true;
  

  setActive(tab: Tab) 
    this.tabs.toArray().forEach((t) => t.active = false);
    tab.active = true;
  

让我们分开看看,我们可以学一些新的概念。

Tabset @Component注解

@Component(
  selector: 'tabset',
  template: `
  <div class="ui top attached tabular menu">
    <a *ngFor="let tab of tabs"
       class="item"
       [class.active]="tab.active"
       (click)="setActive(tab)">

       tab.title 

    </a>
  </div>
  <ng-content></ng-content>
  `
)

这里没有新的概念

Tabset类

Tabset类的第一个要注意的就是实现了AfterContentInit.,这个回调告诉angular,一旦我们子组件的内容已经渲染完成之后就调用我们类的ngAfterContentInit方法。

Tabset的 Query 和 QueryList

接下来,我们声明了一个tabs属性,它存储我们tabset里面每一个tab的引用。我们不是声明称tab的数组或者列表,而是QueryList,为什么?

QueryList是angular提供的一个类,当我使用QueryList保存查询的结果的时候,angular会计算匹配的组件,并让自动保持它们的状态同步更新。

然而,QueryList需要一个Query去计算,看看下面:

在构造器中,我们使用@Query(Tab),这个注解告诉angular,注入本元素下的所有Tab类型的组件进tabs。然后,我们就有了访问所有子组件的入口。

初始化Tabset

当组件初始化后,我们希望将第一个tab设置为激活状态,为了完成这个事情,我们使用ngAfterContentInit函数,注意,我们使用this.tabs.toArray()将QueryList转换成一个Typescript数组。

Tabset 的setActive

最后,我们定义一个setActive方法,当我们的模板中点击一个tab的时候触发,这个函数会迭代所有的tab,并且设置它们的状态到false,然后设置我们点击的tab的状态为true。

使用Tabset

接下来,我们创建应用程序,确保它使用了我们创建的所有组件,如下:

code/advanced_components/app/ts/tabs/tabs.ts

@Component(
  selector: 'tabs-sample-app',
  directives: [Tabset, Tab],
  template: `
  <tabset>
    <tab title="First tab">
      Lorem ipsum dolor sit amet, consectetur adipisicing elit.
      Quibusdam magni quia ut harum facilis, ullam deleniti porro
      dignissimos quasi at molestiae sapiente natus, neque voluptatum
      ad consequuntur cupiditate nemo sunt.
    </tab>
    <tab *ngFor="let tab of tabs" [title]="tab.title">
       tab.content 
    </tab>
  </tabset>
  `
)
export class TabsSampleApp 
  tabs: any;

  constructor() 
    this.tabs = [
       title: 'About', content: 'This is the About tab' ,
       title: 'Blog', content: 'This is our blog' ,
       title: 'Contact us', content: 'Contact us here' ,
    ];
  

显示如下:

生命周期回调

angular允许我们添加下面的回调:

  • OnInit: 当我们的组件初始化后而子组件没有被初始化的时候被调用
  • OnDestroy: 当组件销毁的时候被调用。如果我们需要做一些清理工作,在这里面做是很方便的。
  • DoCheck:默认的通知系统会触发,当任何的指令属性改变的时候,比如OnChange被触发的时候,也会被触发
  • OnChanges: 当我们的组件的某个属性或者某些属性改变的时候被调用,ngOnChanges有一个参数,代表哪些属性改变了
  • AfterContentInit:在OnInit后,并且组件的内容或者子组件渲染完成后被调用
  • AfterContentChecked:工作也是类似的,但是它在指令检测完成后调用,check是指系统check。
  • AfterViewInit:组件的视图完全初始化后被调用
  • AfterViewChecked:组件完全检查完成后调用

使用这些回调都遵循类似的模式,
为了让这些事件通知你,你可以:

  1. 声明你的指令去实现特定的接口
  2. 定义相应的ng…方法

每一个方法都是通过ng+回调名称。

当angular知道组件实现了这些接口的时候,它会在合适的时候调用它们。

为了理解这些回调函数,我们编写一个组件,它有一个计数,我们可以点击按钮增加数字。

code/advanced_components/app/ts/lifecycle-hooks/lifecycle_04.ts

@Component(
  selector: 'afters',
  template: `
  <div class="ui label">
    <i class="list icon"></i> Counter:  counter 
  </div>

  <button class="ui primary button" (click)="inc()">
    Increment
  </button>
  `
)
class AftersCmp implements OnInit, OnDestroy, DoCheck,
                           OnChanges, AfterContentInit,
                           AfterContentChecked, AfterViewInit,
                           AfterViewChecked 
  counter: number;

  constructor() 
    console.log('AfterCmd --------- [constructor]');
    this.counter = 1;
  
  inc() 
    console.log('AfterCmd --------- [counter]');
    this.counter += 1;
  
  ngOnInit() 
    console.log('AfterCmd - OnInit');
  
  ngOnDestroy() 
    console.log('AfterCmp - OnDestroy');
  
  ngDoCheck() 
    console.log('AfterCmp - DoCheck');
  
  ngOnChanges() 
    console.log('AfterCmp - OnChanges');
  
  ngAfterContentInit() 
    console.log('AfterCmp - AfterContentInit');
  
  ngAfterContentChecked() 
    console.log('AfterCmp - AfterContentChecked');
  
  ngAfterViewInit() 
    console.log('AfterCmp - AfterViewInit');
  
  ngAfterViewChecked() 
    console.log('AfterCmp - AfterViewChecked');
  

然后我们增加一个按钮,让我们可以去使用onDestroy的回调。

code/advanced_components/app/ts/lifecycle-hooks/lifecycle_04.ts

<afters *ngIf="displayAfters"></afters>
  <button class="ui primary button" (click)="toggleAfters()">
    Toggle
  </button>

app的最后实现像这样:

@Component(
  selector: 'lifecycle-sample-app',
  directives: [OnInitCmp, OnChangeCmp, DoCheckCmp, AftersCmp],
  template: `
  <h4 class="ui horizontal divider header">
    OnInit and OnDestroy
  </h4>

  <button class="ui primary button" (click)="toggle()">
    Toggle
  </button>
  <on-init *ngIf="display"></on-init>

  <h4 class="ui horizontal divider header">
    OnChange
  </h4>

  <div class="ui form">
    <div class="field">
      <label>Name</label>
      <input type="text" #namefld value="name"
             (keyup)="setValues(namefld, commentfld)">
    </div>

    <div class="field">
      <label>Comment</label>
      <textarea (keyup)="setValues(namefld, commentfld)"
                rows="2" #commentfld>comment</textarea>
    </div>
  </div>

  <on-change [name]="name" [comment]="comment"></on-change>

  <h4 class="ui horizontal divider header">
    DoCheck
  </h4>

  <do-check></do-check>

  <h4 class="ui horizontal divider header">
    AfterContentInit, AfterViewInit, AfterContentChecked and AfterViewChecked
  </h4>

  <afters *ngIf="displayAfters"></afters>
  <button class="ui primary button" (click)="toggleAfters()">
    Toggle
  </button>
  `
)
export class LifecycleSampleApp4 
  display: boolean;
  displayAfters: boolean;
  name: string;
  comment: string;

  constructor() 
    // OnInit and OnDestroy
    this.display = true;

    // OnChange
    this.name = 'Felipe Coury';
    this.comment = 'I am learning so much!';

    // AfterXXX
    this.displayAfters = true;
  

  setValues(namefld, commentfld) 
    this.name = namefld.value;
    this.comment = commentfld.value;
  

  toggle(): void 
    this.display = !this.display;
  

  toggleAfters(): void 
    this.displayAfters = !this.displayAfters;
  

运行后,我们可以看到打印日志:

然后清除console,点击Increment按钮。

你可以看到,只有DoCheck, AfterContentCheck和AfterViewCheck触发了。

接着,我们点击Toggle按钮:

然后,我们再次点击它:

所有的都会被触发。

高级模板

模板元素是一个高级的元素,用来动态创建视图。

为了创建模板元素,angular提供了一些语法糖来简化它。通常,我们不需要手动创建它们。

比如,当我们编写下面的代码时:

<do-check-item
 *ngFor="let comment of comments"
 [comment]="comment"
 (onRemove)="removeComment($event)">
</do-check-item>

这个会转换为:

<do-check-item
 template="ngFor let comment of comments; #i=index"
 [comment]="comment"
 (onRemove)="removeComment($event)">
</do-check-item>

然后转换成:

<template
 ngFor
 [ngForOf]="comments"
 let-comment="$implicit"
 let-index="i">
 <do-check-item
 [comment]="comment"
 (onRemove)="removeComment($event)">
 </do-check-item>
</template>

当我们需要编写自己的指令的时候,理解这个概念是很重要的。

重写ngIf,编写ngBookIf

ngBookIf与ngIf基本上是一样的。

ngBookIf @Directive

首先,让我们为我们的类声明@Directive注解。

@Directive(
 selector: '[ngBookIf]',
 inputs: ['ngBookIf']
)

像上面我们学习的,当我们使用*ngBookIf=”condition”的时候,它会转换为:

<template ngBookIf [ngBookIf]="condition">

既然ngBookIf也是一个属性,我们需要表明我们需要接收一个ngBookIf作为输入。

这个指令的行为,就是当condition为true的时候,将其添加到指令模板里面,为false的时候,从指令模板里面删除。

所以,当条件为true的时候,我们将会使用一个view container,这个view container使用去附加一个或者多个视图给指令。

我们使用view container时:

  • 创建一个新的view,嵌入我们的指令模板
  • 清除view container内容

在做这个事情之前,我们需要注入ViewContainerRef和TemplateRef。他们将会注入指令的view container和模板。

这就是我们代码需要的。

code/advanced_components/app/ts/templates/if.ts

 constructor(private viewContainer: ViewContainerRef,
              private template: TemplateRef<any>) 

现在,我们有了viewContainer和template的引用,我们将会使用Typescript的属性设置构造器。

code/advanced_components/app/ts/templates/if.ts

set ngBookIf(condition) 
    if (condition) 
      this.viewContainer.createEmbeddedView(this.template);
    
    else 
      this.viewContainer.clear();
    
  

每次我们设置一个ngBookIf属性时,这个函数都会被调用。也就是说,每次condition变化的时候都会调用这个函数。

如果条件为true,我们使用viewContainer的createEmbeddedView方法去附加一个指令的模板。如果为false,clear函数会清除所有的内容。

使用ngBookIf

为了使用我们的指令,我们编写下面的组件:

code/advanced_components/app/ts/templates/if.ts

@Component(
  selector: 'template-sample-app',
  directives: [NgBookIf],
  template: `
  <button class="ui primary button" (click)="toggle()">
    Toggle
  </button>

  <div *ngBookIf="display">
    The message is displayed
  </div>

  `
)
export class IfTemplateSampleApp 
  display: boolean;

  constructor() 
    this.display = true;
  

  toggle() 
    this.display = !this.display;
  

当我们运行应用程序的时候,我们可以看到,这个指令像我们期望的那样工作。

重新ngFor-ngBookRepeat

现在,让我们编写一个简单版本的ngFor指令,angular提供了一个根据给定集合处理重复模板的方法。

ngBookRepeat模板解析

这个指令将会被像这样使用:*ngBookRepeat=”let var of collection”
像前面做的,我们需要声明一个selector,[ngBookRepeat],然而在这个例子中,不止有一个ngBookRepeat输入。

如果我们返回去看,angular转换了*something=”let var in collection”到下面的模式。

 <template something [somethingOf]="collection" let-var="$implicit">
 <!-- ... -->
 </template>

我们可以看到,被传递的属性不是something而是somethingOf。这里就是我们的指令接收集合的地方。对于生成的模板,我们要有一个本地变量#var,它会从$implicit接收参数,这是angular使用的局部变量的名称,当de-sugaring进模板。

ngBookRepeat @Directive

我们首先编写指令的注解
code/advanced_components/app/ts/templates/for.ts

@Directive(
  selector: '[ngBookRepeat]',
  inputs: ['ngBookRepeatOf']
)

ngBookRepeat class

然后我们编写类代码:

code/advanced_components/app/ts/templates/for.ts

class NgBookRepeat implements DoCheck 
  private items: any;
  private differ: IterableDiffer;
  private views: Map<any, ViewRef> = new Map<any, ViewRef>();


  constructor(CKG10-高性能高可用Yii2.0电商平台 仿京东商城 高级组件 MySQL LVS

高级正则实例

Flutter PageView组件怎样让子组件不会重复加载

Wix“高级”安装不会卸载以前的版本

怎么理解mixins

如何考虑样式化 AngularJS 组件?