Angular2如何使用jQuery

Angular2是以TypeScript语言作为默认编码语言,所以你看到的全部都是.ts结尾的文件。

那什么是TypeScript

首先,它是一个编译型语言;既然是编译型,那么你像重构、导航、智能提醒这种工具属性就可以发挥出来,所以你会发现使用VS CODE来写Angular简直就是绝配。

同时,TypeScript还带来一些ES6/7才有特性,比如let、const、async等,你无须关心ES几。

最霸气,TypeScript还是一个强类型、泛型、多态等一些面向对象编程的东西。

那,为何我们不选它呢?

当然,说了这么多,这跟主题看似无关,但如果你了解这些,才能看到问题的本质。

首先,第一关键点编译型,既然是编译型,那么你在代码中出现的任何变量、类、函数都是必须要存在,否则TA就会在编译时报错。

但,问题来了,现在的JavaScript世界中已经有那么多现成的第三方库,难道说都不能用了?非也!

TypeScript一开始就照顾这些了,所以就会有一个叫.d.ts的声明文件。MS当然不会让你去编写这一个文件,所以就有一个叫http://definitelytyped.org/ 网站,TA汇集了很多现成类库的第三方.d.ts的声明文件提供我们下载。

方法一

当然是使用最正规军了,用命令安装jQuery的声明文件。

npm install -D @types/jquery

以及使用

import * as $ from 'jquery';

$('body').addClass('');

完美的智能提示,如果你在VS CODE下的话。

方法二

对于一些并未提供 .d.ts 声明文件的类库,我们怎么办?那当然只能自己写了。

什么?自己写?很困难吧!很复杂吧!

没那么一回事,声明文件其实是对一些类库接口的描述,以下是我截取一段jQuery声明文件的部分代码

interface JQueryStatic {
    /**
     * 去掉字符串首尾空格
     *
     * @param str 字符串
     * @see {@link https://api.jquery.com/jQuery.trim/}
     */
    trim(str: string): string;
}

declare var $: JQueryStatic;

我还特意译成中文,这里的含量很少,最关键的就是 declare 它就是把一个变量 $ 定义成类型 JQueryStatic (还是个接口)。

这样,TS编译器在遇到 $ 时会去找该类型,并且你的代码里面不能出现 $.time1() 之类的,因为你的接口,只有一个 $.trim()

等等,jQuery几十个接口,我都要这么写吗?

NO!!!当然不是,除非你想写一个又漂亮、又好看、又是中文、又是完美智能提示的声明文件的话。

否则,你那就拿 any 类型吧,TA就是万能货。你不需要写一个很复杂的声明文件,只需要:

declare var $: any;

简单粗暴有效!

结论

哎~其实是因群里每天都可以看到一句【怎么使用jQuery】;虽然最简单的结果只需要一句话 declare var $: any;,但我还是啰里吧嗦将了一大堆,可不把前因后果将清楚,我烦~。

另,此解只是抛砖引玉,在很多类库中都是通用的办法。但我建议还是找一些Angular2类库来使用,因为如何更有效的管理JavaScript运行,是一门学问。

希望此篇对你的帮助,快乐编程!

JavaScript修饰器-让代码更干净

一般在JavaScript中为了让部分代码延迟执行,一想起的自然是 setTimeout,比如:

setTimeout(() => {
  // doing
}, 0);

这种代码或许你不知道写过多少遍,但,我们在 setTimeout 中多数情况下会去调用另一个方法:

setTimeout(() => {
  this.fn();
}, 0);

你会发现,我们一直都在重复写着 setTimeout,再套用一个匿名函数,最后才真正去编写我们需要执行的方法。我越来越讨厌这种写法,老是写着一些无关系要多余的代码。

使用Angular的同学对 @Component 不陌生,里面大量的使用这种ES7才会有的“修饰器”。

修饰器是一个函数,用于修改类行为。

那, 应该怎么编写一个更干净的 setTimeout,比如,我希望这样来编写我的timeout:

@timeout(1000)
fn() {
  // doing
}

this.fn();

对应的 timeout 修饰器代码:

// timeout.ts
export function timeout(milliseconds: number = 0) {
  return function(target, key, descriptor) {
    // value 值相当于上面示例中 `change` 方法。
    var orgMethod = descriptor.value;
    descriptor.value = function(...args) {
      setTimeout(() => {
        orgMethod.apply(this, args);
      }, milliseconds);
    };
    return descriptor;
  }
}

target:实例对象,即 IndexComponent 实例化对象。
key:方法名称,即 fn
descriptor:对象描述,同Object.getOwnPropertyDescriptor() 。

怎么样,这样子写的代码是不是更酷?

修饰器目前只能在ES7才会有,但一些在Typescript、Babel等转码器已经被支持,特别是Angular2应用中更是给予非常重要的地位。而且应用范围可以非常广,比如类、类方法和属性。

结论

以上只是一个很简单的修饰器示例,你可以根据需要生产一些有意思的修饰器,让编写的代码更优雅、更干净。完整示例

angular-cli如何启用HMR

webpack有一个非常好玩的插件叫:模块换替换(HMR),即添加或删除模块时应用持续运行,不需要页面刷新。虽然angular cli至问世以来就以webpack为核心,可直到【1.0.0-beta.22】以上版本才开始支持。

那么,接下来我们来玩一玩Angular的热替换。

先决条件

  • angular cli 至少 1.0.0-beta.22 以上版本

创建项目

使用Angular CLI创建一个空项目。

ng new ng-hmr
cd ng-hmr

添加HMR环境配置文件

HMR需要一定代码嵌入,而这些模块我们希望只在开发过程中使用,所以这里创建一个环境配置文件,用于区分我们什么时候需要引入 hmr 模块,而不会影响我们生产环境。

首先,创建 src/environments/environment.hmr.ts 文件,内容为:

export const environment = {
  production: false,
  hmr: true
};

这里相比较默认两个环境,增加了 hmr: true 变量。那么,另外两个,也一并设置相应 hmr 变量,来确保什么情况下才允许开启 HMR。

environment.prod.ts 生产环境肯定不会开启它:

export const environment = {
  production: true,
  hmr: false
};

environment.ts 默认环境,如果有必要也可以设置为 true,为了后面更好的演示,这里我设置 false

export const environment = {
  production: false,
  hmr: false
};

新环境配置还需要更新 angular-cli.jsonenvironments 节点,以确保angular cli 能认识:

      "environments": {
        "source": "environments/environment.ts",
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts",
        // 新增的节点
        "hmr": "environments/environment.hmr.ts"
      }

到此,与配置相关已经完成,你可以尝试使用 ng serve --hmr -e=hmr 来启用HMR。而且和之前相比会有这样一段提醒:

NOTICE Hot Module Replacement (HMR) is enabled for the dev server.
The project will still live reload when HMR is enabled,
but to take advantage of HMR additional application code is required
(not included in an Angular CLI project by default).
See https://webpack.github.io/docs/hot-module-replacement.html
for information on working with HMR for Webpack.

添加angular-hmr模块依赖与配置

正如提醒信息中说的,Angular CLI默认并不提供HMR所需要的额外应用代码。但Angular已经有了一个现成HMR模块即【Angular 2 Hot Module Replacement】,安装它:

npm install --save-dev @angularclass/hmr

接下来,创建 src/hmr.ts 文件,配置模块内容:

import { NgModuleRef, ApplicationRef } from '@angular/core';
import { createNewHosts } from '@angularclass/hmr';

export const hmrBootstrap = (module: any, bootstrap: () => Promise<NgModuleRef<any>>) => {
    let ngModule: NgModuleRef<any>;
    module.hot.accept();
    bootstrap().then(mod => {
        ngModule = mod;
    });
    module.hot.dispose(() => {
        let appRef: ApplicationRef = ngModule.injector.get(ApplicationRef);
        let elements = appRef.components.map(c => c.location.nativeElement);
        let makeVisible = createNewHosts(elements);
        ngModule.destroy();
        makeVisible();
    });
};

最后,修改 src/main.ts 内容:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode, ApplicationRef } from '@angular/core';
import { environment } from './environments/environment';
import { AppModule } from './app/app.module';

import { hmrBootstrap } from './hmr';

if (environment.production) {
  enableProdMode();
}

const bootstrap = () => {
  return platformBrowserDynamic().bootstrapModule(AppModule);
}

if (environment.hmr) {
  if (module['hot']) {
    hmrBootstrap(module, bootstrap);
  } else {
    // 未加上 --hmr 时,控制台会有错误提醒
    console.error('HMR没有启用,确保 ng server 命令加上 --hmr 标记');
  }
} else {
  bootstrap();
}

运行

现在,我们可以使用以下命令来启动HMR:

ng serve --hmr -e=hmr

尝试修改一个 app.component.tstitle 属性值,保存后,无须刷新页面,立即生效:

效果图

快点,享受更快乐的编码方式吧!

Angular2 动态创建组件

在ng1.x时我写过Angular 动态创建 directive;而在Angular2应用是由一个庞大的组件树来构建业务,一般而言我们是通过 template 模板来动态加载组件。

一、原因

我依然以1.x所遇到的项目实现为原型。

我们所认知的编辑器是为创建一个富HTML,而为了让用户更好的体验(总不能让客户写HTML语言吧)所产生的按表单填写数据最终生成有效的HTML。那么自然我们不可能只放一种像百度编辑器就完事,你让用创建一个轮播广告几乎是不可能;所以需要有很多组件,比如:商品列表、富文本、图片广告、轮播广告等等,而每一种组件又不可能是统一界面。

二、创建组件

首先需要先创建了两个组件 richconfig

src/components/rich.ts

import { Component } from '@angular/core';

@Component({
  selector: 'rich',
  template: `
    <p>Rich Content</p>
  `
})

export class RichComponent { }

src/components/config.ts

import { Component } from '@angular/core';

@Component({
  selector: 'config',
  template: `
    <p>Config Content</p>
  `
})

export class ConfigComponent { }

三、利用模板动态加载组件

template 模板里面加载组件是最常用加载方式,而且非常简单,只需要存放相应的 selector 模板引擎会自己查找并编译。

//our root app component
import {Component} from '@angular/core'

import {ConfigComponent} from 'src/components/config'
import {RichComponent} from 'src/components/rich'

@Component({
  selector: 'template-load',
  providers: [],
  template: `
      <h1>方式一:通过 template 动态加载</h1>
      <config></config>
      <rich></rich>
  `,
  directives: [ ConfigComponent, RichComponent ]
})
export class TemplateLoadComponent {
  constructor() {

  }
}

这样的方式很明显的一点是,template的结构将决定组件在组件树当中所处的位置。假如我们要把 configrich 组件对换位置,不得不对 template 重新编写,这样更别说让用户来拖拽来调整组件显示位置这么高级的办法了。

四、利用 DynamicComponentLoader 实现动态加载

DynamicComponentLoader 支持两种不同的加载方式来动态创建组件;但在这之前我们需要先做点什么。

首先,我示例里是根据按钮点击事件创建相应的组件,所以我们需要先创建一个组件映射表。

import {ConfigComponent} from 'src/components/config'
import {RichComponent} from 'src/components/rich'

export class DynamicLoadComponent {
  compMaps: {[key: string]: any} = {},
  constructor() {
    this.compMaps.config = ConfigComponent;
    this.compMaps.rich = RichComponent;
  }
}

这样我们可以模板里面定义添加事件,并传递相应的key值,下面是分别定义两个按钮用来创建 configrich 组件,以及 container1 元素用于放置组件位置。

<button type="button" (click)="add('config')">Add Config</button>
<button type="button" (click)="add('rich')">Add Rich</button>
<div id="container1"></div>

准备工作完成后,我们分别来看一下二种方式的差异:

1、loadAsRoot

loadAsRoot(type: Type, overrideSelectorOrNode: string|any, injector: Injector, onDispose?: () => void, projectableNodes?: any[][]) : Promise<ComponentRef<any>>

loadAsRoot 方法会创建一个 type 组件实例,然后放到 overrideSelectorOrNode 放处位置的第一个元素下面,不管你动态加载几次永远都只有一份组件实例

constructor(public dcl:DynamicComponentLoader, public _injector:Injector) {
}

add(comp) {
  this.dcl.loadAsRoot(this.compMaps[comp], "#container1", this._injector);
}

#container1 位置可以在整个文档的任意位置,只要该ID是存在。

最终的DOM结构像这样:

<div id="container1">
   <p>Config Content</p>
</div>

loadAsRoot 方法很好理解,而且很霸道,始终只会创建一个组件实例。

2、loadNextToLocation

loadNextToLocation(type: Type, location: ViewContainerRef, providers?: ResolvedReflectiveProvider[], projectableNodes?: any[][]) : Promise<ComponentRef<any>>

同样也是创建一个 type 组件实例,并插入到指定 location 位置的后面,ViewContainerRef 是一个视图容器,所以通过 loadNextToLocation 方法加载的组件,都会放入容器里面。

首先需要在模板中定义一个本地模板变量 container2,并在组件类中获取该变量。

@Component({
   template: `
    <div #container2></div>
   `
})

export class DynamicLoadComponent {
  @ViewChild('container2', {read: ViewContainerRef}) target;
}

add 方法也非常简单:

  add(type) {
    this.dcl.loadNextToLocation(this.compMaps[comp], this.target)
  }

最终的DOM结构像这样:

<config>
  <p>Config Content</p>
</config>
<rich>
  <p>Rich Content</p>
</rich>

3、传递数据

两种方法返回的结果都是一个 Promise 类型,并传递一个 ComponentRef 的新创建组件对象。


this.dcl.loadNextToLocation(this.compMaps[comp], this.target).then(function(cmpRef) { // doing })

可以通过 cmpRef 参数进行数据传递,比如:

cmpRef.instance.someProperty = someData;

五、总结

文章中只是做个抛砖引玉,实际项目应该做更深一层的抽象,Angular2是以组件为出发点,不能有太多了依赖。但是,文章至少已经解决最核心的问题了。文章并没有给出完整代码,如您感兴趣见Plunker

DynamicComponentLoader 从变更纪录来看非常频繁,在前两个BETA版本中都对其做大量的变动;但不管怎么变,是越来越精简。

Angular2 路由

Angular组件路由使一个视图至另一个视图的导航任务组件。

一、概要

浏览器是一个最熟悉的导航模型应用了,输入一个URL地址后浏览器导航相应URL页面,点击网页上的链接导航至另一个新的页面,浏览器的“后退”或“前近”可以导航至我们浏览器的历史页面。

Angular组件路由(也叫路由)就是借用这个模型,可以理解为通过浏览器的网址来生成客户端视图,并通过一些可选参数指定当前显示哪些视图。当然也可以把路由绑定至一个视图组件里,当点击链接时,导航至适合的视图。并且记录在浏览器历史列表,以便浏览器的前进或后退正常工作。

二、基础知识

先从组件路由基本概念开始,慢慢深入。

1、<base href>

大部分带有路由应用程序都会在 index.html<head> 加上 <base> 元素,用来设置基准URL(也就是应用程序的根路径)。

<base href="/">

2、导入 Router

Angular组件路由是个可选组件,所以并不包括在 core 里,需要我们单独引用 @angular/router 模块。

import { ROUTER_PROVIDERS } from '@angular/router';

3、配置

当浏览器URL改变时,路由会查找相应的 RouteDefinition 来确定组件的显示。

一般路由配置都在宿主组件内,(在Angular1.x经常会看到一些项目所有路由都在一个js文件当中)而且非常简单,通过 @Routers 装饰注册路由。

@Component({ ... })
@Routes([
  {path: '/crisis-center', component: CrisisListComponent},
  {path: '/heroes',        component: HeroListComponent},
  {path: '/hero/:id',      component: HeroDetailComponent}
])
export class AppComponent  implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {
    this.router.navigate(['/crisis-center']);
  }
}

有好几种 RouteDefinition 路由定义方式,大部分都是用URL地址来映射组件。

其中第三条路由 :id 是一个路由参数,比如URL地址 /hero/42 中 “42” 值就是 id 参数值。

4、Router Outlet

现在我们已经知道如何配置路由,当浏览器请求URL地址 /heroes 时,路由会查找 RouteDefinition 匹配到 HeroListComponent,那么匹配到的组件放在哪呢?我们就需要在当前组件模板里加入 RouterOutlet

<router-outlet></router-outlet>

我把它叫成点位符更贴切一点。

5、路由链接

知道如何配置和显示,那接下来就是如何导航,正常我们是通过浏览器的地址栏输入网站,可大部分是用户去触发某个链接进行导航的。

可以在一个锚标签添加一个 RouterLink 指令,模板表达式绑定的是一个字符串数组类型的网址。第一眼一定会很奇怪为什么是数组?这是因为还可能需要URL可选参数,虽然还是很奇怪。

template: `
  <h1>Component Router</h1>
  <nav>
    <a [routerLink]="['/crisis-center']">Crisis Center</a>
    <a [routerLink]="['/heroes']">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`,

6、总结一下

先通过 @RouterConfig 配置组件 AppComponent 的路由,再利用 RouterOutlet 来指定命中路由在哪显示,最后使用 RouterLinks 来处理用户点击链接的导航。

现在 AppComoponent 已经变成了一个路由组件。

路由 意思
Router 类定义了当前活动URL地址环境,可以用它在组件类里进行导航管理。
@RouteConfig 路由装饰器,接收一个 RouteDefinitions 路由定义对象数组。
RouteDefinition 路由定义对象。
RouterOutlet 匹配到组件的显示位置。
RouterLink 用于模板的链接导航。
Link Parameters Array 链接参数数组,可以用在模板的 RouterLink 或 Router.navigate 方法。
Routing Component 配置路由的组件。

三、一个示例

官网用了蛮长的来说明这个示例是怎么回事,大概是这样的:有两个列表分别 “Crisis Center”(记A) 和 “Heroes”(记B),点击列表的项时会进入编辑状态。这在之前二者都是一样的,不同在于编辑页面。

对于A有保存和取消按钮,不管点哪个按钮最后都会回到A列表,而且如果你改动 Name 后点击浏览器后退时会有一个对话框告诉你是不是要放弃编辑;反之B编辑时就很简单只有一个返回按钮,因为你每一次变动都会自动保存。

一个很长的动画,有耐心的可以先看一看效果。

router-anim

那么好,接下来会按里程碑式一步步说说是怎么实现的。

四、里程碑一:开始Router入门

很简单,通过路由来控制两个视图显示。

router-1-anim

1、设置 <base href>

组件路由使用浏览器 history.pushState 来导航,这样的URL地址看起来就像跟直接从服务端URL一样,例如:localhost:3000/crisis-center

现代浏览器都支持HTML5 pushState,为了让 pushState 更好工作,还需要在 index.html 添加一个 <base href="/"> 元素,这样同样可以确保所有请求的资源前缀。

HTML5模式导航是组件路由默认风格,我们也可以用老式的哈希(#)风格,后面会谈到。

2、导入组件路由模块

前面就说过路由模块是独立模块,需要从 angular/router 模块中获取。

import { Routes, Router, ROUTER_DIRECTIVES } from '@angular/router';

3、引导程序中注册路由服务

需要在 main.ts 文件里添加启动时注册路由服务。

import { bootstrap }        from '@angular/platform-browser-dynamic';
import { ROUTER_PROVIDERS } from '@angular/router';

import { AppComponent }     from './app.component';
bootstrap(AppComponent, [
  ROUTER_PROVIDERS
]);

从路由模块中导入 ROUTER_PROVIDERS,它实现一个默认配置的项,并注入到 provider 中。

4、AppComponent组件

和往常一样 AppComponent 组件是我们根组件,模板里包括两个导航链接和一个Router Outlet,通过点击导航链接显示不同的组件。

shell-and-outlet

对应的模板:

template: `
  <h1>Component Router</h1>
  <nav>
    <a [routerLink]="['/crisis-center']">Crisis Center</a>
    <a [routerLink]="['/heroes']">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`,

RouterOutlet

组件模板中 <router-outlet> 标签就是点位符,告诉命中路由的组件放在哪。

RouterLink属性绑定

RouterLink 属性指令,模板表达式的右边是一个数组对象来表示路由器导航所需要的数据:

  • 下标0是固定的URL部分。
  • 下标1以后放置的是可选参数数据。
<a [routerLink]="['/heroes']">Heroes</a>

除此之外,我们还可以在组件类里面使用 Route 导航管理器进行跳转。

this.router.navigate(['/heroes']);

总结

对于组件而言是一个带有层级结构的,那么当一个组件被放置时就已经被确立其URL地址结构了;所以组件自身理应包括路由的定义。如何更好在代码里面编写路由注册代码,应该在构建项目时就要加以考虑。

路由这一课题Angular2.x一直在变动,但整体的使用上并没有太多不同。

Angular2 结构指令

一个单页应用程序(SPA)最大特征就是对DOM树的操作,是根据程序状态对整段DOM的显示和消失。

一、什么是结构指令

Angular有三种指令类型:

  1. 组件
  2. 属性指令
  3. 结构指令

组件实际是一个带有模板的指令,这是最常见的三种指令,我们的应用程序也是基于指令来构建的。

属性指令改变的是元素的外观或行为,例如内置 NgStyle 指令,可以根据绑定组件的属性值同时改变多个元素的样式。

而结构指令是对DOM的添加或移除,这会改变DOM的布局。在模板语法章节就介绍过 ngIfngSwitchngFor

<div *ngIf="hero">{{hero}}</div>
<div *ngFor="let hero of heroes">{{hero}}</div>
<div [ngSwitch]="status">
  <template [ngSwitchWhen]="'in-mission'">In Mission</template>
  <template [ngSwitchWhen]="'ready'">Ready</template>
  <template ngSwitchDefault>Unknown</template>
</div>

二、NgIf 案例研究

ngIf 指令通过一个布尔类型变量来改变DOM的显示或消失。

<p *ngIf="condition">
  condition is true and ngIf is true.
</p>
<p *ngIf="!condition">
  condition is false and ngIf is false.
</p>

ngIf 指令并不是隐藏元素,使用浏览器开发工具我们可以看到,当条件为 true 时显示第一段DOM,反之显示第二段。

为什么要移除而不是隐藏?

我们可以通过设置CSS display 样式为 none 来隐藏元素,元素保留在DOM中只是看不见而已。相反,就用 ngIf

当我们隐藏一个元素,组件的行为还是继续着,像事件监听、变化检测、子组件的事件监听等依然还会在执手,哪怕我们压根看不见,很明显隐藏这一点来说最大的可能是性能问题;当然他也有好处,那就是当元素想要再一次被显示时,会非常快。

ngIf 就不同,设置为 false 时,会释放组件及其子组件的所有资源,并从DOM里移除元素;当然坏处就是,当你很快又需要该元素时,就要重新构建了。

虽然两种方法都有利弊,如果我们确认不使用的元素在短时间内不会再用那么 ngIf 最合理,反之。

接下来,我们用一个示例来说明二者的区别,通过设置css样式和 ngIf 对一个组件 heavy-loader (假设组件非常复杂)的显示或隐藏。

<div><!-- Visibility -->
  <button (click)="isVisible = !isVisible">show | hide</button>
  <heavy-loader [style.display]="isVisible ? 'inline' : 'none'" [logs]="logs"></heavy-loader>
</div>
<div><!-- NgIf -->
  <button (click)="condition = !condition">if | !if</button>
  <heavy-loader *ngIf="condition" [logs]="logs"></heavy-loader>
</div>
<h4>heavy-loader log:</h4>
<div *ngFor="let message of logs">{{message}}</div>

同时还记录组件的创建和销毁日志,这是利用内置 ngOnInitngOnDestroy 生命周期钩子来实现,效果如下:

heavy-loader-toggle

一开始在DOM都显示这两个组件,首先我们反复切换可见性,利用CSS样式由于每一次的可见性都是同一个实例,所以日志没有任何改变。

相反,当我们反复切换第二 ngIf 的可见性时,每一次都会出现创建新实例和销毁日志。

三、<template> 标签

结构指令像 ngIf,使用HTML template标签。对于非Angular应用而言 <template> 标签 display 样式值为 none,内容是不可见的文档片断;而Angular应用中,会移除所有 <template> 标签,并存储起来。

比如以下代码片断,中间的 Hip! 是用一个 <template> 包裹着。

<p>
  Hip!
</p>
<template>
  <p>
    Hip!
  </p>
</template>
<p>
  Hooray!
</p>

左边是非Angular应用,右边是Angular应用,二者的DOM区别。

template-in-out-of-a2

显然,默认情况下Angular会把 <template> 标签替换成一个空内容的 <script> 标签(虽然我本地的测试里并不会有这个空的script标签),不过呢,他可以做更多的事,比如 ngSwitch 指令在 <template> 标签中的应用:

<div [ngSwitch]="status">
  <template [ngSwitchWhen]="'in-mission'">In Mission</template>
  <template [ngSwitchWhen]="'ready'">Ready</template>
  <template ngSwitchDefault>Unknown</template>
</div>

当其中一个 ngSwitch 条件为 true 时,Angular会把标签里模板内容插入DOM当中。

四、关于指令中的 * 号

看看下面代码有什么特别的?

<div *ngIf="hero">{{hero}}</div>
<div *ngFor="let hero of heroes">{{hero}}</div>

上面指令名称有前缀星号(*),星号是一个语法糖,简化了 ngIfngFor 二者的读写,在模板引擎里最后还是会转换成 <template> 模式。

以下是 ngIf 语法糖与 <template> 的写法:

<!-- Examples (A) and (B) are the same -->
<!-- (A) *ngIf paragraph -->
<p *ngIf="condition">
  Our heroes are true!
</p>

<!-- (B) [ngIf] with template -->
<template [ngIf]="condition">
  <p>
    Our heroes are true!
  </p>
</template>

只要不傻肯定用第一种写法。值得注意的是从A扩展到B,先将段落及其内容移到 <template> 标签里面,属性绑定部分作为 <template> 标签的指令。

接下来看看 *ngFor 是如何转换:

<!-- Examples (A) and (B) are the same -->

<!-- (A) *ngFor div -->
<div *ngFor="let hero of heroes">{{ hero }}</div>

<!-- (B) ngFor with template -->
<template ngFor let-hero [ngForOf]="heroes">
  <div>{{ hero }}</div>
</template>

基本上和 ngIf 如出一辙;额外的细微是多了 ngForOf 属性绑定和 hero 本地模板变量。

五、创建结构指令

让我们创建一个 Unless 指令,类似 ngIf,只不过条件是反着的,当为 false 时才会显示内容。创建指令类似于创建组件:

  • 导入 Directive 装饰器。
  • 添加指令的CSS属性选择器标识符。
  • 指定一个公共的 Input 属性属性用于绑定(通常是指令的名字)。
  • 指令类实现。

先从以下代码开始:

import { Directive, Input } from '@angular/core';

@Directive({ selector: '[myUnless]' })
export class UnlessDirective {
}

指令的选择器用的 [myUnless],了解CSS选择器的就很清楚,用中括号来表示某个HTML元素属性。同时命名也尽可能避免 ng 开头,这一点说过很多次,无非就是害怕冲突。

现在还需要分别定义一个 TemplateRef 用来访问模板内容,以及 ViewContainerRef 用来访问视图容器。

constructor(
  private templateRef: TemplateRef<any>,
  private viewContainer: ViewContainerRef
  ) { }

重点来了,给指令定义一个输入属性 myUnless (名称和 selector 一样),并且接收一个布尔类型,用于条件判断添加或移除模板内容。

@Input() set myUnless(condition: boolean) {
  if (!condition) {
    this.viewContainer.createEmbeddedView(this.templateRef);
  } else {
    this.viewContainer.clear();
  }
}

整体的逻辑非常简单,指令所有代码如下:

import { Directive, Input } from '@angular/core';
import { TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[myUnless]' })
export class UnlessDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
    ) { }
  @Input() set myUnless(condition: boolean) {
    if (!condition) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}

现在来试试怎么运用这个指令,同 *ngIf 一样,只不过改成 *myUnless

<p *myUnless="condition">
  condition is false and myUnless is true.
</p>

<p *myUnless="!condition">
  condition is true and myUnless is false.
</p>

© 2017 卡片机色彩

Theme by cipchk

to top