作者:王芃 [email protected]
第一節:Angular2 從0到1 (一)
第二節:Angular2 從0到1 (二)
第三節:建立一個待辦事項應用
這一章我們會建立一個更複雜的待辦事項應用,當然我們的登入功能也還保留,這樣的話我們的應用就有了多個相對獨立的功能子產品。以往的web應用根據不同的功能跳轉到不同的功能頁面。但目前前端的趨勢是開發一個SPA(Single Page Application 單頁應用),是以其實我們應該把這種跳轉叫視圖切換:根據不同的路徑顯示不同的元件。那我們怎麼處理這種視圖切換呢?幸運的是,我們無需尋找第三方元件,Angular官方内建了自己的路由子產品。
建立routing的步驟
由于我們要以路由形式顯示元件,建立路由前,讓我們先把
src\app\app.component.html
中的
<app-login></app-login>
删掉。
第一步:在
src/index.html
中指定基準路徑,即在
<header>
中加入
<base href="/" target="_blank" rel="external nofollow" >
,這個是指向你的
index.html
所在的路徑,浏覽器也會根據這個路徑下載下傳css,圖像和js檔案,是以請将這個語句放在header的最頂端。
第二步:在
src/app/app.module.ts
中引入RouterModule:
import { RouterModule } from '@angular/router';
第三步:定義和配置路由數組,我們暫時隻為login來定義路由,仍然在
src/app/app.module.ts
中的imports中
imports: [
BrowserModule,
FormsModule,
HttpModule,
RouterModule.forRoot([
{
path: 'login',
component: LoginComponent
}
])
],
注意到這個形式和其他的比如BrowserModule、FormModule和HTTPModule表現形式好像不太一樣,這裡解釋一下,forRoot其實是一個靜态的工廠方法,它傳回的仍然是Module,下面的是Angular API文檔給出的
RouterModule.forRoot
的定義。
為什麼叫forRoot呢?因為這個路由定義是應用在應用根部的,你可能猜到了還有一個工廠方法叫forChild,後面我們會詳細講。接下來我們看一下forRoot接收的參數,參數看起來是一個數組,每個數組元素是一個
{path: 'xxx', component: XXXComponent}
這個樣子的對象。這個數組就叫做路由定義(RouteConfig)數組,每個數組元素就叫路由定義,目前我們隻有一個路由定義。路由定義這個對象包括若幹屬性:
- path:路由器會用它來比對路由中指定的路徑和浏覽器位址欄中的目前路徑,如 /login 。
- component:導航到此路由時,路由器需要建立的元件,如
。LoginComponent
- redirectTo:重定向到某個path,使用場景的話,比如在使用者輸入不存在的路徑時重定向到首頁。
- pathMatch:路徑的字元比對政策
-
children:子路由數組
運作一下,我們會發現出錯了
這個錯誤看上去應該是對于”沒有找到比對的route,這是由于我們隻定義了一個’login’,我們再試試在浏覽器位址欄輸入:
。這次仍然出錯,但錯誤資訊變成了下面的樣子,意思是我們沒有找到一個outlet去加載LoginComponent。對的,這就引出了router outlet的概念,如果要顯示對應路由的元件,我們需要一個插頭(outlet)來裝載元件。http://localhost:4200/login
error_handler.js:EXCEPTION: Uncaught (in promise): Error: Cannot find primary outlet to load 'LoginComponent'
Error: Cannot find primary outlet to load 'LoginComponent'
at getOutlet (http://localhost:/main.bundle.js::)
at ActivateRoutes.activateRoutes (http://localhost:/main.bundle.js::)
at http://localhost:/main.bundle.js::
at Array.forEach (native)
at ActivateRoutes.activateChildRoutes (http://localhost:/main.bundle.js::)
at ActivateRoutes.activate (http://localhost:/main.bundle.js::)
at http://localhost:/main.bundle.js::
at SafeSubscriber._next (http://localhost:/main.bundle.js::)
at SafeSubscriber.__tryOrSetError (http://localhost:/main.bundle.js::)
at SafeSubscriber.next (http://localhost:/main.bundle.js::)
下面我們把
<router-outlet></router-outlet>
寫在
src\app\app.component.html
的末尾,位址欄輸入
http://localhost:4200/login
重新看看浏覽器中的效果吧,我們的應用應該正常顯示了。但如果輸入
http://localhost:4200
時仍然是有異常出現的,我們需要添加一個路由定義來處理。輸入
http://localhost:4200
時相對于根路徑的path應該是空,即”。而我們這時希望将使用者仍然引導到登入頁面,這就是
redirectTo: 'login'
的作用。
pathMatch: 'full'
的意思是必須完全符合路徑的要求,也就是說
http://localhost:4200/1
是不會比對到這個規則的,必須嚴格是
http://localhost:4200
RouterModule.forRoot([
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
},
{
path: 'login',
component: LoginComponent
}
])
注意路徑配置的順序是非常重要的,Angular2使用“先比對優先”的原則,也就是說如果一個路徑可以同時比對幾個路徑配置的規則的話,以第一個比對的規則為準。
但是現在還有一點小不爽,就是直接在
app.modules.ts
中定義路徑并不是很好的方式,因為随着路徑定義的複雜,這部分最好還是用單獨的檔案來定義。現在我們建立一個檔案
src\app\app.routes.ts
,将上面在
app.modules.ts
中定義的路徑删除并在
app.routes.ts
中重新定義。
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.component';
export const routes: Routes = [
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
},
{
path: 'login',
component: LoginComponent
}
];
export const routing = RouterModule.forRoot(routes);
接下來我們在
app.modules.ts
中引入routing,
import { routing } from './app.routes';
,然後在imports數組裡添加routing,現在我們的
app.modules.ts
看起來是下面這個樣子。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { Routes, RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { AuthService } from './services/auth.service';
import { routing } from './app.routes';
@NgModule({
declarations: [
AppComponent,
LoginComponent,
TodoComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
routing
],
providers: [
{provide: 'auth', useClass: AuthService}
],
bootstrap: [AppComponent]
})
export class AppModule { }
讓待辦事項變得有意義
現在我們來規劃一下根路徑”,對應根路徑我們想建立一個todo元件,那麼我們使用
ng g c todo
來生成元件,然後在
app.routes.ts
中加入路由定義,對于根路徑我們不再需要重定向到登入了,我們把它改寫成重定向到todo。
export const routes: Routes = [
{
path: '',
redirectTo: 'todo',
pathMatch: 'full'
},
{
path: 'todo',
component: TodoComponent
},
{
path: 'login',
component: LoginComponent
}
];
在浏覽器中鍵入
http://localhost:4200
可以看到自動跳轉到了todo路徑,并且我們的todo元件也顯示出來了。
我們希望的Todo頁面應該有一個輸入待辦事項的輸入框和一個顯示待辦事項狀态的清單。那麼我們先來定義一下todo的結構,todo應該有一個id用來唯一辨別,還應該有一個desc用來描述這個todo是幹什麼的,再有一個completed用來辨別是否已經完成。好了,我們來建立這個todo模型吧,在todo檔案夾下建立一個檔案
todo.model.ts
export class Todo {
id: number;
desc: string;
completed: boolean;
}
然後我們應該改造一下todo元件了,引入剛剛建立好的todo對象,并且建立一個todos數組作為所有todo的集合,一個desc是目前添加的新的todo的内容。當然我們還需要一個addTodo方法把新的todo加到todos數組中。這裡我們暫且寫一個漏洞百出的版本。
import { Component, OnInit } from '@angular/core';
import { Todo } from './todo.model';
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css']
})
export class TodoComponent implements OnInit {
todos: Todo[] = [];
desc = '';
constructor() { }
ngOnInit() {
}
addTodo(){
this.todos.push({id: , desc: this.desc, completed: false});
this.desc = '';
}
}
然後我們改造一下
src\app\todo\todo.component.html
<div>
<input type="text" [(ngModel)]="desc" (keyup.enter)="addTodo()">
<ul>
<li *ngFor="let todo of todos">{{ todo.desc }}</li>
</ul>
</div>
如上面代碼所示,我們建立了一個文本輸入框,這個輸入框的值應該是新todo的描述(desc),我們想在使用者按了Enter鍵後進行添加操作(
(keyup.enter)="addTodo()
)。由于todos是個數組,是以我們利用一個循環将數組内容顯示出來(
<li *ngFor="let todo of todos">{{ todo.desc }}</li>
)。好了讓我們欣賞一下成果吧
如果我們還記得之前提到的業務邏輯應該放在單獨的service中,我們還可以做的更好一些。在todo檔案夾内建立TodoService:
ng g s todo\todo
。上面的例子中所有建立的todo都是id為1的,這顯然是一個大bug,我們看一下怎麼處理。常見的不重複id建立方式有兩種,一個是搞一個自增長數列,另一個是采用随機生成一組不可能重複的字元序列,常見的就是UUID了。我們來引入一個uuid的包:
npm i --save angular2-uuid
,由于這個包中已經含有了用于typescript的定義檔案,這裡就執行這一個指令就足夠了。
然後修改service成下面的樣子:
import { Injectable } from '@angular/core';
import {Todo} from './todo.model';
import { UUID } from 'angular2-uuid';
@Injectable()
export class TodoService {
todos: Todo[] = [];
constructor() { }
addTodo(todoItem:string): Todo[] {
let todo = {
id: UUID.UUID(),
desc: todoItem,
completed: false
};
this.todos.push(todo);
return this.todos;
}
}
當然我們還要把元件中的代碼改成使用service的
import { Component, OnInit } from '@angular/core';
import { Todo } from './todo.model';
import { TodoService } from './todo.service';
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css'],
providers:[TodoService]
})
export class TodoComponent implements OnInit {
todos: Todo[] = [];
desc = '';
constructor(private service:TodoService) { }
ngOnInit() {
}
addTodo(){
this.todos = this.service.addTodo(this.desc);
this.desc = '';
}
}
為了可以清晰的看到我們的成果,我們為chrome浏覽器裝一個插件,在chrome的位址欄中輸入
chrome://extensions
,拉到最底部會看到一個“擷取更多擴充程式”的連結,點選這個連結然後搜尋“Angury”,安裝即可。安裝好後,按F12調出開發者工具,裡面出現一個叫“Angury”的tab。
我們可以看到id這時候被設定成了一串字元,這個就是UUID了。
建立模拟web服務和異步操作
實際的開發中我們的service是要和伺服器api進行互動的,而不是現在這樣簡單的操作數組。但問題來了,現在沒有web服務啊,難道真要自己開發一個嗎?答案是可以做個假的,假作真時真亦假。我們在開發過程中經常會遇到這類問題,等待後端同學的進度是很痛苦的。是以Angular内建提供了一個可以快速建立測試用web服務的方法:記憶體 (in-memory) 伺服器。
一般來說,你需要知道自己對伺服器的期望是什麼,期待它傳回什麼樣的資料,有了這個資料呢,我們就可以自己快速的建立一個記憶體伺服器了。拿這個例子來看,我們可能需要一個這樣的對象
class Todo {
id: string;
desc: string;
completed: boolean;
}
對應的JSON應該是這樣的
{
"data": [
{
"id": "f823b191-7799-438d-8d78-fcb1e468fc78",
"desc": "blablabla",
"completed": false
},
{
"id": "c316a3bf-b053-71f9-18a3-0073c7ee3b76",
"desc": "tetssts",
"completed": false
},
{
"id": "dd65a7c0-e24f-6c66-862e-0999ea504ca0",
"desc": "getting up",
"completed": false
}
]
}
現在我們在todo檔案夾下建立一個
src\app\todo\todo.model.ts
export class Todo {
id: string;
desc: string;
completed: boolean;
}
然後在同一檔案夾下建立一個檔案
src\app\todo\todo-data.ts
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Todo } from './todo.model';
export class InMemoryTodoDbService implements InMemoryDbService {
createDb() {
let todos: Todo[] = [
{id: "f823b191-7799-438d-8d78-fcb1e468fc78", desc: 'Getting up', completed: true},
{id: "c316a3bf-b053-71f9-18a3-0073c7ee3b76", desc: 'Go to school', completed: false}
];
return {todos};
}
}
可以看到,我們建立了一個實作
InMemoryDbService
的記憶體資料庫,這個資料庫其實也就是把數組傳入進去。接下來我們要更改
src\app\app.module.ts
,加入類引用和對應的子產品聲明:
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryTodoDbService } from './todo/todo-data';
然後在imports數組中緊挨着
HttpModule
加上
InMemoryWebApiModule.forRoot(InMemoryTodoDbService),
。
現在我們在service中試着調用我們的“假web服務”吧
import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { UUID } from 'angular2-uuid';
import 'rxjs/add/operator/toPromise';
import { Todo } from './todo.model';
@Injectable()
export class TodoService {
//定義你的假WebAPI位址,這個定義成什麼都無所謂
//隻要確定是無法通路的位址就好
private api_url = 'api/todos';
private headers = new Headers({'Content-Type': 'application/json'});
constructor(private http: Http) { }
// POST /todos
addTodo(desc:string): Promise<Todo> {
let todo = {
id: UUID.UUID(),
desc: desc,
completed: false
};
return this.http
.post(this.api_url, JSON.stringify(todo), {headers: this.headers})
.toPromise()
.then(res => res.json().data as Todo)
.catch(this.handleError);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error);
return Promise.reject(error.message || error);
}
}
上面的代碼我們看到定義了一個
api_url = 'api/todos'
,你可能會問這個是怎麼來的?其實這個我們改寫成
api_url = 'blablabla/nahnahnah'
也無所謂,因為這個記憶體web服務的機理是攔截web通路,也就是說随便什麼位址都可以,記憶體web服務會攔截這個位址并解析你的請求是否滿足RESTful API的要求。
簡單來說RESTful API中GET請求用于查詢,PUT用于更新,DELETE用于删除,POST用于添加。比如如果url是api/todos,那麼
- 查詢所有待辦事項:以GET方法通路
api/todos
- 查詢單個待辦事項:以GET方法通路
,比如id是1,那麼通路api/todos/id
api/todos/1
- 更新某個待辦事項:以PUT方法通路
api/todos/id
- 删除某個待辦事項:以DELETE方法通路
api/todos/id
- 增加一個待辦事項:以POST方法通路
api/todos
在service的構造函數中我們注入了Http,而angular的Http封裝了大部分我們需要的方法,比如例子中的增加一個todo,我們就調用
this.http.post(url, body, options)
,上面代碼中的
.post(this.api_url, JSON.stringify(todo), {headers: this.headers})
含義是:構造一個POST類型的HTTP請求,其通路的url是
this.api_url
,request的body是一個JSON(把todo對象轉換成JSON),在參數配置中我們配置了request的header。
這個請求發出後傳回的是一個Observable(可觀察對象),我們把它轉換成Promise然後處理res(Http Response)。Promise提供異步的處理,注意到then中的寫法,這個和我們傳統程式設計寫法不大一樣,叫做lamda表達式,相當于是一個匿名函數,
(input parameters) => expression
,
=>
前面的是函數的參數,後面的是函數體。
還要一點需要強調的是:在用記憶體Web服務時,一定要注意
res.json().data
中的data屬性必須要有,因為記憶體web服務坑爹的在傳回的json中加了data對象,你真正要得到的json是在這個data裡面。
下一步我們來更改Todo元件的addTodo方法以便可以使用我們新的異步http方法
addTodo(){
this.service
.addTodo(this.desc)
.then(todo => {
this.todos = [...this.todos, todo];
this.desc = '';
});
}
這裡面的前半部分應該還是好了解的:
this.service.addTodo(this.desc)
是調用service的對應方法而已,但後半部分是什麼鬼?
...
這個貌似省略号的東東是ES7中計劃提供的Object Spread操作符,它的功能是将對象或數組“打散,拍平”。這麼說可能還是不懂,舉例子吧:
let arr = [,,];
let arr2 = [...arr];
arr2.push();
// arr2 變成了 [1,2,3,4]
// arr 儲存原來的樣子
let arr3 = [, , ];
let arr4 = [, , ];
arr3.push(...arr4);
// arr3變成了[0, 1, 2, 3, 4, 5]
let arr5 = [, , ];
let arr6 = [-, ...arr5, ];
// arr6 變成了[-1, 0, 1, 2, 3]
是以呢我們上面的
this.todos = [...this.todos, todo];
相當于為todos增加一個新元素,和push很像,那為什麼不用push呢?因為這樣構造出來的對象是全新的,而不是引用的,在現代程式設計中一個明顯的趨勢是不要在過程中改變輸入的參數。第二個原因是這樣做會帶給我們極大的便利性和程式設計的一緻性。下面通過給我們的例子添加幾個功能,我們來一起體會一下。
首先更改
src\app\todo\todo.service.ts
//src\app\todo\todo.service.ts
import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { UUID } from 'angular2-uuid';
import 'rxjs/add/operator/toPromise';
import { Todo } from './todo.model';
@Injectable()
export class TodoService {
private api_url = 'api/todos';
private headers = new Headers({'Content-Type': 'application/json'});
constructor(private http: Http) { }
// POST /todos
addTodo(desc:string): Promise<Todo> {
let todo = {
id: UUID.UUID(),
desc: desc,
completed: false
};
return this.http
.post(this.api_url, JSON.stringify(todo), {headers: this.headers})
.toPromise()
.then(res => res.json().data as Todo)
.catch(this.handleError);
}
// PUT /todos/:id
toggleTodo(todo: Todo): Promise<Todo> {
const url = `${this.api_url}/${todo.id}`;
console.log(url);
let updatedTodo = Object.assign({}, todo, {completed: !todo.completed});
return this.http
.put(url, JSON.stringify(updatedTodo), {headers: this.headers})
.toPromise()
.then(() => updatedTodo)
.catch(this.handleError);
}
// DELETE /todos/:id
deleteTodoById(id: string): Promise<void> {
const url = `${this.api_url}/${id}`;
return this.http
.delete(url, {headers: this.headers})
.toPromise()
.then(() => null)
.catch(this.handleError);
}
// GET /todos
getTodos(): Promise<Todo[]>{
return this.http.get(this.api_url)
.toPromise()
.then(res => res.json().data as Todo[])
.catch(this.handleError);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error);
return Promise.reject(error.message || error);
}
}
然後更新
src\app\todo\todo.component.ts
import { Component, OnInit } from '@angular/core';
import { TodoService } from './todo.service';
import { Todo } from './todo.model';
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css'],
providers: [TodoService]
})
export class TodoComponent implements OnInit {
todos : Todo[];
desc = '';
constructor(private service: TodoService) {}
ngOnInit() {
this.getTodos();
}
addTodo(){
this.service
.addTodo(this.desc)
.then(todo => {
this.todos = [...this.todos, todo];
this.desc = '';
});
}
toggleTodo(todo: Todo) {
const i = this.todos.indexOf(todo);
this.service
.toggleTodo(todo)
.then(t => {
this.todos = [
...this.todos.slice(,i),
t,
...this.todos.slice(i+)
];
});
}
removeTodo(todo: Todo) {
const i = this.todos.indexOf(todo);
this.service
.deleteTodoById(todo.id)
.then(()=> {
this.todos = [
...this.todos.slice(,i),
...this.todos.slice(i+)
];
});
}
getTodos(): void {
this.service
.getTodos()
.then(todos => this.todos = [...todos]);
}
}
更新模闆檔案
src\app\todo\todo.component.html
<section class="todoapp">
<header class="header">
<h1>Todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="desc" (keyup.enter)="addTodo()">
</header>
<section class="main" *ngIf="todos?.length > 0">
<input class="toggle-all" type="checkbox">
<ul class="todo-list">
<li *ngFor="let todo of todos" [class.completed]="todo.completed">
<div class="view">
<input class="toggle" type="checkbox" (click)="toggleTodo(todo)" [checked]="todo.completed">
<label (click)="toggleTodo(todo)">{{todo.desc}}</label>
<button class="destroy" (click)="removeTodo(todo); $event.stopPropagation()"></button>
</div>
</li>
</ul>
</section>
<footer class="footer" *ngIf="todos?.length > 0">
<span class="todo-count">
<strong>{{todos?.length}}</strong> {{todos?.length == 1 ? 'item' : 'items'}} left
</span>
<ul class="filters">
<li><a href="">All</a></li>
<li><a href="">Active</a></li>
<li><a href="">Completed</a></li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
</section
更新元件的css樣式:
src\app\todo\todo.component.css
.todoapp {
background: #fff;
margin: px px ;
position: relative;
box-shadow: px px rgba(, , , ),
px px rgba(, , , );
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: ;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: ;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: ;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -px;
width: %;
font-size: px;
font-weight: ;
text-align: center;
color: rgba(, , , );
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: ;
width: %;
font-size: px;
font-family: inherit;
font-weight: inherit;
line-height: em;
border: ;
color: inherit;
padding: px;
border: px solid #999;
box-shadow: inset -px px rgba(, , , );
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: px px px px;
border: none;
background: rgba(, , , );
box-shadow: inset -px px rgba(,,,);
}
.main {
position: relative;
z-index: ;
border-top: px solid #e6e6e6;
}
label[for='toggle-all'] {
display: none;
}
.toggle-all {
position: absolute;
top: -px;
left: -px;
width: px;
height: px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '❯';
font-size: px;
color: #e6e6e6;
padding: px px px px;
}
.toggle-all:checked:before {
color: #737373;
}
.todo-list {
margin: ;
padding: ;
list-style: none;
}
.todo-list li {
position: relative;
font-size: px;
border-bottom: px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: ;
}
.todo-list li.editing .edit {
display: block;
width: px;
padding: px px;
margin: px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: ;
bottom: ;
margin: auto ;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
word-break: break-all;
padding: px px px px;
margin-left: px;
display: block;
line-height: ;
transition: color s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: ;
right: px;
bottom: ;
width: px;
height: px;
margin: auto ;
font-size: px;
color: #cc9a9a;
margin-bottom: px;
transition: color s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -px;
}
.footer {
color: #777;
padding: px px;
height: px;
text-align: center;
border-top: px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: ;
bottom: ;
left: ;
height: px;
overflow: hidden;
box-shadow: px px rgba(, , , ),
px -px #f6f6f6,
px px -px rgba(, , , ),
px -px #f6f6f6,
px px -px rgba(, , , );
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: ;
}
.filters {
margin: ;
padding: ;
list-style: none;
position: absolute;
right: ;
left: ;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: px;
padding: px px;
text-decoration: none;
border: px solid transparent;
border-radius: px;
}
.filters li a:hover {
border-color: rgba(, , , );
}
.filters li a.selected {
border-color: rgba(, , , );
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: px;
}
.toggle-all {
-webkit-transform: rotate(deg);
transform: rotate(deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: px) {
.footer {
height: px;
}
.filters {
bottom: px;
}
}
更新
src\styles.css
為如下
/* You can add global styles to this file, and also import other style files */
html, body {
margin: ;
padding: ;
}
button {
margin: ;
padding: ;
border: ;
background: none;
font-size: %;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: em;
background: #f5f5f5;
color: #4d4d4d;
min-width: px;
max-width: px;
margin: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: ;
}
:focus {
outline: ;
}
.hidden {
display: none;
}
.info {
margin: px auto ;
color: #bfbfbf;
font-size: px;
text-shadow: px rgba(, , , );
text-align: center;
}
.info p {
line-height: ;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: ;
}
.info a:hover {
text-decoration: underline;
}
現在我們看看成果吧,現在好看多了
第一節:Angular2 從0到1 (一)
第二節:Angular2 從0到1 (二)