首页 > 代码库 > 迈向angularjs2系列(2):angular2组件和指令详解
迈向angularjs2系列(2):angular2组件和指令详解
<%= INIT %>
内容
一:angular2 helloworld!
为了简单快速的运行一个ng2的app,那么通过script引入预先编译好的angular2版本和页面的基本框架。
index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <app></app> <!--使用app组件--> <script src="https://code.angularjs.org/2.0.0-beta.9/angular2-polyfills.min.js"> </script> <script src="https://code.angularjs.org/2.0.0-beta.9/Rx.umd.min.js"> </script> <script src="https://code.angularjs.org/2.0.0-beta.9/angular2-all.umd.min.js"> </script> <script src="./app.js"></script> </body> </html>
app.js:
var App=ng.core.Component({ //定义了名称为App的组件。 selector:"app", //匹配所有的app标签 template:"<h1>hello {{target}}</h1>" }) .Class({ //Class函数传递了一个对象字面量,只有constuctor方法 constructor:function(){ this.target="world"; } }); ng.platform.browser.bootstrap(App); //ng.platform.browser是命名空间
我们看到angularjs2的代码并没有用到typescript,所以它不是必须的,但ng2强烈推荐使用。这里没有用到任何模块加载或者包管理器,那么接下来继续学习。
二: 配置开发环境和angular2的typescript实现
1.通过git克隆项目
step1:安装git
到网站下载exe文件,https://github.com/git-for-windows/git/releases/download/v2.13.2.windows.1/Git-2.13.2-64-bit.exe。安装的过程我除了改了一下安装目录之外。
安装目录:
检测git是否安装正确:
首先在开始菜单里找到git cmd(也可以吧快捷方式发送到桌面),然后输出命令git --version。
step2:进入自己的项目目录,运行命令 git clone https://github.com/mgechev/switching-to-angular2.git 。
已经克隆下来。good!
step3:模块安装和启动开发用的Server。
$ npm install ;
$ npm start;
浏览器显示结果为:
success!
step4:上手试玩(optional)
内容替换,switching-to-angular2/app/ch4/ts/hello-world/app.ts:
import {Component} from ‘@angular/core‘; import {bootstrap} from ‘@angular/platform-browser-dynamic‘; @Component({ selector: ‘app‘, templateUrl: ‘./app.html‘ }) class App { target:string; constructor() { this.target = ‘world‘; } } bootstrap(App);
浏览器显示结果为:
2.hello-world深度解析
首先,看一下一共有4个文件。
index.html负责hello-world的首页(默认)文件的显示,app.html负责app模板的内容,meta.json是一些元信息,app.ts负责js代码逻辑。
接下来详细看index.html。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title><%= TITLE %></title> <!--TITLE变量注入--> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- inject:css --> <!-- endinject --> </head> <body> <app>Loading...</app> <!--app组件--> <!-- inject:js --> <!-- endinject --> <%= INIT %> <!--INIT是变量注入--> </body> </html>
app标签有文本内容,"Loading"一直处于可见状态,直到应用启动好、主组件渲染完毕为止。而 <%= TITLE %> 和 <%= INIT %> 是用来注入变量的。这些变量是在哪里定义的呢?
另,传授一个全局搜索文本的方法:
step1:文件目录右键,选择find in path
step2:搜索"TITLE"。
所以,它的定义在switching-to-angular2/tools/tasks的build.index.ts文件中。
build.index.ts:
import {join, sep} from ‘path‘; import {APP_SRC, APP_DEST, DEPENDENCIES, SYSTEM_CONFIG, ENV} from ‘../config‘; import {transformPath, templateLocals} from ‘../utils‘; export = function buildIndexDev(gulp, plugins) { return function () { return gulp.src(join(APP_SRC, ‘**‘, ‘index.html‘)) // NOTE: There might be a way to pipe in loop. .pipe(inject()) .pipe(plugins.template( require(‘merge‘)(templateLocals(), { TITLE: ‘Switching to Angular 2‘, INIT: ` <script> System.config(${JSON.stringify(SYSTEM_CONFIG)}); System.import("./app") .catch(function () { console.log("Report this error to https://github.com/mgechev/switching-to-angular2/issues", e); }); </script>` }) )) .pipe(gulp.dest(APP_DEST)); }; function inject() { return plugins.inject(gulp.src(getInjectablesDependenciesRef(), { read: false }), { transform: transformPath(plugins, ‘dev‘) }); } function getInjectablesDependenciesRef() { let shims = DEPENDENCIES.filter(dep => dep[‘inject‘] && dep[‘inject‘] === ‘shims‘); let libs = DEPENDENCIES.filter(dep => dep[‘inject‘] && dep[‘inject‘] === ‘libs‘); let all = DEPENDENCIES.filter(dep => dep[‘inject‘] && dep[‘inject‘] === true); return shims.concat(libs).concat(all).map(mapPath); } function mapPath(dep) { let prodPath = join(dep.dest, dep.src.split(sep).pop()); return (‘prod‘ === ENV ? prodPath : dep.src ); } };
第三,分析一下ch4/ts/hello-world/app.ts。
import {Component} from ‘@angular/core‘; //导入了component装饰器 import {bootstrap} from ‘@angular/platform-browser-dynamic‘; //导入了bootstrap函数 @Component({ //Component装饰了class App selector: ‘app‘, templateUrl: ‘./app.html‘ //对象字面量参数和ES5类似,app选择器和视图内容。模板既可以template内联,也可以使用url,和ng1类似。 }) class App { target:string; constructor() { this.target = ‘world‘; } } bootstrap(App); //启动APP
四: angular2指令详解
开发app组件现在开始了!
1.基础to-do list应用
(1)进入代码:
进入ch4/ts/ng-for/detailed-syntax目录,一共4个部分。
app.ts:
import {Component} from ‘@angular/core‘; import {bootstrap} from ‘@angular/platform-browser-dynamic‘; //导入 @Component({ //装饰器 selector: ‘app‘, templateUrl: ‘./app.html‘, }) class App { todos:string[]; name:string; constructor() { //app类的属性定义,可以直接在app组件的代码里使用 this.name = "爱莎"; this.todos = [‘呼风唤雪‘, "保护妹妹"]; } } bootstrap(App); //启动应用
app.html:
<h1>你好,{{name}}!</h1> <h3> to-do清单: </h3> <ul> <template ngFor let-todo [ngForOf]="todos"> <li>{{todo}}</li> </template> </ul>
说明:template标签内部可以放HTML片段,那么屏蔽了浏览器的直接渲染,而接下来由模板引擎来处理。
index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title><%= TITLE %></title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- inject:css --> <!-- endinject --> </head> <body> <app>Loading...</app> <!-- inject:js --> <!-- endinject --> <%= INIT %> </body> </html>
meta.json:
{ "title": "List of items (detailed syntax)", "description": "List of items using explicit template for ng-for", "id": 3, "presented": true }
打开http://localhost:5555/dist/dev/ch4/ts/ng-for/detailed-syntax/地址,显示如下:
2. 更加优秀的ngFor指令
ngFor指令用来遍历集合中的元素,它的行为和ng1中的ng-repeat一样。它有ngForOf属性以及更多的语法特性。
<template ngFor let-todo [ngForOf]="todos"> <li>{{todo}}</li> </template>
(1)优秀一:更简单,提升了指令的语义性。
ng1的指令用法五花八门,需要对各种属性值有深入的理解。而ng2引入了更加简单的约定。
属性有3种用法:
●propertyName="value"
●[propertyName]="expression"
●(eventName)="handlerFunc()"
第一种语法:普通字面量
propertyName接收字符串字面量,angular不会对它进行进一步处理。
第二种语法:方括号语法
提示angular2要当做表达式来处理。属性包围在方括号里面,angular就会尝试执行这个表达式,执行上下文就是模板对应的组件。
第三种语法:小括号语法
当然是事件绑定咯。
可以看到,谁是谁,非常清晰。
(2)模板内部的变量(var-XX)
<template ngFor let-todo [ngForOf]="todos"> [ngForOf]的值作为表达式执行之后会返回一个集合,通过var-todo语法告诉angular需要创建的新变量叫做todo。
(3)使用语法糖
语法糖简单的说就是更方便更简洁的写法。
使用星号,扔掉templatebiaoqian ,指令也直接用到容器标签上。
<ul> <li *ngFor="#todo of todos">{{todo}}</li> </ul>
angular2会自动对上面代码进行“脱糖”处理,就能变成上面比较啰嗦的形式了。
3.自定义angular2指令
前面小节看了如何在DOM标签上使用指令,指令把DOM标签参数化了,而这一节讲自定义指令。自定义才是高级玩法。
本小节将构建一个简单的tooltip自定义指令。
(1)进入代码:
ch4/ts/tooltip/app.html:
<div saTooltip="Hello world!"> </div>
ch4/ts/tooltip/app.ts:
一共分为导入、Overlay类、指令、常规的APP类定义。
//导入 import {HostListener, Input, Injectable, ElementRef, Inject, Directive, Component} from ‘@angular/core‘; import {bootstrap} from ‘@angular/platform-browser-dynamic‘; //Overlay类的定义 class Overlay { private el: HTMLElement; constructor() { var el = document.createElement(‘div‘); el.className = ‘tooltip‘; this.el = el; } close() { this.el.hidden = true; } open(el, text) { this.el.innerHTML = text; this.el.hidden = false; var rect = el.nativeElement.getBoundingClientRect(); this.el.style.left = rect.left + ‘px‘; this.el.style.top = rect.top + ‘px‘; } attach(target) { target.appendChild(this.el); } detach() { this.el.parentNode.removeChild(this.el); } } class OverlayMock { constructor() {} close() {} open(el, text) {} attach(target) {} detach() {} } //指令定义 @Directive({ selector: ‘[saTooltip]‘ }) export class Tooltip { @Input() saTooltip:string; constructor(private el: ElementRef, private overlay: Overlay) { this.overlay.attach(el.nativeElement); } @HostListener(‘mouseenter‘) onm ouseEnter() { this.overlay.open(this.el, this.saTooltip); } @HostListener(‘mouseleave‘) onm ouseLeave() { this.overlay.close(); } } //APP类定义,并被component装饰器修饰以及最后的启动 @Component({ selector: ‘app‘, templateUrl: ‘./app.html‘, providers: [Overlay], directives: [Tooltip] }) class App {} bootstrap(App);
打开http://localhost:5555/dist/dev/ch4/ts/tooltip/地址,那么会有如下结果:
(2)导入的类HostListener、Directive、ElementRef
app.ts导入了HostListener, Input, Injectable, ElementRef, Inject, Directive, Component这些类。
●HostListener(eventname)
用于事件处理。指令实例化的时候,angular2会把这个装饰过的方法当成宿主标签上对应eventname的事件处理函数。
@HostListener(‘mouseenter‘) //定义监听器 onm ouseEnter() { this.overlay.open(this.el, this.saTooltip); } //定义监听函数
●Directive
用于定义指令。用来添加额外的元数据。
//指令定义 @Directive({ selector: ‘[saTooltip]‘ //大小写敏感哦 })
●ElementRef
在宿主标签里注入其他标签的引用,不仅仅是DOM。比如这里的模板就是angular包装过的div标签,里面有tooltip属性。
<div saTooltip="Hello world!"> </div>
(3)指令输入的input装饰器
接收的参数是需要绑定的属性名。如果不传递参数,,默认绑定到同名属性上。
注意:angular的HTML编译器对大小写敏感。
我们来看一下input的构造器到底干了什么?
@Input() //给指令定义一个saTooltip属性,属性的值是表达式。 saTooltip:string;
(4)constructor构造器的理解
constructor(private el: ElementRef, private overlay: Overlay) { //overloay是定义的overlay类 //我们来看一下ElementRef究竟是什么 console.log(el); this.overlay.attach(el.nativeElement); }
●私有属性el
首先,看一下el,也就是ElementRef是什么。根据打印结果,ElementRef就是app.html模板里包装好的div元素。
再来看attach。这行代码负责刚刚的原生div元素添加内容。
attach(target) { target.appendChild(this.el); //target是原生的div,给它添加子元素,内容是overloay定义的this.el }
●私有属性overlay
overlay的类型为Overlay,也就是定义的class类。Overlay类实现了维护tooltip组件外观的逻辑,并可以用angular的DI依赖注入,当然要加上顶层组件Component的定义。
ch4/ts/tooltip/app.ts:
@Component({ selector: ‘app‘, templateUrl: ‘./app.html‘, providers: [Overlay],//数组哦,实现Overlay的依赖注入 directives: [Tooltip] })
(5)封装指令要在顶层组件声明。
注意:声明的是数组哦。整个组件的内部全部指令都会声明在这里。尽管显式声明有些麻烦,但能带来更好的封装性。而在ng1种所有指令都在全局命名空间中,坏处是容易命名冲突。同时我们知道了组件用了哪些指令,更好理解了。
@Component({ //component装饰器,传递对象字面量作为参数 selector: ‘app‘, templateUrl: ‘./app.html‘, providers: [Overlay], directives: [Tooltip]//声明指令数组,angular编译器就可以发现tooltip指令了 }) class App {}
五: 组件详解
todo组件分为导入、接口定义、顶层组件、控制器、启动5个部分。
app.ts:
//导入 import {Component} from ‘@angular/core‘; import {bootstrap} from ‘@angular/platform-browser-dynamic‘; //接口定义 interface Todo { completed: boolean; label: string; } //顶层组件 @Component({ selector: ‘app‘, templateUrl: ‘./app.html‘, styles: [ `ul li { list-style: none; } .completed { text-decoration: line-through; }` ] }) //控制器 class TodoCtrl { todos: Todo[] = [{ label: ‘Buy milk‘, completed: false }, { label: "Save the world", completed: false }]; name: string = ‘John‘; addTodo(label) { this.todos.push({ label, completed: false }) } removeTodo(idx) { this.todos.splice(idx, 1); } toggleCompletion(idx) { let todo = this.todos[idx]; todo.completed = !todo.completed; } } //启动 bootstrap(TodoCtrl);
1.组件样式
我们直接在组件装饰器加入styles属性。
app/ch4/ts/todo-app/app.ts的样式代码:
@Component({ selector: ‘app‘, templateUrl: ‘./app.html‘, styles: [ `ul li { list-style: none; } .completed { text-decoration: line-through; }` //styles属性,值为数组。 ] })
2.组件的控制器
app.ts控制器代码:
//控制器 class TodoCtrl { todos: Todo[] = [{ label: ‘呼风唤雨‘, completed: false }, { label: "保护妹妹", completed: false }]; //todos数组。包含两个元素,每个元素的变量指定为Todo。 name: string = ‘爱莎‘; //name属性 addTodo(label) { this.todos.push({ label, completed: false }) } //用于添加元素 removeTodo(idx) { this.todos.splice(idx, 1); } //用于移除元素 toggleCompletion(idx) { let todo = this.todos[idx]; todo.completed = !todo.completed; } //切换完成或者未完成 }
模板app.html内容:
<h1>你好 {{name}}!</h1> <p> 添加todo: <input #newtodo type="text"> <button (click)="addTodo(newtodo.value); newtodo.value = http://www.mamicode.com/‘‘">Add</button> </p> <p>to-do清单:</p> <ul> <!--遍历todos数组--> <li *ngFor="let todo of todos; let index = index" [class.completed]="todo.completed"> <input type="checkbox" [checked]="todo.completed" (change)="toggleCompletion(index)"> {{todo.label}} </li> </ul>
● <li *ngFor="let todo of todos; let index = index" [class.completed]="todo.completed"> 这里使用了index索引。
● (change)="toggleCompletion(index)" 绑定了toggleCompletion函数到change事件上。
● [checked]="todo.completed" 绑定了控制器的todos数据的completed到checked属性上。
● [class.completed]="todo.completed" 意思是如果todo.completed为true,那么添加completed类。angular也允许绑定style和attribute的。
最后启动app的时候,要把控制器传进去。
//启动 bootstrap(TodoCtrl);
打开http://localhost:5555/dist/dev/ch4/ts/todo-app/,结果为
3.组件的用户交互
实现toggleCompletion方法。它是控制器里定义的,所以直接使用咯。
toggleCompletion(idx) { let todo = this.todos[idx]; todo.completed = !todo.completed; } //切换完成或者未完成的状态
实现addToDo方法,用来添加todo元素。
addTodo(label) { this.todos.push({ label, completed: false }) } //用于添加元素
通过button按钮添加元素如图:
4.app组件的切分
先来看一下指令API的输入输出。可以把指令接受的属性看成输入。把指令触发的事件看成输出。使用第三方库的指令时最主要的关注点就是输入和输出,这些内容构成了指令的API。
如果把todo应用切分成多个独立的组件,组件之间存在交互,那么分析一下这个todo应用咯。
最外层的方框代表整个todo应用。内部的第一个方框有一个组件,用来新建todo项目。下面的方框用来存储所有的项目。
根据分析,我们可以定义3个组件:
●TodoApp
●InputBox
●TodoList
5.InputBox组件:关注输入和输出
迈向angularjs2系列(2):angular2组件和指令详解