
正文
迈向angularjs2系列(3):组件详解
提示:扫一扫查出行【扫一扫了解最新限行尾号】
复制提示
一: 以组件开发一个to-do list应用
1.启动server:
进入switchingToNG2/switching-to-angular2目录,运行npm start,那么打开浏览器,进入红色框的链接。

2.进入switching-to-angular2/app/ch4/ts/todo-app目录。
app.ts:
todo组件分为导入、接口定义、顶层组件、控制器、启动5个部分。
//导入
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;
}
//切换完成或者未完成
}
(3)模板app.html内容:
<h1>你好 {{name}}!</h1><p>
添加todo:
<input #newtodo type="text">
<button (click)="addTodo(newtodo.value); newtodo.value = ''">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组件:关注输入和输出
app/ch4/ts/inputs-outputs/app.ts完整的代码:
//导入
import {Component, Input, Output, EventEmitter} from '@angular/core';
import {bootstrap} from '@angular/platform-browser-dynamic';//todo接口
interface Todo {
completed: boolean;
label: string;
}//InputBox组件
@Component({
selector: 'input-box',
template: `
<input #todoInput [placeholder]="inputPlaceholder">
<button (click)="emitText(todoInput.value); todoInput.value = '';">
{{buttonLabel}}
</button>
`
})
class InputBox {
@Input() inputPlaceholder: string;
@Input() buttonLabel: string;
@Output() inputText = new EventEmitter<string>();
emitText(text: string) {
this.inputText.emit(text);
}
}
//ToDoList组件
@Component({
selector: 'todo-list',
template: `
<ul>
<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>
`,
styles: [
`ul li {
list-style: none;
}
.completed {
text-decoration: line-through;
}`
]
})
class TodoList {
@Input() todos: Todo[];
@Output() toggle = new EventEmitter<Todo>();
toggleCompletion(index: number) {
let todo = this.todos[index];
this.toggle.emit(todo);
}
}
//TodoApp组件
@Component({
selector: 'todo-app',
directives: [TodoList, InputBox],
template: `
<h1>Hello {{name}}!</h1> <p>
Add a new todo:
<input-box inputPlaceholder="New todo..."
buttonLabel="Add"
(inputText)="addTodo($event)">
</input-box>
</p> <p>Here's the list of pending todo items:</p>
<todo-list [todos]="todos"
(toggle)="toggleCompletion($event)">
</todo-list>
`
})
class TodoApp {
todos: Todo[] = [{
label: 'Buy milk',
completed: false
}, {
label: "Save the world",
completed: false
}];
name: string = 'John';
addTodo(label: string) {
this.todos.push({
label,
completed: false
});
}
toggleCompletion(todo: Todo) {
todo.completed = !todo.completed;
}
}
//启动
bootstrap(TodoApp);
查看完整的代码
InputBox组件:
首先看模块的引入。
//导入
import {Component, Input, Output, EventEmitter} from '@angular/core';
import {bootstrap} from '@angular/platform-browser-dynamic';
引入了@Component, @Input, @Output,装饰器和EventEmitter类。@Input和@Output用来声明组件的输入和输出。EventEmitter是一个通用类,可接收泛型参数,它和@output装饰器结合使用,用来产生输出。
再来看组件的声明。
//InputBox组件
@Component({
//需要传对象字面量的组件装饰器
selector: 'input-box',
template: `
<input #todoInput [placeholder]="inputPlaceholder">
<button (click)="emitText(todoInput.value); todoInput.value = '';">
{{buttonLabel}}
</button>
`
})
//定义了输出和输入的InputBox类
class InputBox {
@Input() inputPlaceholder: string;
@Input() buttonLabel: string;
@Output() inputText = new EventEmitter<string>();
emitText(text: string) {
this.inputText.emit(text);
}
}
<input #todoInput [placeholder]="inputPlaceholder"> 模板声明了一个id名为todoInput的文本输入框,placeholder的值为inputPlaceholder,它也是InputBox类定义的第一个输入项
@Input() inputPlaceholder: string; 。buttonLabel也是类似的需要在InputBox类中通过@Input @Input() buttonLabel: string; 定义输入项。
(click)="emitText(todoInput.value); todoInput.value = '';" 有点长,分别解读。首先来看点击click,那么执行emitText函数,并且todoInput的value设置为空。那么对应输出项定义如下。
@Output() inputText = new EventEmitter<string>();
//定义了一个名称为inputText的输出,初始值为EventEmitter。
//所有组件的输出内容都是EventEmitter<string>的实例
emitText(text: string) {
//emitText内部调用了inputText实例的emit方法,把文本输入框的值作为参数传递过去。
this.inputText.emit(text);
}
inputText最终是在input-box的组件中使用。
<input-box inputPlaceholder="New todo..."
buttonLabel="Add"
(inputText)="addTodo($event)">
</input-box>
类似的ToDoList组件:
只看类的定义
class TodoList {
@Input() todos: Todo[];
//todos是ngFor指令接收的参数,那么是组件的输入
@Output() toggle = new EventEmitter<Todo>();
//只要todo项目的完成状态发生改变,组件都会触发change事件。属于组件的输出。
toggleCompletion(index: number) {
let todo = this.todos[index];
this.toggle.emit(todo);
}
}
6.传递输入与使用输出结果
定义好了组件如何把它们整合起来实现完整的应用呢?
最后看一下TodoApp组件,通过 bootstrap(TodoApp); 可以看出TodoApp是最顶层的组件,如何整合到一起的,就看装饰器的模板,魔法就在这里。
<h1>Hello {{name}}!</h1> <p>
Add a new todo:
<input-box inputPlaceholder="New todo..."
buttonLabel="Add"
(inputText)="addTodo($event)">
</input-box>
<!--使用了input-box组件,
使用了组件定义的输入项inputPlaceholder和buttonLabel。如果值是表达式,需要裹一层方括号
使用了输出项inputText-->
</p> <p>Here's the list of pending todo items:</p>
<todo-list [todos]="todos" (toggle)="toggleCompletion($event)"> </todo-list>
<!--输入项todos,因为值是变量,所以裹了一层方括号-->
<!--输出项toggle->
事件冒泡的分析:
<input-box inputPlaceholder="New todo..."
buttonLabel="Add"
(click)="handleClick($event)"
(inputText)="addTodo($event)">
</input-box>
<!--假设有click事件咯-->
<!--input-box模板的内容-->
<input #todoInput [placeholder]="inputPlaceholder">
<button (click)="emitText(todoInput.value); todoInput.value = '';">
{{buttonLabel}}
</button>
angular的事件冒泡与DOM事件相同。用户点击input-box组件内部模板中的按钮,就会执行handleClick($event)表达式。handleClick的第一个参数有一个属性叫target,指向按钮,但currentTarget属性指向input-box标签。
同时,要强调的是EventEmitter触发的事件是不会冒泡的。
7.重命名组件的输入输出
首先在input和outut装饰器重命名:
class TodoList {
@Input('todos') todosList: Todo[];
//todos重命名
@Output('toggle') toggleEvent = new EventEmitter<Todo>();
//toggle重命名
toggle(index: number) {
let todo = this.todos[index];
this.toggle.emit(todo);
}
}
然后在使用的地方依然保持一致就好了。
<todo-list [todos]="todos" (toggle)="toggleCompletion($event)"> </todo-list>
二: 内容投影详解
1.ng-content指令的使用
app/ch4/ts/ng-content/app.ts节选:
@Component({
selector: 'fancy-button',
template: '<button>点击我</button>'
})
class FancyButton { /* Extra behavior */ }
//组件设置了内联模板和选择器
那么我们就可以使用该组件了。
<fancy-button></fancy-button>
这里的fancy-button组件可复用并不高,那么使用ng-content指令就可以动态修改标签了。接下来的使用是组件的标签内容就可以替换ng-content指令所占的内容了。
@Component({
selector: 'fancy-button',
template: '<button><ng-content></ng-content></button>'
})
class FancyButton { /* Extra behavior */ }
在顶级组件的装饰器模板里可以看到使用方法
@Component({
selector: 'app',
template: `
<fancy-button>
<span>I will <i>be</i> projected</span>
</fancy-button>
//fancy-button的使用
<br>
<panel>
<panel-title>Sample title</panel-title>
<panel-content>Content</panel-content>
</panel>
`,
directives: [FancyButton, Panel]
})
2.投射多块内容
例如panel组件,包含标题区域和主题区域。
<div class="panel">
<div class="panel-title">
标题区域
</div>
<div class="panel-content">
主题区域
</div>
</div>`
那么我们定义组件就可以这样做了
@Component({
selector: 'panel',
styles: [
`.panel {
width: auto;
display: inline-block;
border: 1px solid black;
}
.panel-title {
border-bottom: 1px solid black;
background-color: #eee;
}
.panel-content,
.panel-title {
padding: 5px;
}`
],
template: `
<div class="panel">
<div class="panel-title">
<ng-content select="panel-title"></ng-content>
<!--标题区域-->
</div>
<div class="panel-content">
<ng-content select="panel-content"></ng-content>
<!--主题区域-->
</div>
</div>`
})
class Panel { }
重点看组件的模板。定义了一个class为panel的div,包含标题区域和主体区域。通过selet属性匹配panel内部的标签。
3.组件嵌套
@Component({
selector: 'sample-component',
template: '<view-child></view-child>'
})
class Sample { /* Extra behavior */ }
嵌套的代码
<sample-component>
<content-child></content-child>
</sample-component>




![【电子书】[教材辅导] 《四个维度的教育:学习者迈向成功的必备素养》[查尔斯·菲德尔][epub+mobi+azw3] 【电子书】[教材辅导] 《四个维度的教育:学习者迈向成功的必备素养》[查尔斯·菲德尔][epub+mobi+azw3]](https://www.04ip.com/template/qe/style/noimg/12.jpg)

