--- tags: 學習筆記, Web --- Angular ======= ## TypeScript Angular以TypeScript為開發語言,TypeScript是一個為了改善JavaScript諸多問題而誕生的語言。 ### 語法 ### const & let ```typescript= const cannotChange = 9; // 只要改變這個常數,程式會出錯 let canChange = [{'a': '1'}, {'b': '2'}]; //在不改變宣告的資料型別的情況下,可以改變這個參數 ``` ### 資料型別 ```typescript= //Number let decimal: number = 6; let hex: number = 0xf00d; let binary: number = 0b1010; let octal: number = 0o744; //Stirng let person: string = "Mike"; //可以用 "" let age: number = 37; let sentence: string = `Oh, ${person} is ${age} years old.`; //也可以用 `${}` //上面等於 "Oh, " + person + " is " + age + " years old." //Array let list: number[] = [1, 2, 3]; //Tuple let x: [string, number]; // Array中包含不同型別的變數用 Tuple x = ["hello", 10]; // OK x = [10, "hello"]; // Error x[3] = true // Error 往後的變數只能是一開始設定的 string 或 number ``` ### 型別註釋 明確參數型別 ```typescript= function greeter(person: string) { return "Hello, " + person; } ``` ### Interface 使物件有更明確的結構型態。 ```typescript= interface Person { firstName: string; lastName: string; } function greeter(person: Person) { return "Hello, " + person.firstName + " " + person.lastName; } ``` ### 類別 ```typescript= class Student { fullName: string; constructor(public firstName, public middleInitial, public lastName) { this.fullName = `${firstName} ${middleInitial} ${lastName}`; } } interface Person { firstName: string; lastName: string; } function greeter(person : Person) { return `Hello, ${person.firstName} ${person.lastName}`; } let user = new Student("Jane", "M.", "User"); console.log(greeter(user)); ``` 同樣存在static, extends, implements ### 迭代 ```typescript= let list = [4, 5, 6]; for (let i in list) { console.log(i); // "0", "1", "2", } for (let i of list) { console.log(i); // "4", "5", "6" } ``` 等價於 ```typescript= let list = [4, 5, 6]; for (let _i = 0; _i < list.length; _i++) { var num = list[_i]; console.log(num); } ``` ## Angular 架構 ### Component Component 是一個用 TS 寫出來的 Class,這個 Class 之後會被編譯成 JS。而這些 Component 最後可以被包裝變成獨立的 Module 讓其他 Mudule 使用。 一個 Module 可以包裝很多 Component 給其他 Module 使用。 ```typescript= //app.component.ts import { Component } from '@angular/core'; @Component({ moduleId: module.id, selector: 'my-app', templateUrl: 'app.component.html', styleUrls: ['app.component.css'] }) export class AppComponent implements OnInit { //Do Something } ``` ```typescript= //app.module.ts import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; //引入 Component import { BrowserModule } from '@angular/platform-browser'; @NgModule({ imports: [ BrowserModule ], providers: [ Logger ], declarations: [ AppComponent ], exports: [ AppComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } ``` Component 用稱作 decorator function的 @Component來表示為 Metadata 物件。 Metadata 用來綁定 HTML template 、 CSS 和 Component。 至於 selector 則是告訴 Angular 這個 Component 要放在 index.html 裡的自訂標籤 <my-app>。 ```html= //index.html <body> ... <my-app>Loading AppComponent content here ...</my-app> ... ``` ### Directive * Components directives: 用來控制 HTML Template。是我們最常用到的 Directive。 * 結構性 directives:例如 ngFor (用來改變 HTML DOM,可以是增加或刪掉) ```html= <div *ngFor="let hero of heroes">{{hero.fullName}}</div> ``` * 屬性 directives: 例如 ngStyle (用來改變 Elements 的 Style) ```html= <p [style.background]="'lime'">I am green with envy!</p> ``` @Directive適合直接用在已經存在的 DOM element。可以重複使用,一個 DOM element 可以有很 Directive。 @Component適合建立一個可以重複使用的 DOM element,通常用來建立 UI工具,此外一個 DOM elelment 只能有一個 Component,而且必須搭配@View或是templateUrl。 ### Data Binging 目標=來源 * One Way Binding 可以是任何 HTML property。像是innerText style。 * 從數據來源綁定看到的目標用的是: ```html= <h1 [innerText]="employee.name"></h1> <h1>{{employee.name}}</h1> <span [style.backgroundColor]="employee.favouriteColor"></span> <span bind-style.backgroundColor="employee.favouriteColor"></span> ``` * 從看到的目標綁定數據來源用的是: ```html= <span (style.backgroundColor)="employee.favouriteColor"></span> <span on-style.backgroundColor="employee.favouriteColor"></span> ``` * Two Way Binding 可以由使用者控制,像是選單、輸入,也可以由程式控制。 ```html= <input [(ngModel)]="employee.name"/> <input bindon-ngModel="employee.name"/> ``` * Event Binding 綁定事件,像是click、focus或blur ```html= <button (click)="sendForm()">Send</h1> ``` ```typescript= deleteRequest = new EventEmitter<Hero>(); delete() { this.deleteRequest.emit(this.hero); } ``` ```html= <div> ... <button (click)="delete()">Delete</button> ... </div> <hero-detail (deleteRequest)="deleteHero($event)" [hero]="currentHero"></hero-detail> ``` ### Dependency injection 一個 Injector (注入)用 providers 建立 dependencies(依賴)。 Providers 會知道如何去建立 dependencies。 TS 中的型別註釋(Type annotations)可以被用來要求 dependencies 。此外每個 Component 都會有自己的 injector,組成一個架構譜。 用一個一個 provider 如 @NgModule、 @Component或@Directive來建立 Injector 在 Component 的 constructor 注入服務。 > * 一個 Injector (注入)用 providers 建立 dependencies(依賴)。 Providers 會知道如何去建立 dependencies。 > * TS 中的型別註釋(Type annotations)可以被用來要求 dependencies 。此外每個 Component 都會有自己的 injector,組成一個架構譜。 > * 用一個一個 provider 如 @NgModule、 @Component或@Directive來建立 Injector > * 在 Component 的 constructor 注入服務。 ```typescript= export class Car { public description = 'DI'; constructor(public engine: Engine, public tires: Tires) { } } let car = new Car(new Engine(), new Tires()); let bigCylinders = 12; let car = new Car(new V8Engine(bigCylinders), new Tires()); ``` 一個 Class 從外部接收依賴而非內部創建,這就是依賴注入(DI)。 > 依賴反轉原則 > 抽象不要依賴細節,細節要依賴抽象。 > 高階模組不應該依賴低階模組,低階模組應該由高階模組決定其依賴。 但每次要使用一個物件Car,需要自己製造零件Engine、Tires,還要自己組裝零件實在是很費工夫的事情,若是可以直接使用一個物件Car,但所以東西都製造也組裝好了,那該有多好!這時候我們有個injector已經幫你註冊組裝好,就如同品牌產品,直接領取一點也不費工夫,這便是注入依賴框架在做的事情囉! ```typescript= //直接開走,不需要自己裝輪胎,不需要自己裝引擎 let car = injector.get(Car); ``` ```typescript= import { Component } from 'angular2/core'; import { HeroListComponent } from './hero-list.component'; import { HeroService } from './hero.service'; @Component({ selector: 'my-heroes', template: ` <h2>Heroes</h2> <hero-list></hero-list> `, providers:[HeroService], directives:[HeroListComponent] }) export class HeroesComponent { } ``` ```typescript= import { Component } from 'angular2/core'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ selector: 'hero-list', template: ` <div *ngFor="#hero of heroes"> {{hero.id}} - {{hero.name}} </div> `, }) export class HeroListComponent { heroes: Hero[]; constructor(heroService: HeroService) { this.heroes = heroService.getHeroes(); } } ``` 會以隱形式 DI 完成。 ```typescript= import { Injectable } from '@angular/core'; @Injectable() export class Logger { logs: string[] = []; // capture logs for testing log(message: string) { this.logs.push(message); console.log(message); } } ``` @Component、@Directive、@Pipe,都是 Injectable 的子型。 因為都沒有裝飾器,所以需要加上 Injectable 來告訴 ts,讓他產生 metadata,才能正常編譯。 ### Angular 表單 * Component 和 Template 構建表單 * 雙向綁定 `[(ngModel)]` 讀寫表單表。 * ngControl追蹤表單狀態和有效性。 * 根據ngControl改變 CSS。 * 顯示驗證錯誤消息並啟用/禁止表單。 * 使用本地模板變量共享控制間的信息。 ### PIPES 每一個應用程式都是一些非常簡單的任務開始:獲取數據、轉換數據,並把它們顯示給用戶。 有時候想要把所有小寫字母,在顯示的時候變成都是大寫。 ```typescript= // app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'my-app', // uppercase 為 Angular 內建 template: '<p>My name is <strong>{{ name | capitalize }}</strong>.</p>', }) export class AppComponent { name = 'john doe'; } ``` ```typescript= import { Pipe, PipeTransform } from '@angular/core'; @Pipe({name: 'capitalize'}) export class CapitalizePipe implements PipeTransform { transform(value: string, args: string[]): any { if (!value) return value; return value.replace(/\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }); } } ``` 在 App Module 中引入我們寫好的 Pipe 這樣之後要用 Pipe 就不用每個 Component 都一一宣告囉! ```typescript= // app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { CapitalizePipe } from './capitalize.pipe'; // 導入我們的 pipe @NgModule({ imports: [ BrowserModule ], // 引入 capitalize pipe declarations: [ AppComponent, CapitalizePipe ], bootstrap: [ AppComponent ] }) export class AppModule { } ``` # Lazy Loading * 指定 app.routing.ts 中要 lazy load 的路由 (routes) * 建立 Component 路由文件 * 調整要被 lazy load 的 module 的內容 ```typescript= //app.routes.ts import {Routes, RouterModule} from "@angular/router"; import {ModuleWithProviders} from "@angular/core"; const appRoutes: Routes = [ // 假設有首頁 { path: "", redirectTo: "home", pathMatch: "full" }, // 要處理的 Product APP 頁面 { path: "product", loadChildren: "app/product/product.module#ProductModule" } ]; export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);;; ``` ```typescript= //product/product.routing.ts import {RouterModule} from "@angular/router"; import {ModuleWithProviders} from "@angular/core"; import {ProductComponent} from "./product.component"; // 小零件 ( 各種用到的東西 ) import {ProductsListComponent} from "./products-list.component"; import {ProductDetailsComponent} from "./product-details.component"; const routes = [ { path: "", //設定根目錄為這一層 component: ProductComponent, // Product 包含的小組件們 children: [ {path: "", component: ProductsListComponent}, {path: ":id", component: ProductDetailsComponent} ] } ]; export const routing: ModuleWithProviders = RouterModule.forChild(routes);//product/product.routing.ts import {RouterModule} from "@angular/router"; import {ModuleWithProviders} from "@angular/core"; import {ProductComponent} from "./product.component"; // 小零件 ( 各種用到的東西 ) import {ProductsListComponent} from "./products-list.component"; import {ProductDetailsComponent} from "./product-details.component"; const routes = [ { path: "", //設定根目錄為這一層 component: ProductComponent, // Product 包含的小組件們 children: [ {path: "", component: ProductsListComponent}, {path: ":id", component: ProductDetailsComponent} ] } ]; export const routing: ModuleWithProviders = RouterModule.forChild(routes); ``` ```typescript= //product/product.component.ts import {Component} from "@angular/core"; @Component({ template: ` <my-nav></my-nav> <h1>Products</h1> <router-outlet></router-outlet> ` }) export class ProductComponent {} ``` ```typescript= //product/product.module.ts import {NgModule} from "@angular/core"; import {ProductsService} from "./products.service"; import {ProductsListComponent} from "./products-list.component"; import {ProductDetailsComponent} from "./product-details.component"; import {routing} from "./product.routing"; import {ProductComponent} from "./product.component"; @NgModule({ declarations: [ProductComponent, ProductsListComponent, ProductDetailsComponent], imports: [routing], providers: [ProductsService], }) export class ProductModule {} ``` 模板載入組件 ```typescript= //app.component.ts import {Component} from '@angular/core' import {FastComponent} from 'src/components/fast' import {SlowComponent} from 'src/components/slow' @Component({ selector: 'template-load', template: ` <h1>方式一:template 動態載入</h1> <fast></fast> <slow></slow> `, directives: [ FastComponent, SlowComponent ] }) export class TemplateLoadComponent { constructor() { } } //src/components/slow.ts import { Component } from '@angular/core'; @Component({ selector: 'slow', template: ` <p>Slow Content</p> ` }) export class SlowComponent { } //src/components/fast.ts import { Component } from '@angular/core'; @Component({ selector: 'fast', template: ` <p>Fast Content</p> ` }) export class FastComponent { } ``` ### Router ```typescript= @Component({ ... }) @Routes([ {path: '/crisis-center', component: CrisisListComponent}, {path: '/heroes', component: HeroListComponent}, {path: '/hero/:id', component: HeroDetailComponent} // 參數傳址 /hero/58 ]) export class AppComponent implements OnInit { constructor(private router: Router) {} ngOnInit() { // 導向 crisis-center 頁面 this.router.navigate(['/crisis-center']); } } ``` 傳至router-outlet ```htmlm= //app.component.html <h1>Component Router</h1> <nav> <a [routerLink]="['/crisis-center']">Crisis Center</a> <a [routerLink]="['/heroes']">Heroes</a> <!-- 也可以不是陣列 <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> --> </nav> <router-outlet></router-outlet> ``` ### Animation ```typescript= import { Component, OnChanges, Input, trigger, state, animate, transition, style } from '@angular/core'; @Component({ selector : 'my-fader', animations: [ trigger('visibilityChanged', [ state('true' , style({ opacity: 1, transform: 'scale(1.0)' })), state('false', style({ opacity: 0, transform: 'scale(0.0)' })), transition('1 => 0', animate('300ms')), transition('0 => 1', animate('900ms')) ]) ], template: ` <div [@visibilityChanged]="isVisible" > <ng-content></ng-content> <p>Can you see me? I should fade in or out...</p> </div> ` }) export class FaderComponent implements OnChanges { @Input() isVisible : boolean = true; } ``` divdiv -> visibiliityChanged(isVisiblesible) ### API異地資料傳接 #### RxJS ```typescript= import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; @Injectable() export class HttpClientService { constructor (private http: Http) {} get(url: string, optionHeader?: {[header: string]: string}): Observable<Response> { let headers = new Headers(); // set option header for(let header in optionHeader) { let value = optionHeader[header]; headers.append(header, value); } return this.http.get(url, {headers: headers,}) .map(this.extractData) .catch(this.handleError); } private extractData(res: Response) { let body = res.json(); return body.data || { }; } private handleError (error: Response | any) { // In a real world app, we might use a remote logging infrastructure let errMsg: string; if (error instanceof Response) { const body = error.json() || ''; const err = body.error || JSON.stringify(body); errMsg = `${error.status} - ${error.statusText || ''} ${err}`; } else { errMsg = error.message ? error.message : error.toString(); } console.error(errMsg); return Observable.throw(errMsg); } } ``` ```typescript= getData() { this.HttpClientService.get(url:string) .subscribe( data => this.data = data, error => this.errorMessage = <any>error ); } ``` #### AJAX ```typescript= declare var $:any; // typescript看不懂$ 所以建立個$給他 $.ajax({ url: url, type: 'POST', data: {'data':JSON.stringify(json)}, success: function(data: any) { console.log(data); }, error: function(XMLHttpRequest: any, textStatus: any, errorThrown: any) { console.log(errorThrown); } }); ``` ### <ng-content> <ng-content> 嵌入式設計 ```typescript= // card.component.ts import { Component, Input, Output } from '@angular/core'; @Component({ selector: 'card', templateUrl: 'card.component.html', }) export class CardComponent { @Input() header: string = 'this is header'; @Input() footer: string = 'this is footer'; } ``` ```html= <!-- card.component.html --> <div class="card"> <div class="card-header"> {{ header }} </div> <!-- 單一嵌入點 --> <ng-content></ng-content> <div class="card-footer"> {{ footer }} </div> </div> ``` ```html= <!-- app.component.html --> <h1>Single slot transclusion</h1> <card header="my header" footer="my footer"> <!-- 放入要嵌入的動態內容 --> <div class="card-block"> <h4 class="card-title">You can put any content here</h4> <p class="card-text">For example this line of text and</p> <a href="#" class="btn btn-primary">This button</a> </div> <!-- 結束 --> <card> ``` ### NgModule 一個 Module ( 模組 ) 是一個把彼此互相關聯的 components、directives、pipes 和 services 整合的機制。然後這個模組可以再和其他模組結合,最後就形成我們的網頁應用程式。網頁就像拼圖一般,由許多小碎片的組件和一塊一塊的模組所構成。 一個模組又由點像是類別 ( Class ),一樣也有 public 和 private 的概念。應用程式只能取用公開的部分,私有的部分則看不見。 * declarations:模塊內部Components/Directives/Pipes的列表,聲明一下這個模塊內部成員 * providers:指定應用程式的根級別需要使用的service。(Angular2中沒有模塊級別的service,所有在NgModule中聲明的Provider都是註冊在根級別的Dependency Injector中) * imports:導入其他module,其它module暴露的出的Components、Directives、Pipes等可以在本module的組件中被使用。比如導入CommonModule後就可以使用NgIf、NgFor等指令。 * exports:用來控制將哪些內部成員暴露給外部使用。導入一個module並不意味著會自動導入這個module內部導入的module所暴露出的公共成員。除非導入的這個module把它內部導入的module寫到exports中。 * bootstrap:通常是app啟動的根組件,一般只有一個component。bootstrap中的組件會自動被放入到entryComponents中。 * entryCompoenents: 不會再模板中被引用到的組件。這個屬性一般情況下只有ng自己使用,一般是bootstrap組件或者路由組件,ng會自動把bootstrap、路由組件放入其中。 除非不通過路由動態將component加入到dom中,否則不會用到這個屬性。 透過@NgModule定義其為模組。 ```typescript= @NgModule({ imports: [ ... ], declarations: [ ... ], bootstrap: [ ... ] }) ``` ### CSS Shadow DOM ```typescript= @Component({ templateUrl: 'card.html', styles: [` .card { height: 70px; width: 100px; } `], encapsulation: ViewEncapsulation.Native // encapsulation: ViewEncapsulation.None // encapsulation: ViewEncapsulation.Emulated is default }) ``` * None: style 直接被包裝到 `<head>` 的 `<style>` 裡面 * Emulated: style 被包裝到 `<head>` 的 `<style>` 裡面,但可以辨識要對應的 Component * Native: 如同預期一般為網頁組件。 Angular預設以Emulated達到封裝style的效果,替class rename或是加上prefix來模擬Shadow DOM。 ```html= <my-app _nghost-ric-0=""> <div _ngcontent-ric-0="" class="card card-block"> <h4 _ngcontent-ric-0="">Title</h4> </div> </my-app> ``` ```typescript= @Component({ styleUrls: ['css/style.css'], templateUrl: 'card.html', }) ``` * None: style 直接被包裝到 `<head>` 的 `<style>` 裡面。他會被產生於執行完「寫在 Component 裡面」的方式之後。 * Emulated: style 被包裝到 `<head>` 的 `<style>` 裡面,但可以辨識要對應的 Component。反而不是引入 link 喔! * Native: 如同預期一般為網頁組件。 ### Component Inheritance * Metadata (decorator): 定義在衍伸的類別的 metadata 像是 @Input、@Output 等,會覆蓋先前所有在繼承鏈裡面的 metadata,不然則採用基本的類別 (父類別) 的 metadata。 * Constructor: 如果衍伸類別沒有 constructure,會採用基本類別的。意思就是說所以注入到 parent constructure 的服務會被 child constructure 繼承。 * Lifecycle hooks: 生命週期鉤子像是 ngOnInit、ngOnChanges會被呼叫,就算他們在衍伸類別沒有被定義。 ```typescript= // simple-pagination.component.ts import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'simple-pagination', template: ` <button (click)="previousPage()" [disabled]="!hasPrevious()">Previous</button> <button (click)="nextPage()" [disabled]="!hasNext()">Next</button> <p>page {{ page }} of {{ pageCount }}</p> ` }) export class SimplePaginationComponent { @Input() pageCount: number; @Input() page: number; @Output() pageChanged = new EventEmitter<number>(); nextPage() { this.page ++; this.pageChanged.emit(this.page); } previousPage() { this.page --; this.pageChanged.emit(this.page); } hasPrevious(): boolean { return +this.page > 1; } hasNext(): boolean { return +this.page < +this.pageCount; } } ``` ```typescript= // my-pagination.component.ts import { Component } from '@angular/core'; import { SimplePaginationComponent } from './simple-pagination.component'; @Component({ selector: 'my-pagination', template: ` <a (click)="previousPage()" [class.disabled]="!hasPrevious()" href="javascript:void(0)"> «« </a> <span>{{ page }} / {{ pageCount }}</span> <a (click)="nextPage()" [class.disabled]="!hasNext()" href="javascript:void(0)" > »» </a> ` }) export class MyPaginationComponent extends SimplePaginationComponent { } ``` 不同 Component 之間傳遞 inputs、@input() 由上到下 outputs、@output() 由下到上